diff --git a/plugins/aidd-refine/skills/05-improve/SKILL.md b/plugins/aidd-refine/skills/05-improve/SKILL.md index 61f51420e..fd24b0e7a 100644 --- a/plugins/aidd-refine/skills/05-improve/SKILL.md +++ b/plugins/aidd-refine/skills/05-improve/SKILL.md @@ -8,29 +8,17 @@ argument-hint: conversation | export ```mermaid flowchart LR start([conversation ID or export]) --> read-conversation - read-conversation -->|complete| scopes{two or more scopes?} + read-conversation -->|complete| recommend --> target-edits read-conversation -->|unavailable| unavailable([stop]) - scopes -->|no| recommend-local[recommend locally] --> target-edits - scopes -->|yes| isolation{isolated artifact context?} - isolation -->|yes| recommend-parallel[recommend in parallel] --> target-edits - isolation -->|no| recommend-local target-edits --> question([ask next intent]) --> stop([stop]) ``` ## Actions -Run the flow above. Read only the next action file. +Run all three actions in order without confirmation. Read only the next file in `actions/`. Keep the project read-only; write only the unique temporary report. -| Action | Does | +| Order | Action | | --- | --- | -| read-conversation | freeze complete evidence and measure visible cost | -| recommend | analyze relevant scopes and merge grounded findings | -| target-edits | render minimal edits and an executable prompt | - -## Transversal rules - -- After resolving the source, run all three actions without pausing for confirmation. -- Analyze only a complete conversation and the exact skills or documents it names. -- Exclude every current or previous `improve` invocation and report from the analysis evidence. -- Never invent time, tokens, source coverage, or document status. -- Do not modify, stage, or persist any project file. +| 1 | `01-read-conversation.md` | +| 2 | `02-recommend.md` | +| 3 | `03-target-edits.md` | diff --git a/plugins/aidd-refine/skills/05-improve/actions/01-read-conversation.md b/plugins/aidd-refine/skills/05-improve/actions/01-read-conversation.md index 1b146ce3a..02a09fa04 100644 --- a/plugins/aidd-refine/skills/05-improve/actions/01-read-conversation.md +++ b/plugins/aidd-refine/skills/05-improve/actions/01-read-conversation.md @@ -1,6 +1,6 @@ # 01 - Read conversation -Load complete conversation evidence once and profile its visible cost. +Freeze complete evidence and measure visible cost. ## Input @@ -8,28 +8,22 @@ A current conversation, an exact conversation ID, or a complete transcript expor ## Output -An evidence boundary, a `## Timing` table with `Activity | Observed time | Share | Evidence`, a `## Usage` table with `Metric | Value | Evidence`, and a scope index. +- Evidence boundary and resource index. +- Chronological timeline with stable event IDs: `Moment | Actor/tool | Action/source | Observed duration | Result`. +- Timing: `Activity | Observed time | Share | Evidence`. +- Usage: `Metric | Value | Evidence`. ## Process -1. **Resolve.** Choose the host-specific transcript source from [conversation sources](../assets/conversation-sources.md). - - Stop when no complete transcript can be resolved for the exact conversation. -2. **Bound.** Freeze the evidence at the message before the current invocation, or at the supplied export boundary. -3. **Read.** Load every in-scope message, tool call, tool result, and timestamp from that transcript. -4. **Index.** Record relevant turns and invoked or named skills and knowledge files for downstream analysis. -5. **Measure.** Calculate visible time and usage only from timestamps, elapsed records, or host usage data. - - Group explicit tool activity as `research and diagnosis`, `implementation`, or `validation`; keep mixed or unknown time `unattributed`. - - Include tokens, requests, and monetary cost only when the source exposes them. - - Never label unattributed wait as reasoning time. -6. **Render.** Mark a missing metric `unavailable` and order known activity times from longest to shortest. - -## Test - -| Case | Pass | -| --- | --- | -| A conversation is analyzed | its source resolves to one exact complete transcript | -| The same conversation is analyzed again | its evidence excludes every `improve` invocation and report | -| A timing metric is shown | its evidence identifies visible timestamp or elapsed records | -| A metric is unavailable | the report does not estimate or call it reasoning time | -| A usage metric is shown | its evidence identifies the host record that exposes it | -| A scope is indexed | it identifies exact turns or artifact paths, not a generated summary | +1. **Resolve.** Use the host route in [conversation sources](../assets/conversation-sources.md). Prefer its complete export or host reader. Match the exact session ID before direct storage; search only for that ID, never unrelated sessions, configuration, or authentication. Stop unless the exact complete transcript is available. +2. **Freeze.** End before this invocation, or at the export boundary. Exclude every `improve` invocation and report. +3. **Read once.** Load every in-scope message, tool call, result, and timestamp. Index relevant turns; invoked skills and their used actions or resources; applicable `AGENTS.md`; and task-relevant memory references. Do not read maintained sources yet. + - Record one timeline row per message, call, or result, including failures and retries. Link call/result pairs; retain full text, arguments, and output. + - Preserve transcript order. Identify agents, linked calls, and parallel activity when exposed; do not invent hidden calls. +4. **Measure.** Use only timestamps, elapsed records, and exposed host usage. + - Group tool time as `research and diagnosis`, `implementation`, `validation`, or `unattributed`. + - Attribute each call duration once; never sum overlapping intervals into elapsed time. Mark unknown durations `unavailable`. + - Include tokens, requests, and cost only when exposed; otherwise mark them `unavailable`. + - Separate background-process lifetime from blocking time. + - Treat private reasoning duration as unavailable unless exposed directly; never infer it. +5. **Deliver.** Cite every measured value. Order only activity totals by descending time; keep the timeline chronological. diff --git a/plugins/aidd-refine/skills/05-improve/actions/02-recommend.md b/plugins/aidd-refine/skills/05-improve/actions/02-recommend.md index 6e8f2681f..49d54832a 100644 --- a/plugins/aidd-refine/skills/05-improve/actions/02-recommend.md +++ b/plugins/aidd-refine/skills/05-improve/actions/02-recommend.md @@ -1,50 +1,33 @@ # 02 - Recommend -Analyze each relevant scope with the smallest grounded change. +Reduce avoidable work without reducing reliability. ## Input -The evidence boundary, timing and usage tables, complete conversation, and scope index. +The evidence boundary, timeline, timing and usage tables, complete conversation, and resource index. ## Output -A `## Recommendations` table with `ID | Question | Type | Diagnostic | Evidence | Smallest change | Target | Saving`. +Coverage with `Scope | Status | Evidence`, and recommendations with `ID | Focus | Type | Diagnostic | Evidence | Smallest change | Source | Saving`. Pass through the boundary, timeline, and measurements unchanged. ## Process -1. **Scope.** Select `behavior`, plus `skill` and `knowledge` only when the scope index names relevant artifacts. -2. **Dispatch.** Analyze locally when one scope exists; otherwise dispatch one read-only analyst per scope in parallel. - - Prefer a lightweight available model and low reasoning effort when the host supports per-agent overrides; otherwise inherit the run defaults. - - Give the behavior analyst the complete frozen transcript; give artifact analysts the same boundary, indexed turns, and exact artifact paths. - - Request isolated or minimal context for artifact analysts when supported; otherwise analyze every scope locally instead of duplicating the transcript. - - Do not dispatch an analyst with no relevant evidence. - - Require table rows only and forbid file writes. -3. **Question.** Make each analyst answer every prompt for its scope. - - How could the next run be faster or better? - - What information should be removed or clarified? - - Where should the change live? - - How could it save time or tokens? - - What work was counterproductive? -4. **Verify.** Read a named skill or knowledge file before assessing its information. -5. **Assess.** Label relevant information `obsolete`, `over-specific-or-time-bound`, `duplicate`, `inconsistent`, `counterproductive`, or `correct`. - - Use `correct` when no evidence supports another label, and never render it as a recommendation. -6. **Merge.** Deduplicate findings across scopes and verify only their cited evidence against the frozen source. -7. **Render.** Order by question then `behavior`, `skill`, `knowledge`, and describe each change with the fewest unambiguous words. - - Use `skill`, `behavior`, `knowledge`, or `tooling` as the target type. - - State `time`, `tokens`, `both`, or `unknown` as its saving. - - Render `no change` when a scope has no evidence-backed recommendation. - -## Test - -| Case | Pass | -| --- | --- | -| A question is shown | it is answered from conversation evidence | -| One relevant scope exists | no parallel analyst is dispatched | -| Several relevant scopes exist | their analysts run in parallel and return the same columns | -| An artifact analyst cannot receive isolated context | every scope is analyzed locally instead of duplicating the transcript | -| A type is shown | it is `skill`, `behavior`, `knowledge`, or `tooling` | -| Information is assessed | it has one allowed label backed by evidence | -| Information is correct | its scope says `no change` and no recommendation is rendered | -| A named target is assessed | the target file was read before the verdict | -| A saving is shown | it is categorical and never an invented amount | -| The same evidence is analyzed again | recommendations keep the same order and do not cite an earlier `improve` report | +1. **Verify.** Resolve and read indexed maintained skill resources, applicable `AGENTS.md`, and task-relevant memory references once; reuse a still-valid read. Add and read only relevant paths from the project memory index; never scan unrelated memory. Neither judge from nor target caches or installs. Account for host transforms. Mark only read sources `checked`; mark unresolved or skipped ones `missing` or `not reviewed`. +2. **Scope.** Always assess `behavior`; add `skill` or `knowledge` only for checked indexed sources. +3. **Dispatch.** With multiple isolated scopes, run one read-only analyst per scope in parallel. Otherwise analyze locally. + - Prefer a cheap model and low reasoning effort when per-agent overrides are supported; otherwise inherit host defaults. + - Give the behavior analyst the frozen transcript. Give artifact analysts the boundary, indexed turns, and verified excerpts with evidence; allow a targeted read-only refresh only when validity is uncertain. + - Give every analyst steps 4–6 and require table rows. +4. **Reflect.** Ask yourself one question per scope, internally: "Based on the conversation, what shorter, equally reliable path could have achieved the same result, and what minimal reusable change would reduce time, tokens, or cost next run?" +5. **Assess.** Use `obsolete`, `over-specific-or-time-bound`, `duplicate`, `inconsistent`, `counterproductive`, or `correct`. + - Assess repeated reads, back-and-forth, bottlenecks, and calls to remove, batch, parallelize, or replace. Keep checks for mutable state or unresolved uncertainty; reuse valid results. + - Raw call count never proves waste. Ground the smallest general correction in evidence. + - Recommend memory only for concise, durable knowledge that prevents recurring rediscovery, never transient state or one-off implementation details. + - For skills, especially knowledge: delete evidence-backed waste or inconsistency first, without quota; then consolidate or clarify. + - Reuse or strengthen existing content; add only for a demonstrated gap it cannot cover. Preserve useful context and requirements; classify sufficient content as `correct` => `no change`. + - Ground savings in blocking-time, resource-impact, or exposed cost evidence. State trade-offs; background-process uptime alone proves no saving. +6. **Deliver.** Deduplicate, verify cited evidence, and order by focus, then `behavior`, `skill`, `knowledge`. Report findings, not the questionnaire, in the fewest actionable words. + - Every recommendation needs an exact maintained source path, current excerpt, explicit `−`/`+` correction, relevant event IDs, and a short target/action prompt. Leave `+` empty for deletion-only edits. + - Disclose unresolved sources in coverage. Omit findings without a useful persistent edit; do not invent recommendations. + - Type: `skill`, `behavior`, `knowledge`, or `tooling`. + - Saving: `time`, `tokens`, `cost`, their combination, or `unknown`. diff --git a/plugins/aidd-refine/skills/05-improve/actions/03-target-edits.md b/plugins/aidd-refine/skills/05-improve/actions/03-target-edits.md index 53bc43068..aa8dbe195 100644 --- a/plugins/aidd-refine/skills/05-improve/actions/03-target-edits.md +++ b/plugins/aidd-refine/skills/05-improve/actions/03-target-edits.md @@ -1,35 +1,22 @@ # 03 - Target edits -Map recommendations to minimal edits and render the report. +Render the evidence and minimal edits. ## Input -The timing, usage, and recommendations tables. +The evidence boundary, timeline, timing, usage, coverage, and recommendations. ## Output -An HTML report in a temporary directory with local `report.css` and `report.js`, plus its path and the next-intent question. +An HTML report in a temporary directory with local `report.css` and `report.js`, plus its path and a next-intent request. ## Process -1. **Target.** Map each file-targeted recommendation to a real project path. - - Omit behavior-only recommendations from this table. -2. **Summarize.** Build a `Fichier | + Ajout | − Retrait ou clarification` table. - - Consolidate repeated targets and render each diagnostic's smallest change. - - Render one `Aucun fichier recommandé | — | —` row when no recommendation needs a file edit. -3. **Render.** Fill [the report template](../assets/report-template.html) with only measured values and grounded findings, then copy its local CSS and JavaScript beside it. - - Remove every sample value and sample finding from the produced report. - - HTML-escape every injected value; allow only template-owned markup and local asset references. - - Write only to a unique temporary directory, never the project. -4. **Return.** Provide the report path and end with this exact question: `Quel changement d’intention général, même minime, appliquons-nous au prochain run pour rendre notre amélioration cumulative et mesurable ?` - -## Test - -| Case | Pass | -| --- | --- | -| A file is shown | it is the target of an earlier recommendation | -| No file needs editing | the empty-table row is shown | -| The report is rendered | its HTML, CSS, and JavaScript resolve locally with no network dependency | -| Conversation evidence is rendered | it is escaped as text and cannot create markup, scripts, or URLs | -| A value is unavailable | the report says `unavailable` instead of showing sample data | -| The run closes | the returned chat response ends with the exact next-intent question | +1. **Render.** Fill [the report template](../assets/report-template.html) from the provided data; remove its sample and copy its local CSS and JavaScript into the allowed unique temporary directory. Never resolve, judge, or apply sources here. + - Escape injected values; allow only template markup and local assets. Localize HTML `lang`, UI text, and dynamic `data-label-*` text; preserve quoted evidence. + - Start with elapsed seconds, tokens, and call counts only. Use exposed usage only; distinguish linked subcalls. No visible dates or coverage. Keep text sizes uniform except titles and user prompts. + - Show full user prompts in labelled blockquotes, followed by compact calls: tool, relative path or command, and elapsed gap since the previous displayed event. Nest only exposed subcalls; preserve chronological order and parallel relationships. + - Derive gaps only from recorded timestamps; distinguish them from call durations. Omit assistant prose, raw payloads, and evidence explanations. No dropdowns. + - Put recommendations below: an actionable title, one relative path, and unlabelled red `−` / green `+` columns. Use `—` for an empty side; omit descriptions and proof links. + - Keep black emoji tags, accept controls, and the editable prompt; copy each `data-prompt` unchanged. No filters, print control, target table, or limitations section. +2. **Return.** Return its path. Ask the user, in their language, which small general change in intent to apply next run for cumulative, measurable improvement. diff --git a/plugins/aidd-refine/skills/05-improve/assets/conversation-sources.md b/plugins/aidd-refine/skills/05-improve/assets/conversation-sources.md index 329cd7da7..c4a689fbe 100644 --- a/plugins/aidd-refine/skills/05-improve/assets/conversation-sources.md +++ b/plugins/aidd-refine/skills/05-improve/assets/conversation-sources.md @@ -7,11 +7,3 @@ Use this static routing map to load one complete conversation. Do not fill or co | Codex | host thread reader with the exact thread ID, then TUI `/export` Markdown | `CODEX_HOME/history.jsonl`, normally `~/.codex/history.jsonl` | TUI `/status` estimated thread credits or cost; tool elapsed records when exposed | | Claude Code | `/export` text, or the documented script interface for an exact session ID | `~/.claude/projects/{project}/{session-id}.jsonl` | transcript timestamps and tool records when exposed | | OpenCode | `opencode export {session-id} --sanitize` JSON | `~/.local/share/opencode/project/{project-slug}/storage/` | `opencode stats`; `~/.local/share/opencode/log/` | - -## Rules - -- Prefer the complete export or host reader over direct storage parsing. -- Use direct storage only after matching one exact session ID. -- Search shared history or logs only for the exact session ID and ignore unrelated records. -- Never enumerate unrelated sessions or inspect configuration or authentication files. -- Treat private reasoning duration as unavailable unless the host exposes it directly. diff --git a/plugins/aidd-refine/skills/05-improve/assets/report-template.html b/plugins/aidd-refine/skills/05-improve/assets/report-template.html index 0c5300815..d571102aa 100644 --- a/plugins/aidd-refine/skills/05-improve/assets/report-template.html +++ b/plugins/aidd-refine/skills/05-improve/assets/report-template.html @@ -1,13 +1,5 @@ - - + @@ -18,191 +10,109 @@ -
+
-

Conversation actuelle · couverture partielle

-

Rapport d’amélioration

-

- Les changements les plus courts qui rendront le prochain run plus rapide, - plus fiable et moins coûteux. -

-
-

Généré12 septembre 2026 · 10:42

-

SourceConversation + fichiers cités

-

Findings3 recommandations

-
+

Improve report

+

Fictional example.

-
-
-

Mesure

Coût de la session

- Données partielles -
-
-
Durée active24 minmesurée
-
Tokens128,4 kentrée + sortie + cache
-
Requêtes42tous outils
-
Coût monétaireInconnunon exposé par le fournisseur
+
+
+

Elapsed9 s

+

TokensUnavailable

+

Calls · includes 2 subcalls6

-

Source : aidd telemetry report v8 · 2026-09-12T02:00Z → 2026-09-12T03:00Z

-
-
-

Chronologie

Où le temps est parti

- 24 min au total -
-
    -
  1. 01
    Recherche et diagnostic12 min · 50 %
    100 %
  2. -
  3. 02
    Implémentation8 min · 33 %
    66 %
  4. -
  5. 03
    Validation4 min · 17 %
    33 %
  6. -
-
+
+

Conversation

-
-
-

Décisions

Changements recommandés

-

3 recommandations

-
+
+
+
User promptStart
+

Review the project guidance for repetition and unnecessary output. Suggest changes only.

+
-
-
- Diagnostic - - - - - - -
-
- Cible - - - - - +
+
+
read_fileAGENTS.md+2 s
+
+
+
parallel2 linked read_file subcalls+2 s
+
+
+
read_filedocs/example-review.md+0.1 s
+
+
+
read_filedocs/example-guidance.md+0.1 s
+
+
+
+
+
read_filedocs/example-review-notes.md ENOENT+0.8 s
+
+
+
shellrg --files docs -g '*review*' retry+1 s
+
-
+
-
-
-
-
F-001
-
-
Plus rapideObsolèteToolingMettre à jour
-

La version flottante casse le reload

-
-

ÉconomieTemps

- -
-
-
-

Pourquoi

-

latest pointe désormais vers une CLI dont la commande framework build n’existe plus.

-

Preuve

-

Commande observée : error: unknown command 'build'

-
-
-
scripts/dev-sync.sh
-
−AIDD_CLI_VERSION="${AIDD_CLI_VERSION:-latest}"
-+AIDD_CLI_VERSION="${AIDD_CLI_VERSION:-5.2.2}"
-
-
-
+
+
+
User prompt+2 s
+

Show replacements and deletions side by side. Do not invent a new rule for the deleted duplicate.

+
+
+
+ +
+

Recommended changes

-
+
+
-
F-002
-
-
Économiser des tokensDoublonSkillFusionner
-

Deux règles décrivent le même contrôle

-
-

ÉconomieTokens

- +
Counterproductive Behavior

Replace the full-output requirement

+
-
-

Pourquoi

-

Le routeur et l’action répètent le même invariant. Une seule source suffit.

-

Preuve

-

Même exigence relevée dans SKILL.md:24 et actions/02-recommend.md:31.

-
-
skills/05-improve/SKILL.md
-
−Verify every recommendation against evidence.
- The recommend action owns evidence verification.
+
docs/example-review.md
+
+
−Before replying, print every tool result in full.
+
+Report the result and the shortest evidence needed to verify it.
+
- -
+
-
F-003
-
-
Supprimer ou clarifierIncohérentKnowledgeClarifier
-

La documentation promet un coût indisponible

-
-

ÉconomieTemps

- +
Duplicate Knowledge

Remove the second publishing rule

+
-
-

Pourquoi

-

Le montant exact n’est pas exposé. Le rapport doit distinguer valeur inconnue et zéro.

-

Preuve

-

cost_micro_usd absent des observations de cette session.

-
-
aidd_docs/memory/telemetry.md
-
−Le coût de chaque session est affiché.
-+Le coût est affiché lorsqu’il est mesuré ; sinon : Inconnu.
+
docs/example-guidance.md
+
+
−Validate before publishing.
+

—

+
- -
- -
-

Synthèse

Fichiers ciblés

3 fichiers
-
- - - - - - - -
Fichier+ Ajout− Retrait ou clarification
scripts/dev-sync.shÉpingler une version compatibleRetirer latest
skills/05-improve/SKILL.mdConserver un propriétaireRetirer la règle dupliquée
aidd_docs/memory/telemetry.mdExpliciter l’absence de mesureRemplacer la promesse absolue
-
diff --git a/plugins/aidd-refine/skills/05-improve/assets/report.css b/plugins/aidd-refine/skills/05-improve/assets/report.css index 1f5a54a28..2fea03391 100644 --- a/plugins/aidd-refine/skills/05-improve/assets/report.css +++ b/plugins/aidd-refine/skills/05-improve/assets/report.css @@ -3,296 +3,105 @@ --red-dark: color-mix(in oklch, var(--red) 72%, black); --red-soft: color-mix(in oklch, var(--red) 12%, white); --green: #66cc99; - --green-dark: color-mix(in oklch, var(--green) 62%, black); + --green-dark: color-mix(in oklch, var(--green) 58%, black); --green-soft: color-mix(in oklch, var(--green) 15%, white); --ink: oklch(0.16 0.01 25); - --ink-soft: oklch(0.38 0.01 25); - --muted: oklch(0.53 0.008 25); + --soft-ink: oklch(0.4 0.01 25); --line: oklch(0.87 0.006 25); - --line-strong: oklch(0.72 0.01 25); - --surface: oklch(1 0 0); - --surface-soft: oklch(0.975 0.003 25); - --add-bg: var(--green-soft); - --add-ink: var(--green-dark); - --del-bg: var(--red-soft); - --del-ink: var(--red-dark); + --strong-line: oklch(0.72 0.01 25); + --surface: white; + --soft-surface: oklch(0.975 0.003 25); --focus: oklch(0.64 0.18 250); --mono: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace; } * { box-sizing: border-box; } - -html { scroll-behavior: smooth; } - -body { - margin: 0; - background: var(--surface); - color: var(--ink); - font-family: var(--mono); - font-size: 14px; - line-height: 1.6; -} - -button, a { font: inherit; } - -button:focus-visible, -a:focus-visible { - outline: 3px solid var(--focus); - outline-offset: 3px; -} - +body { margin: 0; background: var(--surface); color: var(--ink); font: 13px/1.6 var(--mono); } +button, textarea { font: inherit; } +button:focus-visible, a:focus-visible, textarea:focus { outline: 3px solid var(--focus); outline-offset: 3px; } code { font-family: var(--mono); } - -.site-header { - min-height: 58px; - padding: 0 32px; - border-bottom: 1px solid var(--line); - display: flex; - align-items: center; - justify-content: space-between; - gap: 24px; - position: sticky; - top: 0; - z-index: 10; - background: color-mix(in oklch, var(--surface) 94%, transparent); - backdrop-filter: blur(12px); -} - -.brand { - color: var(--ink); - text-decoration: none; - display: inline-flex; - align-items: center; - gap: 10px; - font-weight: 700; - letter-spacing: -0.02em; -} - -.brand-mark { - width: 25px; - height: 25px; - display: grid; - place-items: center; - background: var(--red); - color: var(--surface); - font-size: 13px; -} - -.header-actions { display: flex; align-items: center; gap: 16px; } -.run-id { color: var(--muted); font-size: 12px; } - -.button { - border: 1px solid var(--ink); - background: var(--ink); - color: var(--surface); - padding: 7px 12px; - cursor: pointer; -} - -.button:hover { background: var(--red); border-color: var(--red); } - -main { width: min(1120px, calc(100% - 48px)); margin: 0 auto; } - -.intro { padding: 72px 0 44px; border-bottom: 1px solid var(--ink); } -.context, .section-index { margin: 0 0 8px; color: var(--red); font-size: 12px; font-weight: 700; } -.context::before, .section-index::before { content: "# "; } - -h1, h2, h3, h4, p { text-wrap: pretty; } -h1 { max-width: 16ch; margin: 0; font-size: 44px; line-height: 1.08; letter-spacing: -0.04em; } -.lede { max-width: 68ch; margin: 22px 0 34px; color: var(--ink-soft); font-size: 16px; } - -.run-meta { margin: 0; display: flex; flex-wrap: wrap; gap: 20px 48px; } -.run-meta p { margin: 0; display: grid; gap: 2px; } -.run-meta span { color: var(--muted); font-size: 11px; } -.run-meta strong { font-size: 12px; } - -.section { padding: 52px 0 56px; border-bottom: 1px solid var(--line); } -.section-heading { display: flex; align-items: flex-end; justify-content: space-between; gap: 24px; margin-bottom: 24px; } -.section-heading h2 { margin: 0; font-size: 23px; line-height: 1.2; letter-spacing: -0.035em; } -.section-total, .result-count { margin: 0; color: var(--muted); font-size: 12px; } - -.coverage { display: inline-flex; align-items: center; gap: 8px; color: var(--ink-soft); font-size: 12px; } -.coverage::before { content: ""; width: 8px; height: 8px; border-radius: 50%; background: var(--red); } - -.metrics { display: grid; grid-template-columns: repeat(4, 1fr); border-top: 1px solid var(--ink); border-bottom: 1px solid var(--line); } -.metric { min-height: 132px; padding: 20px; border-right: 1px solid var(--line); display: flex; flex-direction: column; } -.metric:last-child { border-right: 0; } -.metric > span { color: var(--muted); font-size: 11px; } -.metric strong { margin-top: auto; font-size: 25px; line-height: 1.2; letter-spacing: -0.04em; font-variant-numeric: tabular-nums; } -.metric small { color: var(--muted); font-size: 10px; } -.metric-unknown strong { color: var(--red-dark); font-size: 18px; } -.source-note { margin: 12px 0 0; color: var(--muted); font-size: 11px; } -.source-note code { color: var(--ink-soft); } - -.timing-list { padding: 0; margin: 0; list-style: none; border-top: 1px solid var(--ink); } -.timing-list li { min-height: 74px; border-bottom: 1px solid var(--line); display: grid; grid-template-columns: 44px minmax(220px, 1fr) minmax(160px, 2fr); align-items: center; gap: 20px; } -.timing-rank { color: var(--red); font-weight: 700; } -.timing-list li > div { display: grid; } -.timing-list li > div span { color: var(--muted); font-size: 11px; } -.timing-bar { - width: 100%; - height: 7px; - border: 1px solid var(--line); - border-radius: 0; - background: var(--surface-soft); - appearance: none; -} -.timing-bar::-webkit-progress-bar { background: var(--surface-soft); } -.timing-bar::-webkit-progress-value { background: var(--red); } -.timing-bar::-moz-progress-bar { background: var(--red); } - -.filters { margin-bottom: 24px; padding: 14px 0; border-top: 1px solid var(--ink); border-bottom: 1px solid var(--line); display: flex; flex-wrap: wrap; gap: 16px 36px; } -.filter-group { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; } -.filter-group > span { margin-right: 5px; color: var(--muted); font-size: 11px; } -.filter-group button { - border: 1px solid var(--line-strong); - border-radius: 999px; - padding: 4px 9px; - background: var(--surface); - color: var(--ink-soft); - font-size: 11px; - cursor: pointer; -} -.filter-group button:hover { border-color: var(--ink); color: var(--ink); } -.filter-group button[aria-pressed="true"] { border-color: var(--ink); background: var(--ink); color: var(--surface); } +a { color: var(--red-dark); text-underline-offset: 3px; } + +.site-header { min-height: 58px; padding: 0 32px; border-bottom: 1px solid var(--line); display: flex; align-items: center; position: sticky; top: 0; z-index: 10; background: color-mix(in oklch, white 94%, transparent); backdrop-filter: blur(12px); } +.brand { color: var(--ink); text-decoration: none; display: flex; align-items: center; gap: 10px; font-weight: 700; } +.brand-mark { width: 25px; height: 25px; display: grid; place-items: center; background: var(--red); color: var(--ink); } +.button { border: 1px solid var(--ink); padding: 7px 12px; background: var(--ink); color: white; cursor: pointer; } +.button:hover { border-color: var(--red); background: var(--red); color: var(--ink); } + +main { width: min(1040px, calc(100% - 48px)); margin: auto; } +.intro { padding: 24px 0 16px; } +.intro h1 { margin: 0; font-size: 28px; line-height: 1.2; letter-spacing: -0.03em; } +.intro p { margin: 8px 0 0; color: var(--soft-ink); } +.section { padding: 24px 0; border-bottom: 1px solid var(--line); } +.section-heading, .prompt-heading { display: flex; align-items: end; justify-content: space-between; gap: 20px; margin-bottom: 20px; } +.section-heading h2, .prompt-heading h2 { margin: 0; font-size: 18px; letter-spacing: -0.03em; } +.prompt-heading span { color: var(--soft-ink); } + +.recap { padding-bottom: 16px; border-bottom: 1px solid var(--line); } +.metrics { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); border: 1px solid var(--line); font-variant-numeric: tabular-nums; } +.metrics p { margin: 0; padding: 12px 16px; display: grid; gap: 4px; align-content: start; overflow-wrap: anywhere; } +.metrics p + p { border-left: 1px solid var(--line); } +.metrics span { color: var(--soft-ink); } + +.conversation-turn + .conversation-turn { margin-top: 30px; } +.user-message { margin: 0 0 16px; padding: 16px; border-left: 4px solid var(--ink); background: #f2f2f2; } +.message-meta { display: flex; flex-wrap: wrap; align-items: center; gap: 12px; color: var(--soft-ink); font-variant-numeric: tabular-nums; } +.message-meta > span:first-child { color: var(--ink); font-weight: 700; } +.message-meta .event-delta { margin-left: auto; } +.user-prompt { margin: 8px 0 0; font-size: 18px; line-height: 1.45; letter-spacing: -0.02em; } +.tool-calls { padding-left: 28px; } +.tool-call { margin: 12px 0; } +.tool-line { display: grid; grid-template-columns: 92px minmax(0, 1fr) auto; align-items: baseline; gap: 10px; padding: 4px 0; } +.tool-name { color: var(--soft-ink); font-weight: 700; } +.tool-target { overflow-wrap: anywhere; } +.event-delta { color: var(--soft-ink); white-space: nowrap; font-variant-numeric: tabular-nums; } +.call-error { color: var(--red-dark); background: var(--red-soft); padding: 1px 4px; } +.call-status { color: var(--soft-ink); } +.subcalls { margin-left: 18px; padding-left: 16px; border-left: 1px solid var(--line); } .findings { border-top: 1px solid var(--ink); } .finding { border-bottom: 1px solid var(--ink); } -.finding[hidden] { display: none; } -.finding-header { padding: 26px 0 22px; display: grid; grid-template-columns: 62px minmax(0, 1fr) auto auto; gap: 20px; align-items: start; transition: padding 180ms ease-out; } -.finding-id { color: var(--red); font-weight: 700; font-size: 12px; } -.finding-title h3 { margin: 11px 0 0; font-size: 19px; line-height: 1.3; letter-spacing: -0.025em; } +.finding-header { padding: 22px 0; display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 18px; align-items: start; } +.finding-title h3 { margin: 9px 0 0; font-size: 18px; line-height: 1.3; } .tags { display: flex; flex-wrap: wrap; gap: 6px; } -.tag { padding: 3px 7px; border: 1px solid var(--line-strong); font-size: 10px; line-height: 1.4; } -.tag-question { border-color: var(--ink); background: var(--ink); color: var(--surface); } -.tag-signal { border-color: var(--red); background: var(--red); color: var(--surface); } -.tag-action { border-color: var(--ink); color: var(--ink); } - -.finding-score { margin: 0; display: flex; gap: 24px; } -.finding-score p { margin: 0; display: grid; gap: 2px; } -.finding-score span { color: var(--muted); font-size: 9px; } -.finding-score strong { font-size: 11px; } - -.accept-toggle { - min-height: 31px; - padding: 5px 9px; - border: 1px solid var(--ink); - display: inline-flex; - align-items: center; - gap: 7px; - color: var(--ink); - font-size: 10px; - font-weight: 700; - cursor: pointer; -} -.accept-toggle:hover { border-color: var(--red); color: var(--red-dark); } -.accept-toggle:focus-within { outline: 3px solid var(--focus); outline-offset: 3px; } +.tag { background: black; color: white; padding: 3px 7px; } +.accept-toggle { min-height: 31px; border: 1px solid var(--ink); padding: 5px 9px; display: flex; align-items: center; gap: 7px; font-weight: 700; cursor: pointer; } .accept-input { width: 14px; height: 14px; margin: 0; accent-color: var(--red); } -.accept-input:focus-visible { outline: 0; } - -.finding-collapse { display: grid; grid-template-rows: 1fr; transition: grid-template-rows 200ms ease-out; } -.finding-body { padding: 0 0 30px 82px; display: grid; grid-template-columns: minmax(240px, 0.8fr) minmax(360px, 1.45fr); gap: 36px; transition: padding 180ms ease-out; } -.finding-id, .finding-title, .finding-score, .finding-collapse { transition: opacity 160ms ease-out; } -.finding.is-accepted .finding-header { padding-block: 14px; } -.finding.is-accepted .finding-collapse { grid-template-rows: 0fr; } -.finding.is-accepted .finding-body { min-height: 0; padding-bottom: 0; overflow: hidden; } -.finding.is-accepted .finding-id, -.finding.is-accepted .finding-title, -.finding.is-accepted .finding-score, -.finding.is-accepted .finding-collapse { opacity: 0.48; } +.finding-collapse { display: grid; grid-template-rows: 1fr; transition: grid-template-rows 180ms ease-out, opacity 180ms ease-out; } +.finding-body { min-height: 0; padding-bottom: 26px; overflow: hidden; } +.finding.is-accepted .finding-header { padding-block: 12px; opacity: 0.55; } +.finding.is-accepted .finding-collapse { grid-template-rows: 0fr; opacity: 0.35; } +.finding.is-accepted .finding-body { padding-block: 0; } .finding.is-accepted .accept-toggle { border-color: var(--red); background: var(--red-soft); color: var(--red-dark); } -.finding-copy h4 { margin: 0 0 5px; color: var(--muted); font-size: 10px; } -.finding-copy p { max-width: 62ch; margin: 0 0 18px; color: var(--ink-soft); font-size: 12px; } -.finding-copy .evidence { color: var(--ink); } -.change { align-self: start; border: 1px solid var(--line-strong); background: var(--surface-soft); overflow: hidden; } -.change-header { min-height: 38px; padding: 8px 11px; border-bottom: 1px solid var(--line); display: flex; justify-content: space-between; align-items: center; gap: 16px; font-size: 10px; } -.change-header span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } -.copy-path { padding: 2px 0; border: 0; background: transparent; color: var(--red-dark); font-size: 9px; cursor: pointer; } -.copy-path:hover { text-decoration: underline; } -.diff { margin: 0; padding: 10px 0; overflow-x: auto; font: 11px/1.8 var(--mono); } -.diff code { display: block; min-width: max-content; } -.diff-line { display: block; padding: 0 13px; } +.change { border: 1px solid var(--strong-line); overflow: hidden; } +.change-header { padding: 8px 11px; border-bottom: 1px solid var(--line); } +.change-header code { overflow-wrap: anywhere; } +.diff-columns { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); } +.diff-pane + .diff-pane { border-left: 1px solid var(--line); } +.diff { margin: 0; padding: 11px 0; font: inherit; white-space: pre-wrap; overflow-wrap: anywhere; } +.diff code { display: block; } +.diff-line { display: block; padding: 0 11px; } .diff-line b { display: inline-block; width: 18px; user-select: none; } -.diff-del { background: var(--del-bg); color: var(--del-ink); } -.diff-add { background: var(--add-bg); color: var(--add-ink); } -.diff-del b { color: var(--red-dark); } -.diff-add b { color: var(--green-dark); } -.diff-context { color: var(--muted); } -.empty-state { margin: 0; padding: 48px 0; border-bottom: 1px solid var(--ink); color: var(--muted); text-align: center; } +.diff-del { background: var(--red-soft); color: var(--red-dark); } +.diff-add { background: var(--green-soft); color: var(--green-dark); } +.diff-empty { margin: 0; padding: 11px; color: var(--soft-ink); } -.table-wrap { overflow-x: auto; border-top: 1px solid var(--ink); } -table { width: 100%; min-width: 680px; border-collapse: collapse; font-size: 12px; text-align: left; } -th { padding: 12px 14px; border-bottom: 1px solid var(--ink); color: var(--muted); font-size: 10px; font-weight: 500; } -td { padding: 16px 14px; border-bottom: 1px solid var(--line); vertical-align: top; } -th:first-child, td:first-child { padding-left: 0; } -th:last-child, td:last-child { padding-right: 0; } -td code { color: var(--red-dark); font-weight: 700; } +.prompt-builder { margin: 28px 0; padding-top: 24px; border-top: 4px solid var(--red); } +#execution-prompt { width: 100%; min-height: 220px; padding: 15px; border: 1px solid var(--ink); resize: vertical; background: var(--soft-surface); color: var(--ink); } +.prompt-actions { margin-top: 10px; display: flex; justify-content: end; } -.prompt-builder { margin: 64px 0; padding: 28px 0 0; border-top: 4px solid var(--red); } -.prompt-heading { display: flex; align-items: end; justify-content: space-between; gap: 24px; } -.prompt-heading p { margin: 0 0 5px; color: var(--red); font-size: 11px; font-weight: 700; } -.prompt-heading h2 { margin: 0; font-size: 22px; line-height: 1.3; letter-spacing: -0.025em; } -.prompt-heading > span, .prompt-actions > span { color: var(--muted); font-size: 11px; } -.prompt-help { max-width: 72ch; margin: 18px 0 12px; color: var(--ink-soft); font-size: 12px; } -#execution-prompt { - width: 100%; - min-height: 230px; - padding: 16px; - border: 1px solid var(--ink); - border-radius: 0; - resize: vertical; - background: var(--surface-soft); - color: var(--ink); - font: 12px/1.7 var(--mono); -} -#execution-prompt:focus { outline: 3px solid var(--focus); outline-offset: 3px; } -.prompt-actions { margin-top: 10px; display: flex; align-items: center; justify-content: space-between; gap: 20px; } - -@media (max-width: 820px) { - .metrics { grid-template-columns: repeat(2, 1fr); } - .metric:nth-child(2) { border-right: 0; } - .metric:nth-child(-n + 2) { border-bottom: 1px solid var(--line); } - .finding-header { grid-template-columns: 50px minmax(0, 1fr) auto; } - .finding-score { grid-column: 2; } - .accept-toggle { grid-column: 3; grid-row: 1 / span 2; } - .finding-body { padding-left: 70px; grid-template-columns: 1fr; } -} - -@media (max-width: 580px) { +@media (max-width: 680px) { .site-header { padding: 0 16px; } - .run-id { display: none; } - main { width: min(100% - 32px, 1120px); } - .intro { padding-top: 48px; } - h1 { font-size: 36px; } - .section-heading { align-items: start; flex-direction: column; gap: 8px; } + main { width: min(100% - 32px, 1040px); } .metrics { grid-template-columns: 1fr; } - .metric { min-height: 105px; border-right: 0; border-bottom: 1px solid var(--line); } - .metric:last-child { border-bottom: 0; } - .timing-list li { padding: 14px 0; grid-template-columns: 30px 1fr; gap: 8px 12px; } - .timing-bar { grid-column: 2; } + .metrics p + p { border-left: 0; border-top: 1px solid var(--line); } + .tool-calls { padding-left: 20px; } + .tool-line { grid-template-columns: 76px minmax(0, 1fr) auto; gap: 6px; } + .subcalls { margin-left: 8px; padding-left: 8px; } .finding-header { grid-template-columns: 1fr; gap: 8px; } - .finding-score { grid-column: 1; flex-wrap: wrap; } - .accept-toggle { grid-column: 1; grid-row: auto; justify-self: start; } - .finding-body { padding-left: 0; } - .finding-id { order: -1; } - .prompt-heading { align-items: start; flex-direction: column; gap: 6px; } - .prompt-actions { align-items: stretch; flex-direction: column; } - .prompt-actions .button { align-self: flex-start; } -} - -@media (prefers-reduced-motion: reduce) { - html { scroll-behavior: auto; } - .finding-header, .finding-id, .finding-title, .finding-score, .finding-collapse { transition-duration: 0.001ms; } + .accept-toggle { justify-self: start; } + .section-heading, .prompt-heading { align-items: start; flex-direction: column; } } -@media print { - .site-header, .filters, .copy-path, .accept-toggle, .prompt-actions { display: none !important; } - body { color: black; font-size: 11px; } - main { width: 100%; } - .intro { padding-top: 0; } - .section, .finding { break-inside: avoid; } - .finding[hidden] { display: block; } - .finding.is-accepted { display: none; } - .change { background: white; } -} +@media (prefers-reduced-motion: reduce) { .finding-collapse { transition-duration: 0.001ms; } } diff --git a/plugins/aidd-refine/skills/05-improve/assets/report.js b/plugins/aidd-refine/skills/05-improve/assets/report.js index 72770d06f..a7801ce11 100644 --- a/plugins/aidd-refine/skills/05-improve/assets/report.js +++ b/plugins/aidd-refine/skills/05-improve/assets/report.js @@ -1,76 +1,59 @@ (() => { - const state = { signal: "all", target: "all" }; - const findings = [...document.querySelectorAll(".finding")]; - const count = document.querySelector("#result-count"); - const empty = document.querySelector("#empty-state"); - const prompt = document.querySelector("#execution-prompt"); - const acceptedCount = document.querySelector("#accepted-count"); - const promptAnchor = "Changements acceptés :"; - const closingPrompt = "Une fois le travail validé"; + const promptMarker = "---"; - function render() { - let visible = 0; - for (const finding of findings) { - const matchesSignal = state.signal === "all" || finding.dataset.signal === state.signal; - const matchesTarget = state.target === "all" || finding.dataset.target === state.target; - finding.hidden = !(matchesSignal && matchesTarget); - if (!finding.hidden) visible += 1; - } - count.textContent = `${visible} recommandation${visible === 1 ? "" : "s"}`; - empty.hidden = visible !== 0; + function formatCount(count, one, other) { + const template = count === 1 ? one : other; + return template ? template.replace("{count}", String(count)) : String(count); } - document.querySelectorAll("[data-filter]").forEach((button) => { - button.addEventListener("click", () => { - const kind = button.dataset.filter; - state[kind] = button.dataset.value; - document.querySelectorAll(`[data-filter="${kind}"]`).forEach((candidate) => { - candidate.setAttribute("aria-pressed", String(candidate === button)); - }); - render(); - }); - }); + function composePrompt(value, accepted, savedLines = new Map(), knownIds = new Set(accepted.map(({ id }) => id))) { + const generatedPattern = /^- \[([^\]]+)\] /; + const original = value.split("\n"); + + for (const line of original) { + const match = line.match(generatedPattern); + if (match && knownIds.has(match[1])) savedLines.set(match[1], line); + } - document.querySelectorAll(".copy-path").forEach((button) => { - button.addEventListener("click", async () => { - const original = button.textContent; - try { - await navigator.clipboard.writeText(button.dataset.copy); - button.textContent = "Copié"; - } catch { - button.textContent = "Copie indisponible"; - } - window.setTimeout(() => { button.textContent = original; }, 1400); + const lines = original.filter((line) => { + const match = line.match(generatedPattern); + return !match || !knownIds.has(match[1]); }); - }); + const generated = accepted.map(({ id, prompt }) => savedLines.get(id) || `- [${id}] ${prompt}`); + const markerIndex = lines.findIndex((line) => line.trim() === promptMarker); + lines.splice(markerIndex >= 0 ? markerIndex : lines.length, 0, ...generated); + return lines.join("\n"); + } - function updatePrompt(changedFinding) { - const accepted = findings.filter((finding) => finding.querySelector(".accept-input").checked); - const changedInput = changedFinding.querySelector(".accept-input"); - const id = changedFinding.querySelector(".finding-id").textContent.trim(); - const prefix = `- [${id}] `; - const generatedLine = `${prefix}${changedInput.dataset.prompt}`; - const lines = prompt.value.split("\n").filter((line) => !line.startsWith(prefix)); - const anchorIndex = lines.findIndex((line) => line.trim() === promptAnchor); + if (typeof module !== "undefined") module.exports = { composePrompt, formatCount }; + if (typeof document === "undefined") return; - if (changedInput.checked) { - const closingIndex = lines.findIndex((line) => line.startsWith(closingPrompt)); - let insertAt = closingIndex >= 0 ? closingIndex : anchorIndex + 1; - if (lines[insertAt - 1] === "") insertAt -= 1; - lines.splice(Math.max(insertAt, 0), 0, generatedLine); - } - prompt.value = lines.join("\n"); + const report = document.querySelector("#report"); + const findings = [...document.querySelectorAll(".finding")]; + const prompt = document.querySelector("#execution-prompt"); + const acceptedCount = document.querySelector("#accepted-count"); + const savedPromptLines = new Map(); + const findingIds = new Set(findings.map((finding) => finding.dataset.id)); + + function updatePrompt() { + const accepted = findings.filter((finding) => finding.querySelector(".accept-input").checked); + const entries = accepted.map((finding) => ({ + id: finding.dataset.id, + prompt: finding.querySelector(".accept-input").dataset.prompt, + })); + prompt.value = composePrompt(prompt.value, entries, savedPromptLines, findingIds); const total = accepted.length; - acceptedCount.textContent = `${total} recommandation${total === 1 ? "" : "s"} acceptée${total === 1 ? "" : "s"}`; + acceptedCount.textContent = formatCount(total, report.dataset.labelAcceptedOne, report.dataset.labelAcceptedOther); } document.querySelectorAll(".accept-input").forEach((input) => { input.addEventListener("change", () => { const finding = input.closest(".finding"); finding.classList.toggle("is-accepted", input.checked); - finding.querySelector(".accept-toggle span").textContent = input.checked ? "Accepté" : "Accepter"; - updatePrompt(finding); + const label = finding.querySelector(".accept-toggle span"); + label.textContent = input.checked ? input.dataset.labelOn : input.dataset.labelOff; + updatePrompt(); }); }); @@ -79,14 +62,12 @@ const original = button.textContent; try { await navigator.clipboard.writeText(prompt.value); - button.textContent = "Prompt copié"; + button.textContent = button.dataset.labelSuccess || original; } catch { prompt.focus(); prompt.select(); - button.textContent = "Texte sélectionné"; + button.textContent = button.dataset.labelError || original; } window.setTimeout(() => { button.textContent = original; }, 1600); }); - - document.querySelector("#print-report").addEventListener("click", () => window.print()); })(); diff --git a/scripts/__tests__/check-written-file-hook.test.js b/scripts/__tests__/check-written-file-hook.test.js index 16b8a9242..260f50cd7 100644 --- a/scripts/__tests__/check-written-file-hook.test.js +++ b/scripts/__tests__/check-written-file-hook.test.js @@ -14,7 +14,8 @@ function runHook(payload) { } function probe(name, content) { - const file = path.join(root, "cli/src", `.hook-probe-${process.pid}-${name}.ts`); + // Keep transient probes outside directories scanned by repository-wide tests. + const file = path.join(root, "cli", `.hook-probe-${process.pid}-${name}.ts`); fs.writeFileSync(file, content); return file; } diff --git a/scripts/__tests__/improve-report.test.js b/scripts/__tests__/improve-report.test.js new file mode 100644 index 000000000..8a81779ea --- /dev/null +++ b/scripts/__tests__/improve-report.test.js @@ -0,0 +1,176 @@ +const assert = require('node:assert/strict'); +const { readFileSync } = require('node:fs'); +const { resolve } = require('node:path'); +const test = require('node:test'); +const { runInNewContext } = require('node:vm'); + +const { composePrompt, formatCount } = require(resolve( + __dirname, + '../../plugins/aidd-refine/skills/05-improve/assets/report.js', +)); + +test('accepted edits keep report order and preserve manual prompt text', () => { + const saved = new Map(); + const known = new Set(['F-001', 'F-002']); + const base = [ + 'User introduction.', + '', + 'Accepted changes:', + '- [NOTE] Keep this manual note.', + '', + '---', + '', + 'User closing.', + ].join('\n'); + + let value = composePrompt(base, [{ id: 'F-002', prompt: 'Second edit.' }], saved, known); + value = value.replace('- [F-002] Second edit.', '- [F-002] Second edit, manually refined.'); + value = composePrompt(value, [ + { id: 'F-001', prompt: 'First edit.' }, + { id: 'F-002', prompt: 'Second edit.' }, + ], saved, known); + + assert.ok(value.indexOf('[F-001]') < value.indexOf('[F-002]')); + assert.match(value, /\[F-002\] Second edit, manually refined\./); + assert.match(value, /^User introduction\./); + assert.match(value, /- \[NOTE\] Keep this manual note\./); + assert.match(value, /User closing\.$/); + assert.ok(value.indexOf('[F-002]') < value.indexOf('---')); + + value = composePrompt(value, [{ id: 'F-001', prompt: 'First edit.' }], saved, known); + assert.doesNotMatch(value, /\[F-002\]/); + value = composePrompt(value, [ + { id: 'F-001', prompt: 'First edit.' }, + { id: 'F-002', prompt: 'Second edit.' }, + ], saved, known); + assert.match(value, /\[F-002\] Second edit, manually refined\./); + assert.equal(composePrompt(value, [ + { id: 'F-001', prompt: 'First edit.' }, + { id: 'F-002', prompt: 'Second edit.' }, + ], saved, known), value); +}); + +test('prompt insertion is independent of localized headings', () => { + for (const [heading, closing] of [ + ['Accepted changes:', 'After validation, propose one improvement.'], + ['Changements acceptés :', 'Après validation, propose une amélioration.'], + ]) { + const base = ['Introduction.', '', heading, '', '---', '', closing].join('\n'); + const value = composePrompt(base, [{ id: 'F-001', prompt: 'Apply the edit.' }]); + + assert.ok(value.indexOf('[F-001]') < value.indexOf('---')); + assert.ok(value.indexOf('---') < value.indexOf(closing)); + assert.match(value, new RegExp(heading.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))); + } +}); + +test('missing prompt marker preserves manual text and appends accepted edits safely', () => { + const base = ['Manual introduction.', '', 'Manual closing.'].join('\n'); + const value = composePrompt(base, [{ id: 'F-001', prompt: 'Apply the edit.' }]); + + assert.match(value, /^Manual introduction\./); + assert.match(value, /Manual closing\./); + assert.ok(value.indexOf('Manual closing.') < value.indexOf('[F-001]')); +}); + +test('count labels come from localized HTML data', () => { + assert.equal(formatCount(1, '{count} recommendation', '{count} recommendations'), '1 recommendation'); + assert.equal(formatCount(2, '{count} recommandation', '{count} recommandations'), '2 recommandations'); +}); + +function loadReport() { + function element(fields = {}) { + const listeners = new Map(); + return { + ...fields, + addEventListener: (name, listener) => listeners.set(name, listener), + dispatch(name) { return listeners.get(name)({ currentTarget: this }); }, + }; + } + + const prompt = element({ value: 'Manual introduction.\n\n---\nManual closing.' }); + const acceptedCount = element({ textContent: '' }); + const copy = element({ textContent: 'Copy', dataset: { labelSuccess: 'Copied' } }); + const writes = []; + const findings = ['concise-output', 'duplicate-publishing-rule'].map((id) => { + const classes = new Set(); + const label = { textContent: 'Accepter' }; + const input = element({ + checked: false, + dataset: { prompt: `Apply ${id}.`, labelOff: 'Accepter', labelOn: 'Accepté' }, + }); + const finding = { + dataset: { id }, + classes, + input, + label, + classList: { toggle: (name, enabled) => enabled ? classes.add(name) : classes.delete(name) }, + querySelector: (selector) => ({ + '.accept-input': input, + '.accept-toggle span': label, + })[selector], + }; + input.closest = () => finding; + return finding; + }); + const document = { + querySelector: (selector) => ({ + '#report': { dataset: { labelAcceptedOne: '{count} acceptée', labelAcceptedOther: '{count} acceptées' } }, + '#execution-prompt': prompt, + '#accepted-count': acceptedCount, + '#copy-prompt': copy, + })[selector] || null, + querySelectorAll: (selector) => ({ + '.finding': findings, + '.accept-input': findings.map(({ input }) => input), + })[selector] || [], + }; + + runInNewContext(readFileSync(resolve( + __dirname, + '../../plugins/aidd-refine/skills/05-improve/assets/report.js', + ), 'utf8'), { + document, + navigator: { clipboard: { writeText: async (value) => writes.push(value) } }, + window: { setTimeout() {} }, + }); + return { prompt, acceptedCount, copy, findings, writes }; +} + +test('acceptance uses metadata IDs without visible ID nodes and retains manual edits', () => { + const { prompt, acceptedCount, findings } = loadReport(); + const second = findings[1]; + second.input.checked = true; + second.input.dispatch('change'); + + assert.ok(second.classes.has('is-accepted')); + assert.equal(second.label.textContent, 'Accepté'); + assert.equal(acceptedCount.textContent, '1 acceptée'); + assert.match(prompt.value, /\[duplicate-publishing-rule\] Apply duplicate-publishing-rule\./); + + prompt.value = prompt.value.replace('Apply duplicate-publishing-rule.', 'Keep my edited instruction.'); + second.input.checked = false; + second.input.dispatch('change'); + assert.ok(!second.classes.has('is-accepted')); + assert.equal(second.label.textContent, 'Accepter'); + assert.equal(acceptedCount.textContent, '0 acceptées'); + assert.doesNotMatch(prompt.value, /\[duplicate-publishing-rule\]/); + + second.input.checked = true; + second.input.dispatch('change'); + assert.match(prompt.value, /\[duplicate-publishing-rule\] Keep my edited instruction\./); + assert.match(prompt.value, /^Manual introduction\./); + assert.match(prompt.value, /Manual closing\.$/); +}); + +test('copy reads the current editable prompt after acceptance', async () => { + const { prompt, copy, findings, writes } = loadReport(); + findings[0].input.checked = true; + findings[0].input.dispatch('change'); + prompt.value += '\nAn extra manual instruction.'; + + await copy.dispatch('click'); + + assert.deepEqual(writes, [prompt.value]); + assert.equal(copy.textContent, 'Copied'); +});