@dzhechkov/skills-feature-adr 1.5.12 → 1.5.14
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.
- package/.dz-manifest.json +8 -8
- package/CHANGELOG.md +14 -0
- package/README.md +84 -3
- package/package.json +1 -1
- package/sbom.json +7 -7
- package/src/commands/init.js +38 -6
- package/templates/.claude/skills/feature-adr/modules/08-qe.md +45 -1
- package/templates/.claude/skills/feature-adr/scripts/check-plan-completeness.mjs +104 -3
- package/templates/.claude/workflows/feature-adr.js +679 -46
package/.dz-manifest.json
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"files": [
|
|
6
6
|
{
|
|
7
7
|
"path": "CHANGELOG.md",
|
|
8
|
-
"sha256": "
|
|
8
|
+
"sha256": "26fa03f89862d03ebfc6447c866cb5d4dcbd5591ae4f8fb3a3ea08b6c586e19d"
|
|
9
9
|
},
|
|
10
10
|
{
|
|
11
11
|
"path": "LICENSE",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
},
|
|
14
14
|
{
|
|
15
15
|
"path": "README.md",
|
|
16
|
-
"sha256": "
|
|
16
|
+
"sha256": "8c96e82b41fb8479befb56797a367b3cc2119cc5b022b18fdd18f129ef8cd0f0"
|
|
17
17
|
},
|
|
18
18
|
{
|
|
19
19
|
"path": "bin/cli.js",
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
},
|
|
26
26
|
{
|
|
27
27
|
"path": "package.json",
|
|
28
|
-
"sha256": "
|
|
28
|
+
"sha256": "0cdd048a5aacd52294f72b5903b5a94c049ad61680dbcac532d5cb3fbd45c736"
|
|
29
29
|
},
|
|
30
30
|
{
|
|
31
31
|
"path": "src/cli.js",
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
},
|
|
38
38
|
{
|
|
39
39
|
"path": "src/commands/init.js",
|
|
40
|
-
"sha256": "
|
|
40
|
+
"sha256": "07ff9d955422979ea67aa5aa454c13303f5ae644efb32e5cc6f359a509638347"
|
|
41
41
|
},
|
|
42
42
|
{
|
|
43
43
|
"path": "src/commands/list.js",
|
|
@@ -157,7 +157,7 @@
|
|
|
157
157
|
},
|
|
158
158
|
{
|
|
159
159
|
"path": "templates/.claude/skills/feature-adr/modules/08-qe.md",
|
|
160
|
-
"sha256": "
|
|
160
|
+
"sha256": "5355e2b36ab13b6acb65abe5b206685915ef2b08191b4a3130574620012b9135"
|
|
161
161
|
},
|
|
162
162
|
{
|
|
163
163
|
"path": "templates/.claude/skills/feature-adr/modules/09-fleet-qe.md",
|
|
@@ -249,7 +249,7 @@
|
|
|
249
249
|
},
|
|
250
250
|
{
|
|
251
251
|
"path": "templates/.claude/skills/feature-adr/scripts/check-plan-completeness.mjs",
|
|
252
|
-
"sha256": "
|
|
252
|
+
"sha256": "eb77822f121ade10bd6265b0f0145ec38507a07d2cb72ba5fa8336c962096bda"
|
|
253
253
|
},
|
|
254
254
|
{
|
|
255
255
|
"path": "templates/.claude/skills/feature-adr/scripts/markdown-masker.mjs",
|
|
@@ -317,7 +317,7 @@
|
|
|
317
317
|
},
|
|
318
318
|
{
|
|
319
319
|
"path": "templates/.claude/workflows/feature-adr.js",
|
|
320
|
-
"sha256": "
|
|
320
|
+
"sha256": "6d1bb93be6fae296281ac8f9fc2f266e5f09336cf6e183912fab5ff3bd0d3fd8"
|
|
321
321
|
},
|
|
322
322
|
{
|
|
323
323
|
"path": "templates/lib/memory-protocol.md",
|
|
@@ -329,5 +329,5 @@
|
|
|
329
329
|
}
|
|
330
330
|
]
|
|
331
331
|
},
|
|
332
|
-
"signature": "
|
|
332
|
+
"signature": "+DKyJg37w6aHqhg/UhCpQ+y+w/e66HEEr9hgOv/TttSIMc6TokGEbvJpxEw2pdu19Q0SHlA/o6pSucM+h3KEDw=="
|
|
333
333
|
}
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [Unreleased]
|
|
4
|
+
|
|
5
|
+
### Fixed — `init --force` больше не стирает локальную правку молча
|
|
6
|
+
|
|
7
|
+
- Из трёх путей записи `init --force` был ЕДИНСТВЕННЫМ, который перезаписывал локально
|
|
8
|
+
изменённый файл без резервной копии и без строки в отчёте — при том, что баннер успеха
|
|
9
|
+
рекомендует именно эту команду. `update` сохраняет правку по трёхсторонней сверке,
|
|
10
|
+
`update --force` кладёт `.bak` с первого дня; расходился только `init --force`.
|
|
11
|
+
- Теперь `init --force` копирует изменённый файл в `<file>.bak` ПЕРЕД перезаписью и называет
|
|
12
|
+
его в отчёте. Копия делается только если байты ОТЛИЧАЮТСЯ от шаблона: `.bak`, совпадающий
|
|
13
|
+
с шаблоном, — чистый мусор. `--dry-run --force` называет будущую копию и ничего не пишет.
|
|
14
|
+
- Поведение самого `--force` не смягчено: файл по-прежнему перезаписывается, это его смысл.
|
|
15
|
+
Менялось только то, что правка перестала исчезать бесследно.
|
|
16
|
+
|
|
3
17
|
## [1.5.1] - 2026-08-21
|
|
4
18
|
|
|
5
19
|
### Changed — the Step-8 amendment gate is a COMMAND, and the durable writers are witnessed
|
package/README.md
CHANGED
|
@@ -63,7 +63,9 @@ npx @dzhechkov/skills-feature-adr init # Install core components
|
|
|
63
63
|
npx @dzhechkov/skills-feature-adr init --with-learning # + reward learning
|
|
64
64
|
npx @dzhechkov/skills-feature-adr init --knowledge-extractor # + knowledge extractor
|
|
65
65
|
npx @dzhechkov/skills-feature-adr init --with-learning --knowledge-extractor # + both
|
|
66
|
-
npx @dzhechkov/skills-feature-adr init --force # Overwrite existing files
|
|
66
|
+
npx @dzhechkov/skills-feature-adr init --force # Overwrite existing files (a locally
|
|
67
|
+
# changed file is copied to <file>.bak first,
|
|
68
|
+
# and the copy is named in the report)
|
|
67
69
|
npx @dzhechkov/skills-feature-adr init --dry-run # Preview without making changes
|
|
68
70
|
npx @dzhechkov/skills-feature-adr update # Update to latest version
|
|
69
71
|
npx @dzhechkov/skills-feature-adr remove # Clean uninstall
|
|
@@ -106,6 +108,13 @@ ARCHITECTURE → IMPLEMENTATION → CODE → QE → FLEET QE
|
|
|
106
108
|
# Full protocols + 6 extra skills, up to 7 fleet QE agents
|
|
107
109
|
```
|
|
108
110
|
|
|
111
|
+
### Checkpoint reads have one source (v1.5.14)
|
|
112
|
+
|
|
113
|
+
`loadCheckpoints` in the bundled `feature-adr.js` now builds its read command with the checkpoints blob's own
|
|
114
|
+
`checkpointReadCmd(FDIR)` instead of a hand-written duplicate, so the helper with the speaking name is the one the pipeline
|
|
115
|
+
runs. Behaviour is unchanged: for directory names with spaces, quotes and missing directories the shell output is
|
|
116
|
+
byte-identical (proved against the old command). A wiring test fails if the duplicate is re-inlined.
|
|
117
|
+
|
|
109
118
|
### Advisory micro-recall at two decision points (v1.5.9, staged)
|
|
110
119
|
|
|
111
120
|
The workflow makes one bounded decision-local recall attempt immediately before the live Step 3
|
|
@@ -208,6 +217,32 @@ run; it now survives it in `recordFailures`.
|
|
|
208
217
|
|
|
209
218
|
Requires `@dzhechkov/harness-core >= 0.6.1`.
|
|
210
219
|
|
|
220
|
+
### The QE instrument writes its own ledger row (aqe-ledger-row)
|
|
221
|
+
|
|
222
|
+
Before this, the run-cost ledger recorded `plan`, `full`, `design-gate` and friends automatically, but
|
|
223
|
+
the pass that actually reviews the code — Step 8 QE — left zero autorows: every `qe`/`impl` row in the
|
|
224
|
+
ledger was hand-entered, with no model, no findings count, no cross-family signal. The `Workflow`
|
|
225
|
+
pipeline now writes two additional autorows, both additive-only (existing rows are byte-identical to
|
|
226
|
+
before — `appendRunCostRow` gained an optional fourth `extra` argument, spread in only after every
|
|
227
|
+
pre-existing field):
|
|
228
|
+
|
|
229
|
+
- **`impl`** — written right after the Step 7.5 landing barrier settles: `coder`, `coderFamily`, and
|
|
230
|
+
`landed` (the barrier's verdict string, or `skipped-claude-sync` for a synchronous Claude coder that
|
|
231
|
+
never runs the barrier). Writing it here — before Step 8 starts — means the `qe` row's
|
|
232
|
+
`minutesSincePrev` measures the QE step alone, not QE-plus-code.
|
|
233
|
+
- **`qe`** — written right after the QE step resolves: `reviewer` (the model, or `null` if unknown),
|
|
234
|
+
`reviewerFamily`, `qeRole` (`qe-code-reviewer` for Claude; `codex-review`/`codex-exec` for Codex by
|
|
235
|
+
scope mode; never guessed), `grade`, `gradeSource` (if known), `findings` + `findingsBySeverity` +
|
|
236
|
+
`findingsSource` (normalized from the QE `gaps[]`; an unknown severity counts as `other`, never
|
|
237
|
+
dropped; no `gaps` array at all reads as `findings:0`, `findingsSource:'no-gaps-array'`), `claimCheck`
|
|
238
|
+
(if run), `crossFamily` (bool — reviewer family differs from coder family), and `qeScope` (if the
|
|
239
|
+
reviewer ran scoped, e.g. Codex's `mode`/`ref`/`files`).
|
|
240
|
+
|
|
241
|
+
Both rows are skipped — with a logged reason, never silently — when their stage resumed from a
|
|
242
|
+
checkpoint (`resumedStages`), so a resumed run never double-pays the ledger. Plain-mode runs (the
|
|
243
|
+
interactive SKILL, not the ultracode `Workflow`) carry the same fields as a manual step at the end of
|
|
244
|
+
`modules/08-qe.md` — see that module for the exact command.
|
|
245
|
+
|
|
211
246
|
### Step 0 writes the assessment down, and the acid check gets its input back (v1.5.0)
|
|
212
247
|
|
|
213
248
|
Step 0 classifies the feature and now **writes `00_complexity_assessment.md` before it returns** — the
|
|
@@ -416,10 +451,56 @@ cross-family QE is silently lost — on exactly the big features that need it mo
|
|
|
416
451
|
- **Every fallback names its cause.** The reason carried into
|
|
417
452
|
`opus (cross-family QE DID NOT happen — …)` comes from a locked taxonomy —
|
|
418
453
|
`timeout` (narrow the scope) · `no-verdict` · `tool-error` (fix the invocation) · `unusable-output` ·
|
|
419
|
-
`unavailable` (fix the account/model) · `over-ceiling
|
|
420
|
-
|
|
454
|
+
`unavailable` (fix the account/model) · `over-ceiling` · `scope-not-established` (mode A's scope
|
|
455
|
+
could not be built — see below) · `base-ref-not-established` (the scope's base-ref probe failed or
|
|
456
|
+
was unparseable — see below). A timeout and an unusable output can never render the same string,
|
|
457
|
+
because the operator's next move differs.
|
|
421
458
|
- **The pipeline still never blocks on Codex.** Both modes fail into the same Claude belt as before.
|
|
422
459
|
|
|
460
|
+
**Mode A's `--uncommitted` pass runs in an ISOLATED scope-repo, never on the shared tree.** MEASURED
|
|
461
|
+
2026-09-17 (run `wf_95211e0f`): on a hub with other dirty packages, `codex review --uncommitted`
|
|
462
|
+
wandered into unrelated files (`books/`, `features/clean-code-*`) and timed out at 600s having
|
|
463
|
+
reviewed nothing of the run's own feature — cross-family QE silently lost on exactly the runs that
|
|
464
|
+
need it most. Before mode A is attempted for scope `'uncommitted'` (the default), the pipeline now:
|
|
465
|
+
builds a throwaway git repo under `features/<slug>/.fa-state/review-scope/` containing ONLY the
|
|
466
|
+
ESTABLISHED change set (the same `modeBChanged` measurement mode B already uses — base versions via
|
|
467
|
+
`git show <BASE_REF>:<path>` in one commit, working versions copied on top); verifies the receipt
|
|
468
|
+
(`git status --porcelain` in that repo names EXACTLY the declared files, never "close enough"); and
|
|
469
|
+
runs `codex review --uncommitted` with `repo:` pointed at that isolated tree. Findings come back with
|
|
470
|
+
the scope-repo's own absolute path and are normalized to repo-relative before scoring. A change set
|
|
471
|
+
that is **not established** (`null` — no pre-code baseline) or **established but empty** (`[]` — no
|
|
472
|
+
files changed) refuses BEFORE any dispatch, under one decline kind `scope-not-established` (never a
|
|
473
|
+
silent fallback to `--uncommitted` on the shared tree); a failed scope-repo build or a receipt
|
|
474
|
+
mismatch refuse the same way. Knob: `args.qeIsolatedScope` (default `true`) — `false` restores the
|
|
475
|
+
prior `--uncommitted`-on-the-shared-tree behavior byte-for-byte, with a log line saying so. Scopes
|
|
476
|
+
`commit`/`base` are unaffected.
|
|
477
|
+
|
|
478
|
+
**Fix round 1 hardening (Codex r1 review, 2026-09-17), briefly:** the scope-repo path is validated by
|
|
479
|
+
PATH SEGMENT (`<repo>/features/<slug>/.fa-state/review-scope`, `<slug>` alphanumeric, no `.`/`..`
|
|
480
|
+
segment anywhere), not by a lexical prefix/substring check — a `..`-laced path can no longer walk the
|
|
481
|
+
one destructive `rm -rf` outside `features/`; the script itself repeats the check at runtime (symlink
|
|
482
|
+
+ `case` guard) as a second belt. A base-ref probe that fails or returns something unparseable now
|
|
483
|
+
REFUSES under `base-ref-not-established` instead of silently substituting `HEAD` — a bad ref used to
|
|
484
|
+
make Codex review a full-file addition instead of the real modification. The receipt is now a CONTENT
|
|
485
|
+
check, not just a pathname list: the scope-build script also emits a `sha256sum` of every working
|
|
486
|
+
file, compared against the same pre-measured hashes mode B already computes, so a `cp` that silently
|
|
487
|
+
degraded to a deletion (an unreadable file, one that vanished mid-copy) is caught even though the
|
|
488
|
+
pathname-only receipt would have passed; a genuine `cp` failure now aborts the build rather than being
|
|
489
|
+
read as an intentional deletion. The porcelain receipt parser reads a fixed two-character status
|
|
490
|
+
column (`--no-renames` on the git status call, so a rename can never arrive as the ambiguous
|
|
491
|
+
`old -> new` shape). A declared path is never trimmed and rejects only what the receipt genuinely
|
|
492
|
+
cannot express (control characters, `"`, `\`, backtick, `$`, non-ASCII, a leading `-`) — spaces and
|
|
493
|
+
shell metacharacters are accepted, because every value already passes through the same safe quoting
|
|
494
|
+
function used everywhere else in this file.
|
|
495
|
+
|
|
496
|
+
**Named limit of the isolated scope (owner-facing, so it reads as a boundary, not a defect):** the
|
|
497
|
+
scope-repo review is BLIND to everything outside the declared change set, by construction — that is
|
|
498
|
+
the whole point (it is why the shared-tree run above timed out reviewing unrelated dirty packages).
|
|
499
|
+
A finding phrased as *"file X does not exist"* or *"the twin file is missing"* from mode A is therefore
|
|
500
|
+
an artifact of that intentional narrowness, not a real defect: the twin/sibling file is simply not
|
|
501
|
+
copied into the scope repo. Context that spans beyond the declared files is covered by mode B (which
|
|
502
|
+
is told exactly which files it may open) and by the separate Claude QE pass, never by mode A alone.
|
|
503
|
+
|
|
423
504
|
After a Codex verdict a cheap Claude agent transcribes it into `08_qe_report.md` (mode A takes no
|
|
424
505
|
prompt, so the reviewer cannot be asked to write anything). It is a scribe, not a second reviewer: the
|
|
425
506
|
grade is Codex's and is stated as final.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dzhechkov/skills-feature-adr",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.14",
|
|
4
4
|
"description": "Adaptive Feature Development skill pack for Claude Code — 11-step pipeline with Complexity Router (S/M/L/XL), ADR-driven architecture, 15 agentic-qe skills, multi-agent fleet QE. Supports --full-qe, --full-qe-extended, --with-learning, and --knowledge-extractor modes.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"skills-feature-adr": "./bin/cli.js"
|
package/sbom.json
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"hashes": [
|
|
16
16
|
{
|
|
17
17
|
"alg": "SHA-256",
|
|
18
|
-
"content": "
|
|
18
|
+
"content": "26fa03f89862d03ebfc6447c866cb5d4dcbd5591ae4f8fb3a3ea08b6c586e19d"
|
|
19
19
|
}
|
|
20
20
|
]
|
|
21
21
|
},
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"hashes": [
|
|
36
36
|
{
|
|
37
37
|
"alg": "SHA-256",
|
|
38
|
-
"content": "
|
|
38
|
+
"content": "8c96e82b41fb8479befb56797a367b3cc2119cc5b022b18fdd18f129ef8cd0f0"
|
|
39
39
|
}
|
|
40
40
|
]
|
|
41
41
|
},
|
|
@@ -69,7 +69,7 @@
|
|
|
69
69
|
},
|
|
70
70
|
{
|
|
71
71
|
"name": "dz:canonical-json-sha256-v2",
|
|
72
|
-
"value": "
|
|
72
|
+
"value": "0cdd048a5aacd52294f72b5903b5a94c049ad61680dbcac532d5cb3fbd45c736"
|
|
73
73
|
}
|
|
74
74
|
]
|
|
75
75
|
},
|
|
@@ -99,7 +99,7 @@
|
|
|
99
99
|
"hashes": [
|
|
100
100
|
{
|
|
101
101
|
"alg": "SHA-256",
|
|
102
|
-
"content": "
|
|
102
|
+
"content": "07ff9d955422979ea67aa5aa454c13303f5ae644efb32e5cc6f359a509638347"
|
|
103
103
|
}
|
|
104
104
|
]
|
|
105
105
|
},
|
|
@@ -399,7 +399,7 @@
|
|
|
399
399
|
"hashes": [
|
|
400
400
|
{
|
|
401
401
|
"alg": "SHA-256",
|
|
402
|
-
"content": "
|
|
402
|
+
"content": "5355e2b36ab13b6acb65abe5b206685915ef2b08191b4a3130574620012b9135"
|
|
403
403
|
}
|
|
404
404
|
]
|
|
405
405
|
},
|
|
@@ -629,7 +629,7 @@
|
|
|
629
629
|
"hashes": [
|
|
630
630
|
{
|
|
631
631
|
"alg": "SHA-256",
|
|
632
|
-
"content": "
|
|
632
|
+
"content": "eb77822f121ade10bd6265b0f0145ec38507a07d2cb72ba5fa8336c962096bda"
|
|
633
633
|
}
|
|
634
634
|
]
|
|
635
635
|
},
|
|
@@ -799,7 +799,7 @@
|
|
|
799
799
|
"hashes": [
|
|
800
800
|
{
|
|
801
801
|
"alg": "SHA-256",
|
|
802
|
-
"content": "
|
|
802
|
+
"content": "6d1bb93be6fae296281ac8f9fc2f266e5f09336cf6e183912fab5ff3bd0d3fd8"
|
|
803
803
|
}
|
|
804
804
|
]
|
|
805
805
|
},
|
package/src/commands/init.js
CHANGED
|
@@ -54,14 +54,15 @@ function showKeysariumIntegration(keysariumManifest) {
|
|
|
54
54
|
|
|
55
55
|
// Copies a component file-by-file with per-file overwrite protection:
|
|
56
56
|
// - destination missing -> write, record in `written`
|
|
57
|
-
// - destination exists + --force ->
|
|
57
|
+
// - destination exists + --force -> BACK UP to a .bak sibling if the bytes differ,
|
|
58
|
+
// then overwrite; record in `written` (+ `backedUp`)
|
|
58
59
|
// - destination exists, no --force -> do NOT write, record in `preserved`
|
|
59
60
|
// In dry-run mode nothing is written, but the same written/preserved
|
|
60
61
|
// classification is produced.
|
|
61
62
|
// Returns { missing, fileCount } where `missing` means the template source
|
|
62
63
|
// was absent on disk and `fileCount` is how many files the template provides.
|
|
63
64
|
function installComponent(key, comp, templatesDir, targetDir, opts) {
|
|
64
|
-
const { force, dryRun, written, preserved, hashes } = opts;
|
|
65
|
+
const { force, dryRun, written, preserved, backedUp, hashes } = opts;
|
|
65
66
|
const src = path.join(templatesDir, comp.src);
|
|
66
67
|
const destRoot = path.join(targetDir, comp.src);
|
|
67
68
|
|
|
@@ -89,12 +90,20 @@ function installComponent(key, comp, templatesDir, targetDir, opts) {
|
|
|
89
90
|
}
|
|
90
91
|
|
|
91
92
|
for (const entry of entries) {
|
|
92
|
-
|
|
93
|
+
const existed = fileExists(entry.destFile);
|
|
94
|
+
if (existed && !force) {
|
|
93
95
|
preserved.push(entry.rel);
|
|
94
96
|
continue;
|
|
95
97
|
}
|
|
98
|
+
// `update --force` has backed edits up to a `.bak` sibling since day one; `init --force` was
|
|
99
|
+
// the ONE path that overwrote silently — and the success banner recommends exactly that
|
|
100
|
+
// command, so a locally-edited workflow could vanish with no notice and no copy.
|
|
101
|
+
// Only DIFFERING bytes are backed up: a `.bak` identical to the template is pure litter.
|
|
102
|
+
const differs = existed && hashFile(entry.destFile) !== hashFile(entry.srcFile);
|
|
103
|
+
if (differs && backedUp) backedUp.push(entry.rel);
|
|
96
104
|
if (!dryRun) {
|
|
97
105
|
ensureDir(path.dirname(entry.destFile));
|
|
106
|
+
if (differs) fs.copyFileSync(entry.destFile, `${entry.destFile}.bak`);
|
|
98
107
|
fs.copyFileSync(entry.srcFile, entry.destFile);
|
|
99
108
|
// Record the SHA-256 of the TEMPLATE bytes we just installed (not the
|
|
100
109
|
// dest) as this file's baseline. This makes baseline == mine immediately
|
|
@@ -110,6 +119,23 @@ function installComponent(key, comp, templatesDir, targetDir, opts) {
|
|
|
110
119
|
return { missing: false, fileCount: entries.length };
|
|
111
120
|
}
|
|
112
121
|
|
|
122
|
+
// Print the block of locally-changed files that were (or would be) backed up before --force
|
|
123
|
+
// overwrote them. Silence here is what the field report caught: the operator had no way to learn
|
|
124
|
+
// that their edit was gone, let alone where the copy is.
|
|
125
|
+
function printBackedUpBlock(backedUp, dryRun) {
|
|
126
|
+
if (backedUp.length === 0) return;
|
|
127
|
+
const MAX_SHOWN = 10; // `printPreservedBlock` держит свою копию — она объявлена в его теле
|
|
128
|
+
console.log('');
|
|
129
|
+
const verb = dryRun ? 'would be backed up' : 'backed up';
|
|
130
|
+
warn(`${backedUp.length} locally-changed file(s) ${verb} to a .bak sibling before being overwritten:`);
|
|
131
|
+
for (const rel of backedUp.slice(0, MAX_SHOWN)) {
|
|
132
|
+
console.log(dim(` ${rel} -> ${rel}.bak`));
|
|
133
|
+
}
|
|
134
|
+
if (backedUp.length > MAX_SHOWN) {
|
|
135
|
+
console.log(dim(` …and ${backedUp.length - MAX_SHOWN} more`));
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
113
139
|
// Print the block of pre-existing files that were (or would be) preserved
|
|
114
140
|
function printPreservedBlock(preserved, dryRun) {
|
|
115
141
|
console.log('');
|
|
@@ -233,6 +259,7 @@ async function run(options) {
|
|
|
233
259
|
const installedFiles = []; // files written (or would-write in dry-run)
|
|
234
260
|
const installedHashes = {}; // rel -> sha256 of the TEMPLATE bytes installed (baseline)
|
|
235
261
|
const preservedFiles = []; // pre-existing files NOT overwritten (no --force)
|
|
262
|
+
const backedUpFiles = []; // locally-changed files copied to .bak before --force overwrote them
|
|
236
263
|
const completedKeys = []; // component keys processed so far (for partial manifest)
|
|
237
264
|
const installedOptionalKeys = [];
|
|
238
265
|
let stepNum = 0;
|
|
@@ -246,7 +273,7 @@ async function run(options) {
|
|
|
246
273
|
|
|
247
274
|
installComponent(key, comp, templatesDir, targetDir, {
|
|
248
275
|
force, dryRun, written: installedFiles, preserved: preservedFiles,
|
|
249
|
-
hashes: installedHashes,
|
|
276
|
+
backedUp: backedUpFiles, hashes: installedHashes,
|
|
250
277
|
});
|
|
251
278
|
completedKeys.push(key);
|
|
252
279
|
}
|
|
@@ -262,7 +289,7 @@ async function run(options) {
|
|
|
262
289
|
|
|
263
290
|
const res = installComponent(key, comp, templatesDir, targetDir, {
|
|
264
291
|
force, dryRun, written: installedFiles, preserved: preservedFiles,
|
|
265
|
-
hashes: installedHashes,
|
|
292
|
+
backedUp: backedUpFiles, hashes: installedHashes,
|
|
266
293
|
});
|
|
267
294
|
if (!res.missing && res.fileCount > 0) {
|
|
268
295
|
installedOptionalKeys.push(key);
|
|
@@ -296,6 +323,10 @@ async function run(options) {
|
|
|
296
323
|
if (dryRun) {
|
|
297
324
|
console.log('');
|
|
298
325
|
info(`Dry run: ${installedFiles.length} file(s) would be written, ${preservedFiles.length} pre-existing file(s) would be preserved.`);
|
|
326
|
+
// Отчёт о копиях стоит СНАРУЖИ стража сохранённых: при --force сохранять нечего, список
|
|
327
|
+
// пуст, и внутри стража блок был бы недостижим ровно в том случае, ради которого написан.
|
|
328
|
+
// Пустой список функция отсекает сама.
|
|
329
|
+
printBackedUpBlock(backedUpFiles, true);
|
|
299
330
|
if (preservedFiles.length > 0) {
|
|
300
331
|
printPreservedBlock(preservedFiles, true);
|
|
301
332
|
}
|
|
@@ -318,10 +349,11 @@ async function run(options) {
|
|
|
318
349
|
process.exit(0);
|
|
319
350
|
}
|
|
320
351
|
|
|
352
|
+
printBackedUpBlock(backedUpFiles, false); // снаружи: при --force preservedFiles пуст
|
|
321
353
|
if (preservedFiles.length > 0) {
|
|
322
354
|
printPreservedBlock(preservedFiles, false);
|
|
323
|
-
console.log('');
|
|
324
355
|
}
|
|
356
|
+
if (preservedFiles.length > 0 || backedUpFiles.length > 0) console.log('');
|
|
325
357
|
|
|
326
358
|
// ── f) Write manifest ──────────────────────────────────────────────────
|
|
327
359
|
const pkgPath = path.resolve(__dirname, '../../package.json');
|
|
@@ -205,7 +205,9 @@ list), grep for unfinished-stub markers: `TODO` / `FIXME` / `HACK` / `XXX` / `PL
|
|
|
205
205
|
file:line, UNLESS the line carries an inline `no-stubs: <reason>` waiver WITH a non-empty reason, or
|
|
206
206
|
`.dz/guard.json` `stubWaivers` lists the path WITH a reason. A REASONLESS waiver is itself a HIGH gap,
|
|
207
207
|
never an exemption. Cross-check mechanically: `dz guard check --op publish --json` runs the same scan
|
|
208
|
-
as the SOFT `no-stubs` rule over the working-tree diff.
|
|
208
|
+
as the SOFT `no-stubs` rule over the working-tree diff.
|
|
209
|
+
After the Step-7 code has landed, run `dz guard check --op code --json` and treat a HARD `block` verdict
|
|
210
|
+
as a HIGH finding naming the drifted file. When you QUOTE a marker in `08_qe_report.md`,
|
|
209
211
|
backtick it so the report itself scans clean (the claim-check forbidden-phrase convention). Record the
|
|
210
212
|
verdict in the ADR Fitness section.
|
|
211
213
|
|
|
@@ -311,6 +313,9 @@ Compile all findings into a structured report:
|
|
|
311
313
|
✅ READY FOR MERGE | ❌ NEEDS FIXES | ⚠️ CONDITIONAL APPROVAL
|
|
312
314
|
```
|
|
313
315
|
|
|
316
|
+
Settle a cross-family re-QE debt with `dz reqe --slug <s> --done --report <08b>`.
|
|
317
|
+
For `dz reqe --done`, exit 3 means the review settled but named BLOCKER/HIGH findings — stop and surface them to the owner.
|
|
318
|
+
|
|
314
319
|
### 7.1 Findings ledger (machine-readable)
|
|
315
320
|
|
|
316
321
|
Prose is for people; `dz score`/`dz recap` need a machine-readable surface too (qe-findings-record,
|
|
@@ -356,6 +361,45 @@ An empty (header-only) table is read as `hollow: true` — worse than no table a
|
|
|
356
361
|
claims a ledger exists and says nothing. If there are no findings, omit the section entirely rather
|
|
357
362
|
than writing an empty table.
|
|
358
363
|
|
|
364
|
+
### 7.2 Ledger row for this QE step (mandatory)
|
|
365
|
+
|
|
366
|
+
Plain mode (this SKILL, not the ultracode `Workflow`) has no in-process `appendRunCostRow` — without
|
|
367
|
+
this step the QE instrument's own pass leaves ZERO trace in the run-cost ledger (aqe-ledger-row,
|
|
368
|
+
owner audit 17.09, decision #3).
|
|
369
|
+
|
|
370
|
+
**Resume guard — check this FIRST, before writing anything.** If QE was RESTORED from a checkpoint
|
|
371
|
+
for THIS run (`features/<slug>/.fa-state/checkpoints.jsonl` already contains a line for stage `qe`
|
|
372
|
+
that matches this run) — the row for this review was already written by the run that actually did
|
|
373
|
+
it. Do **NOT** run the command below in that case; instead write exactly this line into
|
|
374
|
+
`08_qe_report.md`: `qe ledger row skipped — stage resumed`. Writing the row anyway would double-pay
|
|
375
|
+
the same review in the ledger (aqe-ledger-row fix-round-1/#3, Codex r1 HIGH #3 — plain mode had no
|
|
376
|
+
resume guard at all before this).
|
|
377
|
+
|
|
378
|
+
Otherwise — QE ran fresh in this pass — after `08_qe_report.md` is written and the grade is final, run:
|
|
379
|
+
|
|
380
|
+
```bash
|
|
381
|
+
dz feature-adr-record --kind ledger --stage qe --slug <slug> --row '<json>' --json
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
fix-round-1 (Codex r1 HIGH finding 4, ADR-001 D5): no `--auto` here. `--auto` is the trusted marker
|
|
385
|
+
of an AUTOMATED pipeline run and requires an experiment envelope that the manual path does not have;
|
|
386
|
+
this plain-mode row is written without it.
|
|
387
|
+
|
|
388
|
+
`<json>` carries the same fields the ultracode pipeline writes for this row: `reviewer` (the model
|
|
389
|
+
that reviewed, or `null` if unknown — never guessed), `reviewerFamily` (`claude`|`codex`|`null` when
|
|
390
|
+
the reviewer identity is not one of the two known families — never guessed as `claude`), `qeRole`
|
|
391
|
+
(`qe-code-reviewer` for a Claude reviewer; `codex-review`/`codex-exec` for a codex reviewer by scope
|
|
392
|
+
mode; `null` otherwise), `grade`, `gradeSource` (if known), `findings` (gap count), `findingsBySeverity`
|
|
393
|
+
(normalized `sev` -> count, unknown severities counted as `other`, never dropped), `findingsSource`
|
|
394
|
+
(`gaps` or `no-gaps-array`), `claimCheck` (if run), `crossFamily` (bool: reviewer family != coder
|
|
395
|
+
family — `null` when either family is unknown, since a same/cross-family verdict cannot be asserted
|
|
396
|
+
without both), `qeScope` (if the reviewer ran scoped, e.g. codex `mode`/`ref`/`files`). Insert the
|
|
397
|
+
writer's verdict (`{verdict:"written"|...}`) verbatim into `08_qe_report.md` so the record is
|
|
398
|
+
auditable from the artifact itself. NAMED LIMIT (layer 4 of the cost-of-detection ladder, said
|
|
399
|
+
honestly): this step is a skill instruction, not a code gate — nothing forces the agent to run it or
|
|
400
|
+
to check the resume guard first; only its PRESENCE in this module is deterministically pinned (grep
|
|
401
|
+
for the command above and for the resume-guard sentence).
|
|
402
|
+
|
|
359
403
|
### 8. QE Pattern Store (Direct Mode only)
|
|
360
404
|
|
|
361
405
|
When `{AGENTIC_QE_MODE}` = `direct` | `direct-extended`, after the gap loop is closed and the verdict is set:
|
|
@@ -31,12 +31,18 @@
|
|
|
31
31
|
// side) for C1 and C8 together, so a prose mention stops satisfying either check, is a separate
|
|
32
32
|
// backlog item — filed by the lead, not chased here.
|
|
33
33
|
//
|
|
34
|
+
// KNOWN LIMITATION (C9): only byte-identical copies are visible before the edit. Already drifted
|
|
35
|
+
// or intentionally different pinned copies remain the identity tests' job; the frozen
|
|
36
|
+
// features/wave1-instrument-repair/check-plan-completeness.mjs is a real example C9 cannot see.
|
|
37
|
+
//
|
|
34
38
|
// Checks:
|
|
35
39
|
// C1 every ADR file in 03_adr/ has >=1 task line in 06_implementation_plan.md citing it (ADR-00N)
|
|
36
40
|
// C2 every Confirmation-numbered check in each ADR is named in the plan (by its test-file path)
|
|
37
41
|
// C3 the plan carries an EXPECTED_CODE_TARGETS: block, non-empty, and EVERY line parses to a
|
|
38
42
|
// plausible repo-relative path (no spaces unless quoted, no traversal, no markdown residue)
|
|
39
43
|
// — SFDIPOT condition: line-level validation, reject-with-reason, not just block presence
|
|
44
|
+
// C9 every tracked byte-identical twin of a target is also listed or explicitly waived with a
|
|
45
|
+
// reason; size-first narrowing, fixed exclusions, one FAIL per missing (target, twin) pair
|
|
40
46
|
// C4 the plan names the feature's OWN acid corpus (see "acid corpus" below)
|
|
41
47
|
// C5 the plan has an 'Inputs read:' line naming 03_adr, 05_architecture (wave-2 seam, cheap here)
|
|
42
48
|
// C8 every requirement id DECLARED in 01_requirements.md (FR-N, NFR-N, AC-N, C-N, with an optional
|
|
@@ -79,7 +85,9 @@
|
|
|
79
85
|
// supplied explicitly with `--acid=T1,T2,…`. If neither establishes a corpus, C4 is SKIPPED-with-note
|
|
80
86
|
// (a feature that declared no acid cases cannot be failed for not naming them).
|
|
81
87
|
import { maskMarkdown } from './markdown-masker.mjs';
|
|
82
|
-
import { readFileSync, readdirSync, existsSync } from 'node:fs';
|
|
88
|
+
import { readFileSync, readdirSync, existsSync, statSync } from 'node:fs';
|
|
89
|
+
import { execFileSync } from 'node:child_process';
|
|
90
|
+
import { createHash } from 'node:crypto';
|
|
83
91
|
import { isAbsolute, join, resolve } from 'node:path';
|
|
84
92
|
|
|
85
93
|
const argv = process.argv.slice(2);
|
|
@@ -335,6 +343,7 @@ function classifyTargetPath(path) {
|
|
|
335
343
|
|
|
336
344
|
// C3 — EXPECTED_CODE_TARGETS block, line-level validation
|
|
337
345
|
const blockM = plan.match(/EXPECTED_CODE_TARGETS:\s*\n((?:\s*[-*]\s*.+\n?)+)/);
|
|
346
|
+
const listedTargets = new Set();
|
|
338
347
|
if (!blockM) failures.push('C3: no EXPECTED_CODE_TARGETS: block in the plan');
|
|
339
348
|
else {
|
|
340
349
|
const lines = blockM[1].split('\n').map(s => s.trim()).filter(Boolean);
|
|
@@ -343,6 +352,81 @@ else {
|
|
|
343
352
|
const path = ln.replace(/^[-*]\s*/, '').replace(/`/g, '').trim();
|
|
344
353
|
const reasons = classifyTargetPath(path);
|
|
345
354
|
if (reasons.length) failures.push(`C3: target line rejected: "${safe(ln)}" — ${reasons.join(', ')}`);
|
|
355
|
+
else listedTargets.add(path);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// C9 — every byte-identical copy is listed or carries a reasoned waiver (ADR-001).
|
|
360
|
+
const TWIN_EXCLUDED_PREFIXES = ['out/', '.claude/worktrees/', 'node_modules/'];
|
|
361
|
+
{
|
|
362
|
+
const excluded = (path) => TWIN_EXCLUDED_PREFIXES.some((prefix) => path.startsWith(prefix) || path.includes('/' + prefix));
|
|
363
|
+
const waivers = [];
|
|
364
|
+
for (const line of maskMarkdown(plan, { unclosed: 'mask' }).split('\n')) {
|
|
365
|
+
if (!/^\s*(?:[-*]\s*)?TWIN_NOT_A_TARGET:/.test(line)) continue;
|
|
366
|
+
const match = line.match(/^\s*(?:[-*]\s*)?TWIN_NOT_A_TARGET:\s*`?([^\s`]+)`?\s*(?:—|--)\s*(\S.*)$/);
|
|
367
|
+
if (!match) {
|
|
368
|
+
failures.push(`C9: waiver "${safe(line.trim())}" carries no reason — a waiver without a reason is an allowlist entry`);
|
|
369
|
+
} else waivers.push({ path: match[1], used: false });
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
let tracked = null;
|
|
373
|
+
try {
|
|
374
|
+
tracked = execFileSync('git', ['ls-files', '-z'], {
|
|
375
|
+
cwd: process.cwd(), encoding: 'utf-8', maxBuffer: 16 * 1024 * 1024,
|
|
376
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
377
|
+
}).split('\0').filter(Boolean);
|
|
378
|
+
} catch (error) {
|
|
379
|
+
warnings.push(`C9: twin scan had no input — git ls-files failed in ${safe(process.cwd())}: ${safe(error.message)}`);
|
|
380
|
+
}
|
|
381
|
+
if (tracked !== null) {
|
|
382
|
+
// Stat the universe first; only size matches ever reach readFileSync / md5.
|
|
383
|
+
const sizes = new Map();
|
|
384
|
+
for (const path of new Set([...tracked, ...listedTargets])) {
|
|
385
|
+
if (excluded(path)) continue;
|
|
386
|
+
try {
|
|
387
|
+
const stat = statSync(path);
|
|
388
|
+
if (stat.isFile()) sizes.set(path, stat.size);
|
|
389
|
+
} catch (error) {
|
|
390
|
+
// Missing targets are new files; tracked files can also have been deleted locally.
|
|
391
|
+
if (error.code !== 'ENOENT' && error.code !== 'ENOTDIR') warnings.push(`C9: cannot stat ${safe(path)}: ${safe(error.message)}`);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
const targetSizes = new Set([...listedTargets].map((path) => sizes.get(path)).filter((size) => size > 0));
|
|
395
|
+
const hashes = new Map();
|
|
396
|
+
const twinsByHash = new Map();
|
|
397
|
+
for (const [path, size] of sizes) {
|
|
398
|
+
if (!targetSizes.has(size)) continue;
|
|
399
|
+
try { hashes.set(path, createHash('md5').update(readFileSync(path)).digest('hex')); }
|
|
400
|
+
catch (error) { warnings.push(`C9: cannot hash ${safe(path)}: ${safe(error.message)}`); }
|
|
401
|
+
}
|
|
402
|
+
for (const path of tracked) {
|
|
403
|
+
const hash = hashes.get(path);
|
|
404
|
+
if (hash === undefined) continue;
|
|
405
|
+
if (!twinsByHash.has(hash)) twinsByHash.set(hash, []);
|
|
406
|
+
twinsByHash.get(hash).push(path);
|
|
407
|
+
}
|
|
408
|
+
let unlisted = 0;
|
|
409
|
+
for (const target of listedTargets) {
|
|
410
|
+
for (const twin of twinsByHash.get(hashes.get(target)) ?? []) {
|
|
411
|
+
if (twin === target) continue;
|
|
412
|
+
let waived = false;
|
|
413
|
+
for (const waiver of waivers) {
|
|
414
|
+
if (waiver.path.endsWith('/') ? twin.startsWith(waiver.path) : twin === waiver.path) {
|
|
415
|
+
waiver.used = true;
|
|
416
|
+
waived = true;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
if (listedTargets.has(twin) || waived) continue;
|
|
420
|
+
unlisted++;
|
|
421
|
+
// Keep pairs separate: safe() truncates each echoed value at 300 characters.
|
|
422
|
+
failures.push(`C9: target ${safe(target)} has a byte-identical twin the plan does not list: ${safe(twin)} — list it or waive it (TWIN_NOT_A_TARGET: ${safe(twin)} — <why>)`);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
for (const waiver of waivers) if (!waiver.used) warnings.push(`C9: unused waiver ${safe(waiver.path)}`);
|
|
426
|
+
if (unlisted === 0) {
|
|
427
|
+
const onDisk = [...listedTargets].filter((path) => sizes.has(path)).length;
|
|
428
|
+
out(`NOTE C9: twin scan over ${listedTargets.size} target(s), ${onDisk} on disk, 0 unlisted twins (byte-identical only; drifted copies need identity tests)`);
|
|
429
|
+
}
|
|
346
430
|
}
|
|
347
431
|
}
|
|
348
432
|
|
|
@@ -417,7 +501,21 @@ else for (const t of acidTokens) if (!new RegExp(`\\b${t.replace(/[.*+?^${}()|[\
|
|
|
417
501
|
// bullet-less row was invisible here while being a row there (so it evaded this check), and
|
|
418
502
|
// this side lacked the range guard, so «AM-1..AM-4 are covered elsewhere» was falsely
|
|
419
503
|
// reported as a definition. One shape, one meaning.
|
|
420
|
-
|
|
504
|
+
// ЯЧЕЙКА ТАБЛИЦЫ ВНЕ СЕКЦИИ — НЕ ОПРЕДЕЛЕНИЕ. Внутри `## Amendments` строка таблицы
|
|
505
|
+
// законно объявляет поправку, поэтому там `|` остаётся допустимым началом строки. Снаружи
|
|
506
|
+
// же таблица — это СВОДКА, ссылающаяся на поправки, определённые в другом месте, и читать
|
|
507
|
+
// её как определение значит отказывать плану за оглавление.
|
|
508
|
+
// ИЗМЕРЕНО 2026-09-05: четыре ложных FAIL за один день на четырёх разных фичах
|
|
509
|
+
// (narrated-error-must-be-taught, fa-phase-statusline, core-boundary-guard,
|
|
510
|
+
// run-registry-liveness); каждый стоил ручной правки ведущего и перезапуска конвейера,
|
|
511
|
+
// то есть 5–10 минут и один цикл роутера. Один планировщик дошёл до того, что вписал в
|
|
512
|
+
// план оговорку «no line of this paragraph may begin with AM-» — прибор начал
|
|
513
|
+
// диктовать людям форму прозы, а это уже не проверка, а суеверие.
|
|
514
|
+
// ОДИН механизм, а не два: `|` УБРАН из класса начал строки. Первая редакция этой правки
|
|
515
|
+
// добавляла ещё и отдельную проверку `isTableRow`, и мутация, снимавшая только её,
|
|
516
|
+
// оставалась ЭКВИВАЛЕНТНОЙ — 77 тестов из 77 зелёные при «снятой» починке. Дублирующая
|
|
517
|
+
// защита не усиливает гейт, она прячет от мутационной пробы, что именно держит свойство.
|
|
518
|
+
const isDef = /^\s*(?:[-*]\s*)?\*{0,2}AM-(?:CP-)?\d+\*{0,2}\b(?!\s*\.)/.test(pl);
|
|
421
519
|
const inSection = sectionStart >= 0 && cursor2 >= sectionStart && cursor2 < sectionEnd;
|
|
422
520
|
if (isDef && !inSection) {
|
|
423
521
|
const tok = (/AM-(?:CP-)?\d+/.exec(pl) || ['AM-?'])[0];
|
|
@@ -479,7 +577,10 @@ else for (const t of acidTokens) if (!new RegExp(`\\b${t.replace(/[.*+?^${}()|[\
|
|
|
479
577
|
}
|
|
480
578
|
if (superseded) break;
|
|
481
579
|
}
|
|
482
|
-
|
|
580
|
+
// Сообщение называет ТУ ЖЕ форму, которую требует MARK выше. Прежняя редакция обещала
|
|
581
|
+
// простое `→ test <name>`, а проверка требовала имя и файл в обратных кавычках — человек,
|
|
582
|
+
// написавший ровно то, что просило сообщение, получал отказ снова и не понимал, почему.
|
|
583
|
+
if (!hasTest && !superseded) failures.push(`C6: ${defs[k].id} carries neither \`\u2192 test \`<имя>\` in \`<файл>\`\` (обратные кавычки обязательны, как и слово in) nor \`superseded by AM-N\` — an amendment without a confirmation is a wish, and a retracted one must say its successor`);
|
|
483
584
|
}
|
|
484
585
|
}
|
|
485
586
|
}
|