@entelligentsia/forgecli 0.11.3 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/CHANGELOG.md +314 -0
  2. package/README.md +2 -1
  3. package/dist/CHANGELOG-forge-plugin.md +183 -0
  4. package/dist/bin/forge.js +20 -1
  5. package/dist/bin/forge.js.map +1 -1
  6. package/dist/extensions/forgecli/config-layer.d.ts +15 -0
  7. package/dist/extensions/forgecli/config-layer.js.map +1 -1
  8. package/dist/extensions/forgecli/enhance.js +1 -1
  9. package/dist/extensions/forgecli/enhance.js.map +1 -1
  10. package/dist/extensions/forgecli/forge-cli-schema.json +19 -0
  11. package/dist/extensions/forgecli/forge-tools.js +80 -0
  12. package/dist/extensions/forgecli/forge-tools.js.map +1 -1
  13. package/dist/extensions/forgecli/forge-update-command.js +24 -18
  14. package/dist/extensions/forgecli/forge-update-command.js.map +1 -1
  15. package/dist/extensions/forgecli/friction-emit.d.ts +97 -0
  16. package/dist/extensions/forgecli/friction-emit.js +246 -0
  17. package/dist/extensions/forgecli/friction-emit.js.map +1 -0
  18. package/dist/extensions/forgecli/hook-dispatcher.js +20 -0
  19. package/dist/extensions/forgecli/hook-dispatcher.js.map +1 -1
  20. package/dist/extensions/forgecli/index.js +29 -5
  21. package/dist/extensions/forgecli/index.js.map +1 -1
  22. package/dist/extensions/forgecli/regenerate.d.ts +22 -0
  23. package/dist/extensions/forgecli/regenerate.js +133 -3
  24. package/dist/extensions/forgecli/regenerate.js.map +1 -1
  25. package/dist/extensions/forgecli/skill-curation-flag.d.ts +21 -0
  26. package/dist/extensions/forgecli/skill-curation-flag.js +71 -0
  27. package/dist/extensions/forgecli/skill-curation-flag.js.map +1 -0
  28. package/dist/extensions/forgecli/skill-curator-subagent.d.ts +101 -0
  29. package/dist/extensions/forgecli/skill-curator-subagent.js +342 -0
  30. package/dist/extensions/forgecli/skill-curator-subagent.js.map +1 -0
  31. package/dist/extensions/forgecli/skill-retriever.d.ts +84 -0
  32. package/dist/extensions/forgecli/skill-retriever.js +246 -0
  33. package/dist/extensions/forgecli/skill-retriever.js.map +1 -0
  34. package/dist/extensions/forgecli/skill-usage-tracker.d.ts +91 -0
  35. package/dist/extensions/forgecli/skill-usage-tracker.js +224 -0
  36. package/dist/extensions/forgecli/skill-usage-tracker.js.map +1 -0
  37. package/dist/forge-payload/.base-pack/workflows/enhance.md +331 -11
  38. package/dist/forge-payload/.claude-plugin/plugin.json +1 -1
  39. package/dist/forge-payload/.schemas/event.schema.json +20 -2
  40. package/dist/forge-payload/.schemas/migrations.json +96 -0
  41. package/dist/forge-payload/.schemas/proposal.schema.json +40 -0
  42. package/dist/forge-payload/agents/store-query-validator.md +103 -0
  43. package/dist/forge-payload/agents/tomoshibi.md +185 -0
  44. package/dist/forge-payload/commands/regenerate.md +109 -20
  45. package/dist/forge-payload/hooks/check-update.js +378 -0
  46. package/dist/forge-payload/hooks/forge-permissions.js +158 -0
  47. package/dist/forge-payload/hooks/triage-error.js +71 -0
  48. package/dist/forge-payload/hooks/validate-write.js +236 -0
  49. package/dist/forge-payload/integrity.json +32 -0
  50. package/dist/forge-payload/meta/workflows/meta-enhance.md +331 -11
  51. package/dist/forge-payload/schemas/structure-manifest.json +511 -0
  52. package/dist/forge-payload/tools/compression-gate.cjs +192 -0
  53. package/dist/forge-payload/tools/delete-candidate-detector.cjs +114 -0
  54. package/dist/forge-payload/tools/judge-proposal.cjs +177 -0
  55. package/dist/forge-payload/tools/manage-versions.cjs +132 -4
  56. package/dist/forge-payload/tools/queue-drain.cjs +152 -0
  57. package/dist/forge-payload/tools/replay-scoring.cjs +117 -0
  58. package/node_modules/@mariozechner/clipboard/package.json +2 -1
  59. package/node_modules/@mariozechner/clipboard-linux-x64-musl/README.md +3 -0
  60. package/node_modules/@mariozechner/clipboard-linux-x64-musl/clipboard.linux-x64-musl.node +0 -0
  61. package/node_modules/@mariozechner/clipboard-linux-x64-musl/package.json +25 -0
  62. package/package.json +4 -2
@@ -38,6 +38,40 @@ Phases 2 and 3 write proposal artifacts to `.forge/enhancement-proposals/`. This
38
38
  distinct from `.forge/enhancements/` (FR-007, S14 scope). This workflow uses `mkdir -p` before
39
39
  writing the first proposal artifact to avoid assuming the directory exists. No conflict with S14.
40
40
 
41
+ ### Sub-directory: `.forge/enhancement-proposals/queue/`
42
+
43
+ FORGE-S24-T07 introduces a project-local **enhancement queue** at
44
+ `.forge/enhancement-proposals/queue/<sprintId>/<taskId>-<ts>.json` — one file per
45
+ per-task curator run (T10). The queue is **append-only**: each curator run writes
46
+ a fresh file (the ISO compact `<ts>` suffix differentiates writes; nothing is
47
+ overwritten). Phase 2 drains the queue at sprint close, dedupes by
48
+ `{op, target_path, sha256(diff_body)}`, and feeds the merged batch into the
49
+ existing recurrence → delete-candidate → compression-gate → judge pipeline.
50
+ The result: **one batched review prompt per sprint, not one per task** (paper
51
+ §3.2.1 grouped reward). The drain is read-only — Phase 2 never deletes queue
52
+ files; operators triage them during retrospective if needed.
53
+
54
+ **Per-task curator (T10) write contract.** A curator MUST write via
55
+ `forge/tools/queue-drain.cjs` to preserve the append-only invariant:
56
+
57
+ ```sh
58
+ node -e "
59
+ const { appendToQueue } = require('./forge/tools/queue-drain.cjs');
60
+ appendToQueue({
61
+ queueRoot: '.forge/enhancement-proposals/queue',
62
+ sprintId: process.env.FORGE_SPRINT_ID,
63
+ taskId: process.env.FORGE_TASK_ID,
64
+ ts: new Date().toISOString().replace(/[-:]|\\.\\d{3}/g, ''),
65
+ proposals: PROPOSALS_ARRAY,
66
+ });
67
+ "
68
+ ```
69
+
70
+ `appendToQueue` throws if the exact file path already exists; curators MUST
71
+ choose a fresh `ts` per run rather than overwriting. The drain is empty-safe:
72
+ if no curator ever wrote (queue dir missing) or no files exist in the sprint
73
+ sub-dir, Phase 2 reports "no proposals" and exits cleanly (AC5).
74
+
41
75
  ## Confidence gating (Phase 1)
42
76
 
43
77
  A key substitution is **high-confidence** when there is exactly one unambiguous signal source
@@ -179,11 +213,40 @@ Invoked by T09 post-sprint hook or manually via `/forge:enhance --phase 2`.
179
213
  "
180
214
  ```
181
215
 
182
- 2. **Zero-friction guard**: If the friction event list is empty, print:
216
+ 1a. **Drain enhancement queue** (FORGE-S24-T07) read per-task curator
217
+ proposals from `.forge/enhancement-proposals/queue/<sprintId>/`, dedupe by
218
+ `{op, target_path, sha256(diff_body)}`, and produce a `queuedProposals`
219
+ array that joins the synthesised proposals from step 5. This is what makes
220
+ the review **batched** rather than per-task (paper §3.2.1):
221
+
222
+ ```sh
223
+ node -e "
224
+ const { drainQueue } = require('./forge/tools/queue-drain.cjs');
225
+ const drained = drainQueue({
226
+ queueRoot: '.forge/enhancement-proposals/queue',
227
+ sprintId: process.env.FORGE_SPRINT_ID,
228
+ });
229
+ process.stdout.write(JSON.stringify(drained));
230
+ "
231
+ ```
232
+
233
+ Contract (per `forge/tools/queue-drain.cjs`):
234
+ - Returns `{ proposals: [...], files: [...], errors: [...] }`. `proposals`
235
+ is the deduped union of every per-task curator file in the sprint
236
+ sub-dir. `files` is the lexicographic-sorted list of source paths (used
237
+ by step 6 to log provenance). `errors` carries any malformed JSON files
238
+ skipped during read — log them, do not abort.
239
+ - Empty / missing queue → empty result. The drain never throws on absent
240
+ queue dir (first-run or no curators registered yet, AC5).
241
+ - The drain is read-only. Operators are responsible for queue triage
242
+ after sprint close.
243
+
244
+ 2. **Zero-input guard**: If both the friction event list AND `queuedProposals`
245
+ are empty, print:
183
246
  ```
184
- No friction events queued for the active sprint — nothing to enhance.
247
+ No friction events or queued proposals for the active sprint — nothing to enhance.
185
248
  ```
186
- and exit Phase 2 immediately (skip steps 3–9; emit the enhancement event with `"notes": "{\"phase\":2,\"frictionCount\":0}"`). Do not create `.forge/enhancement-proposals/` when there are no proposals.
249
+ and exit Phase 2 immediately (skip steps 3–9; emit the enhancement event with `"notes": "{\"phase\":2,\"frictionCount\":0,\"queuedCount\":0}"`). Do not create `.forge/enhancement-proposals/` when there are no proposals.
187
250
 
188
251
  3. **Deduplicate** friction events by composite key `workflow + persona + issue`. Keep the most
189
252
  recent occurrence of each composite key.
@@ -192,19 +255,276 @@ Invoked by T09 post-sprint hook or manually via `/forge:enhance --phase 2`.
192
255
  `retrospective-done`), sorted by completion date. Read its task records from
193
256
  `.forge/store/tasks/` filtered by the sprint ID.
194
257
 
195
- 5. **Synthesize enrichment proposals** — for each friction event:
196
- - Identify which persona or skill file it references.
197
- - Propose a targeted addition: e.g., "architect persona lacks routing pattern knowledge —
198
- suggest adding `{{KB_PATH}}/routing.md` reference to deps.kb_docs."
199
- - For large committed file sets (> 5 files in the sprint), also check whether
200
- `engineer-skills.md` or `architect-skills.md` should reference new patterns.
258
+ 5. **Synthesize enrichment proposals** — for each friction event, classify the proposed
259
+ change into exactly one of three ops (see `forge/schemas/proposal.schema.json`):
260
+
261
+ | `op` | When to use |
262
+ |-----------------|-----------------------------------------------------------------------------|
263
+ | `insert_skill` | A new skill / persona / kb_docs reference is needed; target file does not yet carry the guidance. |
264
+ | `update_skill` | An existing skill or persona file needs revised guidance — e.g., add a routing pattern reference to `deps.kb_docs`, replace a stale instruction. |
265
+ | `delete_skill` | A skill is unused, redundant, or stale (`skill_unused` / `skill_redundant` / `skill_stale` friction subkinds); target file or section should be removed. |
266
+
267
+ For each proposal capture **at minimum** the schema-required triplet
268
+ `{op, target_path, diff_body}` plus optional `rationale` and `sourceFrictionIds`.
269
+ `sourceFrictionIds` MUST carry the `eventId` of every friction event that
270
+ contributed to the proposal — the next step depends on it to resolve the
271
+ originating task for the recurrence scan.
272
+ For large committed file sets (> 5 files in the sprint), also check whether
273
+ `engineer-skills.md` or `architect-skills.md` should be updated (`update_skill`).
274
+ The op classification is the foundation for the downstream judge (T03),
275
+ delete-candidate detection (T05), compression gate (T06), and queue drain (T07).
276
+
277
+ **Merge with queued proposals (T07).** Concatenate the synthesised
278
+ proposals built in this step with the `queuedProposals` array from
279
+ step 1a, then dedupe the combined array with the same key the drain
280
+ uses (`{op, target_path, sha256(diff_body)}`) so a friction-synthesised
281
+ proposal that happens to be byte-identical to a curator-queued one
282
+ collapses. Use:
283
+
284
+ ```sh
285
+ node -e "
286
+ const { dedupeProposals } = require('./forge/tools/queue-drain.cjs');
287
+ // synthesised = proposals built above from friction events.
288
+ // queued = drained.proposals from step 1a.
289
+ const merged = dedupeProposals(synthesised.concat(queued));
290
+ process.stdout.write(JSON.stringify(merged));
291
+ "
292
+ ```
293
+
294
+ The merged array is what feeds steps 5a (recurrence) → 5b (delete
295
+ candidates) → 5b.5 (compression gate) → 5c (judge) — a single batched
296
+ pipeline, never one per task (AC4).
297
+
298
+ 5a. **Cross-task replay scoring (recurrence boost)** — before writing the
299
+ artifact, stamp each proposal with `recurrence_count` and
300
+ `recurrence_task_ids` so the T03 judge can score "this friction recurred
301
+ across N tasks" rather than treating every signal as a singleton:
302
+
303
+ ```sh
304
+ node -e "
305
+ const { annotateProposals } = require('./forge/tools/replay-scoring.cjs');
306
+ // friction = deduped friction events from step 3, each carrying eventId,
307
+ // taskId, subkind, evidence.skillId (orchestrator-stamped).
308
+ // proposals = array built in step 5.
309
+ // taskOrder = task IDs of the most-recent sprint sorted by completion
310
+ // order — same source as step 4.
311
+ const annotated = annotateProposals(proposals, friction, taskOrder);
312
+ process.stdout.write(JSON.stringify(annotated));
313
+ "
314
+ ```
315
+
316
+ Contract (per `forge/tools/replay-scoring.cjs`):
317
+ - `recurrence_count` is the number of distinct tasks (origin task + later
318
+ tasks in `taskOrder`) whose friction events match the proposal's
319
+ originating `(subkind, evidence.skillId)` pair. Always `>= 1`.
320
+ - `recurrence_task_ids` is the `taskOrder`-sorted list of those task IDs.
321
+ - Proposals whose `sourceFrictionIds` cannot be resolved (no matching
322
+ `eventId` in the friction set, or the resolved event lacks
323
+ `subkind`/`evidence.skillId`) receive `recurrence_count: 1` and an empty
324
+ `recurrence_task_ids: []` — neutral signal, not silent failure.
325
+ - The annotator returns new proposal objects; the input array is not
326
+ mutated.
327
+
328
+ 5b. **Delete-candidate detection (3-sprint zero-use)** — scan `skill_usage`
329
+ events across the trailing 3 sprints and emit a `delete_skill` proposal
330
+ for every skill with zero retrieval AND zero invocation across the
331
+ window. This is the only mechanism by which the skill repository shrinks:
332
+
333
+ ```sh
334
+ node -e "
335
+ const { buildDeleteProposals } = require('./forge/tools/delete-candidate-detector.cjs');
336
+ // skillUsageEvents = all events with type === 'skill_usage' across the
337
+ // sprints in scope (collected via the same Step 1
338
+ // walker, filtered by type instead of friction).
339
+ // sprintOrder = sprint IDs sorted by completion order (oldest →
340
+ // newest). The detector takes the trailing windowSize
341
+ // entries.
342
+ // windowSize = 3 by default; configurable. Defined as the trailing
343
+ // N sprints of sprintOrder.
344
+ // targetPathFor = (skillId) => the on-disk path of the skill file to
345
+ // delete. Workflow chooses the mapping convention.
346
+ const deletes = buildDeleteProposals({
347
+ events: skillUsageEvents,
348
+ sprintOrder,
349
+ windowSize: 3,
350
+ targetPathFor: (skillId) => 'forge/skills/' + skillId + '.md',
351
+ });
352
+ process.stdout.write(JSON.stringify(deletes));
353
+ "
354
+ ```
355
+
356
+ Append the resulting `delete_skill` proposals to the proposal array from
357
+ step 5/5a before step 6. Each delete proposal already carries
358
+ `recurrence_count: 1` and `recurrence_task_ids: []` (the annotator from
359
+ step 5a is for friction-derived proposals; delete candidates come from
360
+ usage telemetry, not friction, so recurrence is neutral by construction).
361
+
362
+ 5b.5. **Compression gate (reject >20% growth without 3+ frictions)** — a cheap
363
+ deterministic filter that runs BEFORE the LLM judge (step 5c). Any
364
+ `update_skill` proposal that would grow the target file by more than 20%
365
+ (byte-wise, UTF-8) must be backed by at least 3 supporting friction events;
366
+ otherwise it is rejected here and never reaches the judge. `insert_skill`
367
+ and `delete_skill` proposals pass through unconditionally — insert growth
368
+ is handled by the judge's `body_under_2kb` axis and delete only shrinks.
369
+
370
+ Why a pre-judge gate? Judging is expensive. Unbounded skill-body growth is
371
+ the classic SkillOS failure mode — pasting pages of trajectory copy-paste to
372
+ "patch" a friction. It is cheap to detect deterministically and wasteful to
373
+ ask the judge to rule on.
374
+
375
+ ```sh
376
+ node -e "
377
+ const fs = require('node:fs');
378
+ const path = require('node:path');
379
+ const { filterProposals } = require('./forge/tools/compression-gate.cjs');
380
+ // proposals = post-5b array (synthesis + recurrence + delete-candidates).
381
+ // PROJECT_ROOT resolves the target_path; forge plugin source is the source
382
+ // of truth for current bodies. The workflow renders the diff via its own
383
+ // applyProposalDiff helper (left abstract here — the gate is body-agnostic).
384
+ const projectRoot = process.env.PROJECT_ROOT;
385
+ const result = filterProposals({
386
+ proposals,
387
+ currentBodyFor: (p) => {
388
+ const abs = path.join(projectRoot, p.target_path);
389
+ try { return fs.readFileSync(abs, 'utf8'); }
390
+ catch (e) { return ''; } // insert_skill or missing file → empty
391
+ },
392
+ newBodyFor: (p) => applyProposalDiff(currentBodyFor(p), p),
393
+ // Default supporting count = proposal.sourceFrictionIds.length. Override
394
+ // if the policy is 'count frictions citing the same skill across the
395
+ // sprint' rather than 'count citations on the proposal itself'.
396
+ });
397
+ const proposalsAfterGate = result.admitted;
398
+ const compressionRejections = result.rejected; // [{ proposal, ...evaluation }]
399
+ process.stdout.write(JSON.stringify({ kept: proposalsAfterGate, rejected: compressionRejections }));
400
+ "
401
+ ```
402
+
403
+ **Logging gate rejections.** Append every rejection from this step to the
404
+ same `phase2-<timestamp>-rejections.json` sibling that step 5c uses, with
405
+ the rejection record carrying `{ proposal, admit: false,
406
+ reason: 'compression_gate_growth_unsupported', growthRatio, currentBytes,
407
+ newBytes, supportingFrictionCount, threshold, minSupportingFrictions }`.
408
+ This keeps every drop — gate or judge — traceable in one place.
409
+
410
+ Contract (per `forge/tools/compression-gate.cjs`):
411
+ - `GROWTH_THRESHOLD === 0.20`; comparison is **strict** (`> 0.20`). A
412
+ proposal at exactly 20% growth admits without friction support.
413
+ - `MIN_SUPPORTING_FRICTIONS === 3`. Two or fewer citations is not enough.
414
+ - An update on an empty current body yields `growthRatio: Infinity`; the
415
+ friction-support rule still applies.
416
+ - Negative growth (shrink) admits unconditionally.
417
+ - `filterProposals` partitions the input array preserving order; the
418
+ output `rejected` array carries the structured evaluation alongside the
419
+ original proposal.
420
+
421
+ 5c. **LLM-judge gate (Sonnet rubric, drop <3/5)** — score every proposal
422
+ against the 5-axis rubric and drop low-signal proposals before
423
+ presentation. The rubric is single-sourced in
424
+ `forge/tools/judge-proposal.cjs`:
425
+
426
+ | Axis (0..5) | What it measures |
427
+ |---|---|
428
+ | `specificity` | Names a concrete target_path beyond `forge/skills/*` floor; carries a non-trivial rationale; recurrence trail boosts. |
429
+ | `when_not_to_use` | Body contains a literal "When NOT to use" section. |
430
+ | `no_trajectory_copy_paste` | No long verbatim runs or unbroken non-whitespace blocks (>= 400 bytes) that suggest pasted trajectory log. |
431
+ | `body_under_2kb` | `Buffer.byteLength(diff_body, 'utf8') <= 2048`. |
432
+ | `cites_friction` | Proposal carries at least one `sourceFrictionIds` entry; multiple citations or recurrence boost the score. |
433
+
434
+ For each proposal in the post-5b array, the workflow asks Sonnet to
435
+ apply the rubric and emit per-axis 0..5 scores; in the absence of an
436
+ LLM call, the deterministic `scoreProposal(proposal)` helper in
437
+ `judge-proposal.cjs` is used as both the fallback scorer and the
438
+ validation contract for Sonnet-produced scores (single source of truth
439
+ for the rubric definition).
440
+
441
+ ```sh
442
+ node -e "
443
+ const {
444
+ scoreProposal,
445
+ decideJudgement,
446
+ } = require('./forge/tools/judge-proposal.cjs');
447
+ // proposals = post-5b array of proposal records.
448
+ const judged = proposals.map((p) => {
449
+ const scored = scoreProposal(p);
450
+ const decision = decideJudgement(scored);
451
+ return { proposal: p, ...decision };
452
+ });
453
+ const kept = judged.filter((j) => j.verdict === 'keep').map((j) => j.proposal);
454
+ const dropped = judged.filter((j) => j.verdict === 'drop');
455
+ process.stdout.write(JSON.stringify({ kept, dropped }));
456
+ "
457
+ ```
458
+
459
+ Contract (per `forge/tools/judge-proposal.cjs`):
460
+ - `scoreProposal(proposal)` returns `{ axes, average }` with `axes`
461
+ keyed by every entry in `RUBRIC_AXES` and `average` rounded to one
462
+ decimal place.
463
+ - `decideJudgement({ axes })` returns
464
+ `{ verdict, average, axes, reason }`. `verdict === 'drop'` iff
465
+ `average < 3` (strictly less than); ties at exactly 3.0 keep.
466
+ - `decideJudgement` fails loud on missing or out-of-range axes — the
467
+ judge will NOT silently coerce a malformed score sheet into a verdict.
468
+
469
+ **Logging dropped proposals (AC3).** Every rejection MUST be persisted
470
+ for retro review. Replace the proposal array passed to step 6 with the
471
+ `kept` list, and append the `dropped` list to
472
+ `$PROJECT_ROOT/.forge/enhancement-proposals/phase2-<timestamp>-rejections.json`
473
+ as a sibling artifact. Each rejection record carries the original
474
+ proposal alongside `{ verdict: 'drop', average, axes, reason }`. The
475
+ markdown summary written in step 6 SHOULD include a "Dropped (N)" line
476
+ pointing at the rejections file when N > 0.
477
+
478
+ **Carry-over caveat** — the rubric is deterministic; Sonnet's role is
479
+ to add semantic judgement to axes that the heuristic scorer
480
+ approximates (specificity in particular). When Sonnet is invoked, its
481
+ per-axis scores MUST be validated against the 0..5 range via the same
482
+ `validateAxes` invariant `decideJudgement` enforces. Operators
483
+ investigating an unexpected drop should consult the per-axis trace in
484
+ `reason`.
485
+
486
+ Contract (per `forge/tools/delete-candidate-detector.cjs`):
487
+ - A skill qualifies for deletion iff it has at least one `skill_usage`
488
+ event inside the trailing window AND every in-window observation has
489
+ `retrieved === false` AND `used === false`. Any single `retrieved: true`
490
+ or `used: true` event disqualifies the skill.
491
+ - Skills with zero observations in the window are NOT proposed — this
492
+ case is indistinguishable from a newly-added skill that hasn't been
493
+ loaded yet, so silence is the safe default.
494
+ - Each proposal carries `window_size`, `window_sprint_ids`, and a
495
+ `sourceFrictionIds: []` (delete candidates derive from usage telemetry,
496
+ not friction).
497
+
498
+ **Carry-over caveat** — the trailing-3-sprint window is only meaningful
499
+ once 3 sprints have actually elapsed since `skill_usage` event emission
500
+ landed in FORGE-S24-T01 (forge 0.45.1). During the carry-over period the
501
+ detector still runs over whatever sprintOrder it receives, but the
502
+ signal is noisier: a skill flagged after only one or two sprints of
503
+ history may simply be new or temporarily idle. Operators should treat
504
+ delete proposals from short-history runs as advisory until the full
505
+ window is populated.
201
506
 
202
507
  6. **Write proposal artifact**:
203
508
  ```sh
204
509
  mkdir -p "$PROJECT_ROOT/.forge/enhancement-proposals"
205
510
  ```
206
- Write to `$PROJECT_ROOT/.forge/enhancement-proposals/phase2-<timestamp>.md`. Format:
207
- one section per proposed change, with a fenced diff block showing before/after text.
511
+ Write **two** outputs for each Phase 2 run (using the `kept` list from
512
+ step 5c dropped proposals are persisted separately to the
513
+ `phase2-<timestamp>-rejections.json` sibling described in step 5c):
514
+
515
+ - `phase2-<timestamp>.md` — human-readable markdown, one section per proposal,
516
+ showing op + target_path + a fenced diff block.
517
+ - `phase2-<timestamp>.json` — machine-readable array of proposal records, each
518
+ conforming to `forge/schemas/proposal.schema.json` (required keys: `op`,
519
+ `target_path`, `diff_body`; `op` ∈ {insert_skill, update_skill, delete_skill};
520
+ optional `recurrence_count` ≥ 1 and `recurrence_task_ids` populated by step 5a).
521
+
522
+ **Back-compat on read** — pre-0.45.2 proposal records lack `op`. Downstream
523
+ consumers MUST route legacy records through
524
+ `forge/tools/proposal-normalize.cjs:normaliseProposal()` which defaults the
525
+ missing `op` to `insert_skill` (the only op the prior insert-biased flow
526
+ could produce). Do NOT silently coerce — call the helper explicitly so the
527
+ normalisation is auditable.
208
528
 
209
529
  7. **Present to user**:
210
530
  ```
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "forge",
3
- "version": "0.44.8",
3
+ "version": "0.46.1",
4
4
  "description": "Self-enhancing AI software development lifecycle \u2014 generates project-specific SDLC instances from meta-definitions",
5
5
  "license": "MIT",
6
6
  "author": {
@@ -61,7 +61,9 @@
61
61
 
62
62
  "bug-triaged", "fix-planned", "fix-review-passed", "fix-review-failed",
63
63
  "fix-implemented", "fix-code-review-passed", "fix-code-review-failed",
64
- "fix-approved", "bug-committed"
64
+ "fix-approved", "bug-committed",
65
+
66
+ "skill_usage"
65
67
  ]
66
68
  },
67
69
  "workflow": { "type": "string" },
@@ -89,7 +91,13 @@
89
91
  "haltedAtTaskId": { "type": "string" },
90
92
  "lastError": { "type": "string" },
91
93
  "waveCount": { "type": "integer", "minimum": 1 },
92
- "maxConcurrency": { "type": "integer", "minimum": 1 }
94
+ "maxConcurrency": { "type": "integer", "minimum": 1 },
95
+
96
+ "skillId": { "type": "string", "minLength": 1 },
97
+ "retrieved": { "type": "boolean" },
98
+ "used": { "type": "boolean" },
99
+ "tool_call_success_rate": { "type": "number", "minimum": 0, "maximum": 1 },
100
+ "retrieval_score": { "type": "number", "minimum": 0, "maximum": 1 }
93
101
  },
94
102
  "additionalProperties": false,
95
103
  "allOf": [
@@ -161,6 +169,16 @@
161
169
  "lastError": { "type": "string" }
162
170
  }
163
171
  }
172
+ },
173
+ {
174
+ "if": { "properties": { "type": { "const": "skill_usage" } }, "required": ["type"] },
175
+ "then": {
176
+ "required": [
177
+ "eventId", "sprintId", "taskId",
178
+ "skillId", "retrieved", "used",
179
+ "tool_call_success_rate", "retrieval_score"
180
+ ]
181
+ }
164
182
  }
165
183
  ]
166
184
  }
@@ -1,4 +1,100 @@
1
1
  {
2
+ "0.46.0": {
3
+ "version": "0.46.1",
4
+ "date": "2026-05-22",
5
+ "notes": "build: regenerate base-pack/workflows/enhance.md from meta sources to ship the FORGE-S24 SKILL-CURATION Phase 2 pipeline. The 0.45.1–0.45.7 entries declared `workflows:enhance` in their `regenerate` lists, but the meta→base-pack derivation (node tools/build-base-pack.cjs) was not re-run during the sprint, so the installable workflow stayed at the pre-S24 algorithm and downstream projects migrating to 0.46.0 received a stale workflow that lacked queue-drain (step 1a), recurrence scoring, delete-candidate detection (step 5b), compression gate (step 5b.5), and the LLM-judge step (step 5c). This patch bump regenerates the base-pack copy (init/base-pack/workflows/enhance.md — 280→600 lines, 20 S24 markers landed) and forces a re-copy on any project sitting at 0.46.0 by declaring `workflows:enhance` here. No plugin source change beyond the regenerated workflow file. Additive, non-breaking.",
6
+ "regenerate": ["workflows:enhance"],
7
+ "breaking": false,
8
+ "manual": []
9
+ },
10
+ "0.45.7": {
11
+ "version": "0.46.0",
12
+ "date": "2026-05-22",
13
+ "notes": "chore: FORGE-S24 SKILL-CURATION sprint completion — gated rollout marker (FORGE-S24-T12). No plugin source change in this bump; the seven preceding entries (0.45.1 → 0.45.7) carry the actual SKILL-CURATION machinery: `skill_usage` event variant (T01, 0.45.1), proposal op classification (T02, 0.45.2), recurrence scoring (T04, 0.45.3), delete-candidate detection (T05, 0.45.4), LLM-judge rubric (T03, 0.45.5), compression gate (T06, 0.45.6), and queue-drain (T07, 0.45.7). T12 lands the gated-rollout contract on the forge-cli side via `forgeCli.skillCuration.enabled` (default OFF) — the four forge-cli modules (T08 skill-retriever, T09 skill-usage-tracker, T10 skill-curator-subagent, T11 friction-emit) no-op at entry when the flag is off, so a flag-off run is byte-identical to pre-FORGE-S24 behaviour. This plugin-side minor bump (0.45.7 → 0.46.0) is the convergent terminal marker for the sprint: it signals to operators running `/forge:update` that the full SKILL-CURATION pipeline has shipped end-to-end, and pairs with the forge-cli 0.13.4 → 0.14.0 bump that lands the rollout flag. No regeneration is required (no manifest entry change, no workflow change, no schema change in this bump alone — those landed in 0.45.1–0.45.7). Operators who want to enable the pipeline must (1) upgrade forge-cli to 0.14.0+ AND (2) set `forgeCli.skillCuration.enabled: true` in their project config at `<cwd>/.pi/forge-cli/config.json` (or the env override `FORGE_CLI_SKILL_CURATION_ENABLED=1` for one-shot operator use). Until both conditions hold, the new event variants and friction subkinds remain emitter-silent — the plugin schema continues to accept them on receipt, so a delayed forge-cli upgrade is non-blocking. Additive, non-breaking.",
14
+ "regenerate": [],
15
+ "breaking": false,
16
+ "manual": []
17
+ },
18
+ "0.45.6": {
19
+ "version": "0.45.7",
20
+ "date": "2026-05-22",
21
+ "notes": "feat: queue drain at sprint close — per-task curator → batched review (FORGE-S24-T07). Adds `forge/tools/queue-drain.cjs` exporting `bodyHash(body)` (sha256 hex digest, UTF-8), `dedupeKey(proposal)` (`<op>|<target_path>|<sha256(diff_body)>` composite), `dedupeProposals(proposals)` (first-seen-wins, no-mutation), `queuePathFor({queueRoot, sprintId, taskId, ts})` (canonical `.forge/enhancement-proposals/queue/<sprintId>/<taskId>-<ts>.json` path), `appendToQueue({queueRoot, sprintId, taskId, ts, proposals})` (writes one file per curator run; throws if path exists — append-only invariant per AC1), and `drainQueue({queueRoot, sprintId})` (returns `{proposals, files, errors}` after deduping the sprint sub-dir; empty-safe when queue missing per AC5; skips malformed JSON and reports them via `errors`; deterministic lexicographic file ordering ⇒ chronological because of the ts suffix). Per-task curators (T10) append proposals to the queue throughout the sprint without collision (AC2 — distinct ts per call); Phase 2 reads the queue once at sprint close, dedupes by `{op, target_path, body-hash}` (AC3), and merges the queued proposals with the friction-synthesised ones, then feeds a SINGLE batched array through recurrence (T04) → delete-candidate detection (T05) → compression gate (T06) → judge (T03) — one prompt per sprint, not one per task (AC4, paper §3.2.1 grouped reward). `meta/workflows/meta-enhance.md` Phase 2 gains step 1a (drain queue) and step-5 merge sub-block; the zero-input guard now checks both friction events AND queued proposals before exiting cleanly with `frictionCount:0,queuedCount:0`. The drain is read-only — Phase 2 never deletes queue files; operators triage them during retrospective. Test-first per Iron Law 2: `queue-drain.test.cjs` (19 cases — exports surface, bodyHash sha256 contract + empty-body digest + collision contract, dedupeKey AC3 composite + field-sensitivity, dedupeProposals dedup + order-preservation + no-mutation + empty + non-array, queuePathFor AC1 canonical layout + input validation x3, appendToQueue AC1 fresh-file write + AC2 no-collision + append-only refusal, drainQueue AC5 empty/missing safety + AC3 multi-file dedup merge + AC4 single-batch output + sprint scoping + input validation x2 + malformed-JSON resilience) landed before the helper. Pure helpers; fs-touching helpers are `appendToQueue` and `drainQueue` only. Full suite 1437/1437 (was 1418; +19). Additive, non-breaking.",
22
+ "regenerate": ["tools:queue-drain", "workflows:enhance"],
23
+ "breaking": false,
24
+ "manual": []
25
+ },
26
+ "0.45.5": {
27
+ "version": "0.45.6",
28
+ "date": "2026-05-22",
29
+ "notes": "feat: compression gate — reject >20% growth without 3+ frictions (FORGE-S24-T06). Adds `forge/tools/compression-gate.cjs` exporting `GROWTH_THRESHOLD` (0.20), `MIN_SUPPORTING_FRICTIONS` (3), `evaluateGrowth({currentBody, newBody}) -> {currentBytes, newBytes, growthRatio}`, `evaluateProposal({proposal, currentBody, newBody, supportingFrictionCount}) -> {admit, reason, growthRatio, currentBytes, newBytes, supportingFrictionCount, threshold, minSupportingFrictions, op}`, and `filterProposals({proposals, currentBodyFor, newBodyFor, supportingFrictionCountFor}) -> {admitted, rejected}`. The gate runs in `meta/workflows/meta-enhance.md` Phase 2 as new step 5b.5 BEFORE the LLM judge (5c) — cheap deterministic filter first. Only `update_skill` proposals are gated; `insert_skill` and `delete_skill` pass through unconditionally (insert bloat is the judge's `body_under_2kb` axis; delete only shrinks). Growth is measured byte-wise via `Buffer.byteLength(body, 'utf8')` on the new body that would land after applying the proposal's diff. Threshold comparison is strict (>0.20) so ties at exactly 20% admit. Default supporting friction count = `proposal.sourceFrictionIds.length`; the caller may override via `supportingFrictionCountFor(proposal)` when the policy is to count frictions citing the same skill across the sprint. Updates on an empty current body yield `growthRatio: Infinity` and the friction-support rule still applies; shrinks (negative growth) admit unconditionally. Rejected proposals are persisted to the same `phase2-<timestamp>-rejections.json` sibling that the T03 judge writes, with reason `compression_gate_growth_unsupported` and the full evaluation record alongside the original proposal — every drop (gate or judge) stays traceable. Closes the unbounded-growth failure mode of SkillOS — pasting pages of trajectory copy-paste to 'patch' a friction now requires real recurrence evidence. Test-first per Iron Law 2: `compression-gate.test.cjs` (25 cases — GROWTH_THRESHOLD/MIN_SUPPORTING_FRICTIONS constants, evaluateGrowth shape, UTF-8 multibyte byte counting, empty-current-body Infinity ratio, AC1 0-friction rejection, AC2 3-friction admission, 2-friction insufficiency, AC5 exact-20% strict admission, under-threshold admission, sourceFrictionIds fallback, shrink admission, AC4 insert_skill/delete_skill pass-through, input validation x5, filterProposals AC6 ordering/partition, empty input, no-mutation invariant, callback contracts, supportingFrictionCountFor override) landed before the helper. Pure module; no fs access; no LLM call. Full suite 1418/1418 (was 1393; +25). Additive, non-breaking.",
30
+ "regenerate": ["tools:compression-gate", "workflows:enhance"],
31
+ "breaking": false,
32
+ "manual": []
33
+ },
34
+ "0.45.4": {
35
+ "version": "0.45.5",
36
+ "date": "2026-05-22",
37
+ "notes": "feat: LLM-judge step in Phase 2 — Sonnet rubric, drop <3/5 (FORGE-S24-T03). Adds `forge/tools/judge-proposal.cjs` exporting `RUBRIC_AXES` (frozen 5-axis tuple: specificity, when_not_to_use, no_trajectory_copy_paste, body_under_2kb, cites_friction), `scoreProposal(proposal) -> { axes, average }` (deterministic per-axis 0..5 scorer), and `decideJudgement({ axes }) -> { verdict, average, axes, reason }` (pure aggregator: verdict === 'drop' iff average < 3, strict; ties at 3.0 keep). The rubric is single-sourced in this helper — Sonnet's role is to apply the same axes and emit scores that the helper then validates and aggregates; in the absence of an LLM call the deterministic scorer is the fallback. `decideJudgement` fails loud on missing or out-of-range axes (RangeError) so a malformed score sheet never silently coerces into a verdict. `meta/workflows/meta-enhance.md` Phase 2 gains step 5c calling `scoreProposal` + `decideJudgement` for every proposal between delete-candidate detection (5b) and artifact write (6); kept proposals continue to step 6, dropped proposals are persisted to a sibling `phase2-<timestamp>-rejections.json` carrying the original proposal alongside `{ verdict, average, axes, reason }` so every rejection is traceable for retro review (AC3). Per-axis heuristics: specificity scores deep paths, named (non-generic) skill files, non-trivial rationales, and recurrence trails; when_not_to_use checks for the literal phrase; no_trajectory_copy_paste flags long verbatim runs or unbroken non-whitespace blocks >= 400 bytes; body_under_2kb gates on `Buffer.byteLength(diff_body,'utf8') <= 2048`; cites_friction rewards `sourceFrictionIds.length` >= 1, boosting for multi-citation and recurrence. Consumes recurrence data from T04 in the specificity and cites_friction axes — proposals that recurred across multiple tasks score higher. Test-first per Iron Law 2: `judge-proposal.test.cjs` (13 cases — rubric-axes shape, 5/5 keep fixture, 1/5 drop fixture, body byte boundary at exactly 2048, zero-citation suppression, phrase-presence binary, AC6 explicit 2.8/5 drop fixture, keep + drop end-to-end decisions, exact-3.0 boundary keeps, missing-axis RangeError, out-of-range RangeError) landed before the helper. Pure module; no fs access; no LLM call. Full suite 1393/1393 (was 1380; +13). Additive, non-breaking.",
38
+ "regenerate": ["tools:judge-proposal", "workflows:enhance"],
39
+ "breaking": false,
40
+ "manual": []
41
+ },
42
+ "0.45.3": {
43
+ "version": "0.45.4",
44
+ "date": "2026-05-22",
45
+ "notes": "feat: delete-candidate detection — 3-sprint zero-use (FORGE-S24-T05). Adds `forge/tools/delete-candidate-detector.cjs` exporting `scanZeroUse({events, sprintOrder, windowSize})` and `buildDeleteProposals({events, sprintOrder, windowSize, targetPathFor})`. The detector scans `skill_usage` events across the trailing `windowSize` (default 3) sprints; any skill with at least one in-window observation AND zero `retrieved` AND zero `used` qualifies as a delete candidate. Skills with zero in-window observations are explicitly NOT proposed — that case is indistinguishable from a newly-added skill that hasn't been loaded yet. `meta/workflows/meta-enhance.md` Phase 2 gains step 5b calling `buildDeleteProposals` between recurrence annotation (step 5a) and artifact write (step 6). Each delete proposal carries `op: 'delete_skill'`, `target_path` resolved via the supplied `targetPathFor(skillId)` callback, a diff body and rationale that name the cold skill and the observed sprints, `sourceFrictionIds: []` (usage-derived, not friction-derived), `window_size`, `window_sprint_ids`, plus neutral `recurrence_count: 1` and `recurrence_task_ids: []` so the proposal shape stays uniform with the rest of the Phase 2 array. Carry-over caveat documented in the workflow: the trailing-3-sprint window only becomes meaningful once 3 sprints have elapsed since `skill_usage` emission landed in T01 (0.45.1); short-history runs produce advisory rather than authoritative deletes. This is the only mechanism by which the skill repository shrinks, completing the three-op classification rollout from T02 (insert/update were already in play). Test-first per Iron Law 2: `delete-candidate-detector.test.cjs` (16 cases — three-sprint zero-use baseline, retrieved=true disqualification, used=true disqualification, trailing-window guard against pre-window usage, default windowSize 3, configurable windowSize, never-observed-in-window suppression, empty events, friction-event filtering, short-history carry-over case, no-mutation invariant, multi-skill output, deterministic ordering, callback contract, plus AC restatement cases) landed before the detector. Pure module; no fs access. Full suite 1380/1380 (was 1364; +16). Additive, non-breaking.",
46
+ "regenerate": ["tools:delete-candidate-detector", "workflows:enhance"],
47
+ "breaking": false,
48
+ "manual": []
49
+ },
50
+ "0.45.2": {
51
+ "version": "0.45.3",
52
+ "date": "2026-05-22",
53
+ "notes": "feat: cross-task replay scoring — recurrence boost (FORGE-S24-T04). Adds `forge/tools/replay-scoring.cjs` exporting `computeRecurrence({events, subkind, skillId, fromTaskId, taskOrder})` and `annotateProposals(proposals, frictionEvents, taskOrder)`. For each Phase 2 enrichment proposal synthesised from a friction event at task `t`, the annotator scans tasks `t+1..N` in the same sprint for friction events matching the same `(subkind, evidence.skillId)` pair and stamps `recurrence_count` (>= 1, includes the origin task) and `recurrence_task_ids` (taskOrder-sorted) onto the proposal. Forward-only scan: earlier tasks before `fromTaskId` are excluded. Proposals without resolvable `sourceFrictionIds` (eventId not in the friction set, or the resolved event lacks subkind/evidence.skillId) receive a neutral `recurrence_count: 1` and empty `recurrence_task_ids: []`. `meta/workflows/meta-enhance.md` Phase 2 gains step 5a calling `annotateProposals` between synthesis (step 5) and artifact write (step 6); the step 5 contract is tightened to require `sourceFrictionIds` to carry every contributing eventId so the recurrence scan can resolve provenance. `forge/schemas/proposal.schema.json` gains optional `recurrence_count` (integer minimum:1) and `recurrence_task_ids` (array of strings) properties — additive only, both fields are optional, `additionalProperties: false` continues to reject unknown keys. Feeds T03 (judge) so specificity scoring can boost frictions that recurred across multiple tasks. Test-first per Iron Law 2: `replay-scoring.test.cjs` (11 cases — AC3 three-task recurrence, single-task baseline, forward-only-direction guard, subkind mismatch rejection, skillId mismatch rejection, intra-task dedup, fromTaskId-outside-taskOrder fallback, missing-evidence guard, full annotateProposals happy path, unresolved-provenance neutral case, no-mutation invariant) landed before the helper. Full suite 1364/1364 (was 1353; +11). Additive, non-breaking.",
54
+ "regenerate": ["tools:replay-scoring", "schemas:proposal", "workflows:enhance"],
55
+ "breaking": false,
56
+ "manual": []
57
+ },
58
+ "0.45.1": {
59
+ "version": "0.45.2",
60
+ "date": "2026-05-22",
61
+ "notes": "feat: Phase 2 proposal op classification (FORGE-S24-T02). Adds `forge/schemas/proposal.schema.json` and `forge/tools/proposal-normalize.cjs`. The new schema enumerates `op` ∈ {insert_skill, update_skill, delete_skill} with required `target_path` + `diff_body`; `additionalProperties: false` rejects unknown fields. `meta/workflows/meta-enhance.md` Phase 2 (steps 5–6) now requires every enrichment proposal to carry this triplet and writes a machine-readable `phase2-<timestamp>.json` alongside the existing markdown artifact. Back-compat: legacy proposals without `op` are normalised to `insert_skill` via `proposal-normalize.cjs:normaliseProposal()` — downstream consumers route through the helper explicitly so normalisation is auditable. Foundation for T03 (judge), T05 (delete-candidate detection), T06 (compression gate), T07 (queue drain). Test-first per Iron Law 2: `proposal-schema.test.cjs` (6 cases including missing-op rejection, unknown-op rejection, additionalProperties:false, and the legacy normalisation contract) landed before the schema. Full suite 1353/1353. Additive, non-breaking.",
62
+ "regenerate": ["schemas:proposal"],
63
+ "breaking": false,
64
+ "manual": []
65
+ },
66
+ "0.45.0": {
67
+ "version": "0.45.1",
68
+ "date": "2026-05-22",
69
+ "notes": "feat: event.schema.json — new `skill_usage` event variant (FORGE-S24-T01, plan 08 Phase B). Adds `skill_usage` to the `type` enum and a conditional allOf branch requiring {skillId, retrieved, used, tool_call_success_rate, retrieval_score}. Top-level numeric properties (`tool_call_success_rate`, `retrieval_score`) carry [0,1] declarative bounds; `skillId` is `minLength: 1`. The root `additionalProperties: false` gate continues to reject unknown fields on every variant. Foundation for Sprint S24 SKILL-CURATION: T02 emits, T04 retrieval-score correlation, T05 delete-candidate detection, T08 replay scoring all consume records of this type. No code path change to validate-store.cjs — the existing validateRecord() handles the new allOf branch via the same machinery used for friction / sprint-complete / sprint-halted variants. Additive, non-breaking: existing event records remain valid. Note on target version: TASK_PROMPT requested v0.45.0 but that tag already shipped (snapshot-replay change); bumping to 0.45.1 is the minimum forward step.",
70
+ "regenerate": ["schemas:event"],
71
+ "breaking": false,
72
+ "manual": []
73
+ },
74
+ "0.44.10": {
75
+ "version": "0.45.0",
76
+ "date": "2026-05-21",
77
+ "notes": "feat: Approach A — snapshot replay (forge#107). manage-versions gains a new replay subcommand that restores user-enhanced files captured by /forge:enhance Phase 2 snapshots after /forge:regenerate writes fresh base-pack content. Fulfills layer 3 of the composition contract (Working Artifact = base + snapshot + user_enhancements) declared at manage-versions.cjs:13. regenerate.md updated to invoke replay per category (personas, skills, workflows, templates) between subagent generation and manifest record. Overlay semantics: user-enhanced files retain captured content; later snapshots win on collision. Pairs with v0.44.10 (forge#108 archive-path fix) which made layer 2 functional in the first place.",
78
+ "regenerate": ["commands:regenerate", "tools:manage-versions"],
79
+ "breaking": false,
80
+ "manual": []
81
+ },
82
+ "0.44.9": {
83
+ "version": "0.44.10",
84
+ "date": "2026-05-21",
85
+ "notes": "fix: manage-versions add-snapshot now archives files correctly when --enhanced-elements paths include the .forge/ prefix (forge#108). Previously, every archive directory has been created empty since basePackVersion 0.43.3 — layer 2 of the composition contract was silently a no-op. Tool now strips a leading .forge/ from each element path before joining; both .forge/-relative (e.g. personas/X.md) and project-root-relative (.forge/personas/X.md) forms accepted. Unblocks forge#107 (Approach A — snapshot replay). Pure tool fix.",
86
+ "regenerate": ["tools:manage-versions"],
87
+ "breaking": false,
88
+ "manual": []
89
+ },
90
+ "0.44.8": {
91
+ "version": "0.44.9",
92
+ "date": "2026-05-21",
93
+ "notes": "fix: /forge:regenerate personas + skills silently overwrote manual modifications (forge#106 / FORGE-BUG-037). Pre-write generation-manifest check added to both single-file and full-rebuild paths in commands/regenerate.md, mirroring the workflows + templates pattern. Closes asymmetric modification-detection across the four structural-element categories. Markdown-only fix.",
94
+ "regenerate": ["commands:regenerate"],
95
+ "breaking": false,
96
+ "manual": []
97
+ },
2
98
  "0.44.7": {
3
99
  "version": "0.44.8",
4
100
  "date": "2026-05-21",
@@ -0,0 +1,40 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "forge/proposal.schema.json",
4
+ "title": "EnhancementProposal",
5
+ "description": "A single Phase 2 enrichment proposal emitted by /forge:enhance. Each record carries an op classification (insert/update/delete a skill artifact), the target file path, and a diff body. Foundation for the T03 judge, T05 delete detection, T06 compression gate, and T07 queue drain.",
6
+ "type": "object",
7
+ "required": ["op", "target_path", "diff_body"],
8
+ "properties": {
9
+ "proposalId": { "type": "string", "minLength": 1 },
10
+ "op": {
11
+ "type": "string",
12
+ "enum": ["insert_skill", "update_skill", "delete_skill"],
13
+ "description": "Three-op classification: insert a new skill artifact, update an existing one, or delete an existing one."
14
+ },
15
+ "target_path": {
16
+ "type": "string",
17
+ "minLength": 1,
18
+ "description": "Repository-relative path to the artifact being inserted, updated, or deleted (e.g. forge/skills/engineer-skills.md)."
19
+ },
20
+ "diff_body": {
21
+ "type": "string",
22
+ "description": "Unified-diff body or full file body for inserts. For delete_skill this may be empty — the op + target_path is sufficient."
23
+ },
24
+ "rationale": { "type": "string" },
25
+ "sourceFrictionIds": { "type": "array", "items": { "type": "string" } },
26
+ "generated_at": { "type": "string", "format": "date-time" },
27
+ "sprintId": { "type": "string" },
28
+ "recurrence_count": {
29
+ "type": "integer",
30
+ "minimum": 1,
31
+ "description": "FORGE-S24-T04 — count of distinct tasks within the same sprint that emitted a matching (subkind, evidence.skillId) friction event, including the originating task. Always >= 1."
32
+ },
33
+ "recurrence_task_ids": {
34
+ "type": "array",
35
+ "items": { "type": "string" },
36
+ "description": "FORGE-S24-T04 — taskOrder-sorted list of task IDs that contributed to recurrence_count. Empty when the originating friction event cannot be resolved from sourceFrictionIds."
37
+ }
38
+ },
39
+ "additionalProperties": false
40
+ }