@boyingliu01/xp-gate 0.35.0 → 0.35.1

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 (31) hide show
  1. package/bin/xp-gate.js +8 -0
  2. package/hooks/pre-commit +92 -0
  3. package/lib/__tests__/rework.test.js +127 -0
  4. package/lib/gate-audit.ts +8 -0
  5. package/lib/mattpocock-license-verdict.json +1 -1
  6. package/lib/rework.js +262 -0
  7. package/mock-policy/AGENTS.md +2 -2
  8. package/mutation/AGENTS.md +2 -2
  9. package/package.json +1 -1
  10. package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
  11. package/plugins/claude-code/skills/delphi-review/AGENTS.md +2 -2
  12. package/plugins/claude-code/skills/sprint-flow/AGENTS.md +2 -2
  13. package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +2 -2
  14. package/plugins/dsh/package.json +1 -1
  15. package/plugins/dsh/skills/delphi-review/AGENTS.md +2 -2
  16. package/plugins/dsh/skills/sprint-flow/AGENTS.md +2 -2
  17. package/plugins/dsh/skills/test-specification-alignment/AGENTS.md +2 -2
  18. package/plugins/opencode/package.json +1 -1
  19. package/plugins/opencode/skills/delphi-review/AGENTS.md +2 -2
  20. package/plugins/opencode/skills/sprint-flow/AGENTS.md +2 -2
  21. package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +2 -2
  22. package/plugins/qoder/plugin.json +1 -1
  23. package/plugins/qoder/skills/delphi-review/AGENTS.md +2 -2
  24. package/plugins/qoder/skills/sprint-flow/AGENTS.md +2 -2
  25. package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +2 -2
  26. package/principles/AGENTS.md +2 -2
  27. package/scripts/__tests__/sync-package-content-isolation.test.js +73 -0
  28. package/scripts/__tests__/sync-package-content.test.js +109 -2
  29. package/skills/delphi-review/AGENTS.md +2 -2
  30. package/skills/sprint-flow/AGENTS.md +2 -2
  31. package/skills/test-specification-alignment/AGENTS.md +2 -2
package/bin/xp-gate.js CHANGED
@@ -350,6 +350,14 @@ const COMMANDS = {
350
350
  },
351
351
  usage: 'xp-gate retro [--days N] [--all] [--json] [--sprint <id>]'
352
352
  },
353
+ 'rework': {
354
+ description: 'Report rework: first-pass rate, rounds before landing, top blocking reasons (#651)',
355
+ run: subargs => {
356
+ const { main } = require('../lib/rework.js');
357
+ process.exit(main(subargs));
358
+ },
359
+ usage: 'xp-gate rework [--top N] [--dir <path>] [--json]'
360
+ },
353
361
  'crap': {
354
362
  description: 'Rank functions by CRAP score (complexity x weak coverage) — advisory, never blocks (#404)',
355
363
  run: subargs => {
package/hooks/pre-commit CHANGED
@@ -627,6 +627,98 @@ record_gate_audit() {
627
627
  disown 2>/dev/null || true
628
628
  }
629
629
 
630
+ # ── #651: journal a blocking exit ──────────────────────────────────────────
631
+ #
632
+ # Failure was invisible in this repo's own history: audit.jsonl had 8,258 records
633
+ # and essentially zero gate FAILs, because pre-commit has ~76 `exit 1` blocking
634
+ # paths and record_gate_audit() is only reached AFTER a gate settles. The moment a
635
+ # gate FAILs and exits, the record that would have said so is never written — so
636
+ # `xp-gate retro` and `sprint-status --rework-check` computed rework over a
637
+ # denominator with no failures in it, and rework trended to zero by construction.
638
+ #
639
+ # The trap catches the failure on its way OUT rather than adding a record call to
640
+ # all 76 branches, which would rot the first time someone adds branch 77.
641
+ _BLOCKING_RECORDED=""
642
+ _record_blocking_exit() {
643
+ local rc=$?
644
+ # 0 = success, nothing to journal.
645
+ [ "$rc" -eq 0 ] && return 0
646
+ # One record per run. Without the latch, a gate that already recorded and then
647
+ # exited non-zero would be counted twice, halving the reported first-pass rate.
648
+ [ -n "$_BLOCKING_RECORDED" ] && return 0
649
+ _BLOCKING_RECORDED="1"
650
+
651
+ # Identify the gate that was running. Status vars are NO*-gradeable: any of
652
+ # FAIL/BLOCK means this gate is the reason. Latest failing gate wins, which is
653
+ # the one that actually called exit.
654
+ local _bg_id="" _bg_name="" _bg_status="" _bg_detail=""
655
+ local _i _st_var _st
656
+ for _i in 0 1 2 3 4 5 6 7 8 9 10 11 12 13; do
657
+ _st_var="GATE_${_i}_STATUS"
658
+ _st="${!_st_var:-}"
659
+ case "$_st" in
660
+ FAIL|BLOCK)
661
+ # Latest failing gate wins: it is the one that actually called exit.
662
+ _bg_id="$_i"
663
+ _bg_status="$_st"
664
+ ;;
665
+ esac
666
+ done
667
+ local _bg_detail_var="GATE_${_bg_id}_DETAIL"
668
+ _bg_detail="${!_bg_detail_var:-}"
669
+
670
+ # Gate names for the journal. Deliberately short: this runs during shutdown.
671
+ case "$_bg_id" in
672
+ 0) _bg_name="Version Consistency" ;;
673
+ 1) _bg_name="Code Quality" ;;
674
+ 2) _bg_name="Duplicate Code" ;;
675
+ 3) _bg_name="Cyclomatic Complexity" ;;
676
+ 4) _bg_name="Principles" ;;
677
+ 5) _bg_name="Tests + Coverage" ;;
678
+ 6) _bg_name="Architecture + Boy Scout" ;;
679
+ 7) _bg_name="IaC Security" ;;
680
+ 8) _bg_name="Secret Scanning" ;;
681
+ 9) _bg_name="Build Integrity" ;;
682
+ 10) _bg_name="SAST Security" ;;
683
+ 11) _bg_name="Sprint Flow" ;;
684
+ 12) _bg_name="File Hygiene" ;;
685
+ 13) _bg_name="Compiler Strictness" ;;
686
+ *) _bg_name="unknown" ;;
687
+ esac
688
+ [ -n "$_bg_id" ] || _bg_id="unknown"
689
+
690
+ # The reason must reach the journal or "top-N failing rules" cannot exist.
691
+ # Prefer the gate's own detail (it carries the rule/threshold), else the status.
692
+ local _bg_reason="${_bg_detail:-gate_exit_${rc}}"
693
+
694
+ local _bg_start_var="GATE_${_bg_id}_START"
695
+ local _bg_start="${!_bg_start_var:-}"
696
+ local _bg_dur="null"
697
+ if [ -n "$_bg_start" ]; then
698
+ local _bg_end
699
+ _bg_end=$(now_ms 2>/dev/null || echo "")
700
+ if [ -n "$_bg_end" ]; then
701
+ _bg_dur=$((_bg_end - _bg_start))
702
+ [ "$_bg_dur" -ge 0 ] 2>/dev/null || _bg_dur="null"
703
+ fi
704
+ fi
705
+
706
+ # Fire-and-forget, and never let journaling change the exit code.
707
+ {
708
+ run_tsx "${PROJECT_ROOT_FOR_AUDIT}/src/npm-package/lib/gate-audit.ts" record \
709
+ --gate-id "$_bg_id" \
710
+ --gate-name "$_bg_name" \
711
+ --passed false \
712
+ --issues-found 1 \
713
+ --duration-ms "$_bg_dur" \
714
+ --trigger commit \
715
+ --failure-reason "$_bg_reason" \
716
+ 2>/dev/null
717
+ } >/dev/null 2>&1 || true
718
+ return 0
719
+ }
720
+ trap _record_blocking_exit EXIT
721
+
630
722
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
631
723
  echo " QUALITY GATES - PRE-COMMIT CHECK"
632
724
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
@@ -0,0 +1,127 @@
1
+ /**
2
+ * @test REQ-651 rework
3
+ * @intent #651 asks for a command that turns the gate journal into the three
4
+ * numbers rework management actually needs:
5
+ *
6
+ * 1. first-pass rate -- how often a round clears the gates on try 1
7
+ * 2. rework round stats -- median / mean / worst rounds per landing
8
+ * 3. top-N failing rules -- which rule_id blocks most often
9
+ *
10
+ * Without (3) the only thing you can do is "try harder". With it, the
11
+ * answer to "why is development slow" becomes a ranked list.
12
+ *
13
+ * Two data hazards decide the design:
14
+ *
15
+ * - Before #651 the journal contained NO failures, so every derived rate
16
+ * was 100% by construction. A command that reports "first-pass rate: 100%"
17
+ * from an empty failure set is worse than no command: it manufactures
18
+ * confidence. So an input with zero FAIL records must report that the
19
+ * figure is NOT MEASURABLE, not that it is perfect.
20
+ *
21
+ * - ~401 historical records carry epoch-sized `duration_ms` (flagged
22
+ * `duration_anomaly`). Any duration aggregate must exclude them rather
23
+ * than average them in -- one of them once produced "average 430 million
24
+ * seconds".
25
+ *
26
+ * These tests exercise the pure aggregation function, so the numbers are
27
+ * pinned independently of file layout.
28
+ * @covers AC-670-59, AC-670-60, AC-670-61, AC-670-62
29
+ */
30
+ 'use strict';
31
+
32
+ const { computeReworkStats } = require('../rework');
33
+
34
+ /** One journal record, defaulting to a pass. */
35
+ function rec(overrides) {
36
+ return {
37
+ timestamp: '2026-10-30T10:00:00.000Z',
38
+ gate_id: '1',
39
+ gate_name: 'Code Quality',
40
+ passed: true,
41
+ issues_found: 0,
42
+ duration_ms: 1000,
43
+ trigger: 'commit',
44
+ ...overrides,
45
+ };
46
+ }
47
+
48
+ describe('computeReworkStats (#651)', () => {
49
+ it('AC-670-59: reports an unmeasurable first-pass rate when the journal has no failures', () => {
50
+ // This is exactly the pre-#651 journal: thousands of passes, zero fails.
51
+ const entries = Array.from({ length: 50 }, () => rec());
52
+ const stats = computeReworkStats(entries);
53
+ // A rate derived from an empty failure set is not 100% -- it is unknown.
54
+ expect(stats.firstPassRate).toBeNull();
55
+ expect(stats.measurable).toBe(false);
56
+ expect(stats.reason).toMatch(/no failure/i);
57
+ });
58
+
59
+ it('AC-670-60: counts failing rules and ranks them, so the worst rule is actionable', () => {
60
+ const entries = [
61
+ rec({ passed: false, failure_reason: 'long-function' }),
62
+ rec({ passed: false, failure_reason: 'long-function' }),
63
+ rec({ passed: false, failure_reason: 'long-function' }),
64
+ rec({ passed: false, failure_reason: 'tests_failed' }),
65
+ rec(),
66
+ rec(),
67
+ ];
68
+ const stats = computeReworkStats(entries);
69
+ expect(stats.measurable).toBe(true);
70
+ expect(stats.topFailures[0]).toEqual({ reason: 'long-function', count: 3 });
71
+ expect(stats.topFailures[1]).toEqual({ reason: 'tests_failed', count: 1 });
72
+ // 4 failures out of 6 attempts -> 33% first-pass.
73
+ expect(stats.firstPassRate).toBeCloseTo(33.3, 1);
74
+ });
75
+
76
+ it('AC-670-61: excludes anomaly-flagged durations instead of averaging them in', () => {
77
+ // A real epoch-sized value from the corpus; averaging it in produced
78
+ // "average 430 million seconds" on gate-1.
79
+ const entries = [
80
+ rec({ duration_ms: 1000 }),
81
+ rec({ duration_ms: 3000 }),
82
+ rec({ duration_ms: 1784536915891, duration_anomaly: true }),
83
+ ];
84
+ const stats = computeReworkStats(entries);
85
+ expect(stats.duration).not.toBeNull();
86
+ // Median over the two sane values (1000, 3000) is 2000, not ~5.9e11.
87
+ expect(stats.duration.medianMs).toBe(2000);
88
+ expect(stats.duration.samples).toBe(2);
89
+ expect(stats.duration.excludedAnomalies).toBe(1);
90
+ });
91
+
92
+ it('AC-670-62: a failure with no reason is bucketed, never silently dropped', () => {
93
+ const entries = [
94
+ rec({ passed: false }), // no failure_reason
95
+ rec({ passed: false, failure_reason: '' }), // empty
96
+ rec({ passed: false, failure_reason: 'short-circuit' }),
97
+ ];
98
+ const stats = computeReworkStats(entries);
99
+ // Dropping unlabelled failures would understate the failure count and push
100
+ // the first-pass rate UP -- the optimistic direction is the dangerous one.
101
+ expect(stats.failures).toBe(3);
102
+ const unattributed = stats.topFailures.find((f) => /unattributed|unknown/i.test(f.reason));
103
+ expect(unattributed, 'unlabelled failures must appear in the ranking').toBeDefined();
104
+ expect(unattributed.count).toBe(2);
105
+ });
106
+ });
107
+
108
+ describe('rework duration sampling (#651)', () => {
109
+ it('AC-670-61: an UNFLAGGED epoch-sized duration is still excluded', () => {
110
+ // Measured on the real journal: 391 records carry duration_anomaly, but 6
111
+ // more epoch-sized values do not. Trusting the flag alone left the mean at
112
+ // 1.3e9 ms -- the "average 430 million seconds" artifact returning through
113
+ // the back door. Plausibility must be checked independently of the flag.
114
+ const entries = [
115
+ rec({ duration_ms: 2000 }),
116
+ rec({ duration_ms: 4000 }),
117
+ rec({ duration_ms: 1785001784992 }), // no duration_anomaly flag
118
+ ];
119
+ const stats = computeReworkStats(entries);
120
+ expect(stats.duration).not.toBeNull();
121
+ expect(stats.duration.medianMs).toBe(3000);
122
+ expect(stats.duration.samples).toBe(2);
123
+ // Excluded count covers BOTH flagged and implausible, or the sample size
124
+ // looks closer to the record count than it really is.
125
+ expect(stats.duration.excludedAnomalies).toBe(1);
126
+ });
127
+ });
package/lib/gate-audit.ts CHANGED
@@ -41,6 +41,12 @@ export interface GateAuditEntry {
41
41
  duration_anomaly?: boolean; // Issue #370: set when duration_ms > max_duration_ms
42
42
  duration_reason?: 'poisoned_start_ms' | 'clock_skew'; // REQ-B: why duration_ms is null
43
43
  detail?: string; // #507: optional gate-specific payload (e.g. effective CCN threshold)
44
+ /**
45
+ * #651: WHY the gate blocked. Without this the journal can only answer "how
46
+ * many failed", never "which rule fails most" — and the second question is the
47
+ * one that drives fixes. Only meaningful when passed=false.
48
+ */
49
+ failure_reason?: string;
44
50
  }
45
51
 
46
52
  // ── Public API ───────────────────────────────────────────────────────────────
@@ -415,6 +421,8 @@ function runCli(): void {
415
421
  ...(durationReason ? { duration_reason: durationReason as 'poisoned_start_ms' | 'clock_skew' } : {}),
416
422
  // #507: only set when provided (omit field otherwise)
417
423
  ...(opts['detail'] ? { detail: opts['detail'] } : {}),
424
+ // #651: only set when provided, so successful records stay byte-identical.
425
+ ...(opts['failure-reason'] ? { failure_reason: String(opts['failure-reason']).slice(0, 300) } : {}),
418
426
  };
419
427
  // Pre-commit (trigger=commit) is staging for a commit that does not exist
420
428
  // yet: park it in the pending sidecar so post-commit can stamp the real
@@ -2,6 +2,6 @@
2
2
  "verdict": "PASS",
3
3
  "license_type": "MIT",
4
4
  "sha256": "0e7ac423bf2c6e223b7c5b156f8cf72da49d748e56a1641402c31f22ad07dbb5",
5
- "checked_at": "2026-10-30T15:31:30.899Z",
5
+ "checked_at": "2026-10-30T15:51:17.008Z",
6
6
  "source": "network"
7
7
  }
package/lib/rework.js ADDED
@@ -0,0 +1,262 @@
1
+ /**
2
+ * `xp-gate rework` — what the gate journal says about rework (#651).
3
+ *
4
+ * `xp-gate retro` already answers "how much did we ship". This answers the
5
+ * narrower and more actionable question: how often does a round of work clear
6
+ * the gates on the first attempt, and which rule stops it most.
7
+ *
8
+ * The reason this needs a dedicated command rather than a `retro` line: before
9
+ * #651 the journal contained NO failure records at all, so a naive rework rate
10
+ * computed from it returned 100% every time. A metric that reads "perfect" when
11
+ * it is actually unmeasured is worse than no metric -- it manufactures
12
+ * confidence. So `measurable: false` is a first-class outcome here.
13
+ *
14
+ * No runtime dependencies: this ships inside the zero-install package.
15
+ */
16
+ 'use strict';
17
+
18
+ const fs = require('node:fs');
19
+ const path = require('node:path');
20
+
21
+ const AUDIT_DIR = '.xp-gate';
22
+ const AUDIT_FILE = 'audit.jsonl';
23
+ const PENDING_FILE = 'audit.pending.jsonl';
24
+
25
+ /** Bucket name for failures the journal cannot attribute to a rule. */
26
+ const UNATTRIBUTED = 'unattributed';
27
+
28
+ /**
29
+ * Read every journal record under `repoRoot`, oldest first.
30
+ *
31
+ * Malformed lines are skipped rather than thrown on: the journal is append-only
32
+ * and a single truncated write must not take the whole report down.
33
+ *
34
+ * @param {string} [repoRoot]
35
+ * @returns {Array<object>}
36
+ */
37
+ function readJournal(repoRoot = process.cwd()) {
38
+ const dir = path.join(repoRoot, AUDIT_DIR);
39
+ const entries = [];
40
+ for (const file of [AUDIT_FILE, PENDING_FILE]) {
41
+ const full = path.join(dir, file);
42
+ let raw;
43
+ try {
44
+ raw = fs.readFileSync(full, 'utf8');
45
+ } catch {
46
+ continue;
47
+ }
48
+ for (const line of raw.split('\n')) {
49
+ if (!line.trim()) continue;
50
+ try {
51
+ const parsed = JSON.parse(line);
52
+ if (parsed && typeof parsed === 'object') entries.push(parsed);
53
+ } catch {
54
+ /* truncated/garbled line -- skip, do not fail the report */
55
+ }
56
+ }
57
+ }
58
+ return entries;
59
+ }
60
+
61
+ /** Median of a numeric array (0 for an empty array). */
62
+ function median(values) {
63
+ if (values.length === 0) return 0;
64
+ const sorted = [...values].sort((a, b) => a - b);
65
+ const mid = Math.floor(sorted.length / 2);
66
+ return sorted.length % 2 === 0 ? (sorted[mid - 1] + sorted[mid]) / 2 : sorted[mid];
67
+ }
68
+
69
+ /** Mean of a numeric array (0 for an empty array). */
70
+ function mean(values) {
71
+ if (values.length === 0) return 0;
72
+ return values.reduce((a, b) => a + b, 0) / values.length;
73
+ }
74
+
75
+ /**
76
+ * Group a flat record list into rework rounds.
77
+ *
78
+ * The journal is a flat stream of gate executions with no round identifier, so a
79
+ * round is reconstructed the only way the data allows: a run of FAIL records
80
+ * terminated by the next PASS. That is exactly "how many attempts before this
81
+ * landed", which is the definition rework needs. A trailing run of failures with
82
+ * no resolving pass is a round that has not landed yet, and is counted as
83
+ * `unresolved` rather than being dropped.
84
+ *
85
+ * @param {Array<object>} entries
86
+ */
87
+ function groupRounds(entries) {
88
+ const rounds = [];
89
+ let current = 0;
90
+ for (const entry of entries) {
91
+ if (entry.passed) {
92
+ if (current > 0) rounds.push(current);
93
+ current = 0;
94
+ continue;
95
+ }
96
+ current += 1;
97
+ }
98
+ return { resolved: rounds, unresolved: current };
99
+ }
100
+
101
+ /**
102
+ * Aggregate the journal into rework metrics.
103
+ *
104
+ * @param {Array<object>} entries journal records
105
+ * @returns {object} see AC-670-59..62
106
+ */
107
+ function computeReworkStats(entries) {
108
+ const list = Array.isArray(entries) ? entries : [];
109
+ const totals = list.length;
110
+ const failures = list.filter((e) => e.passed === false).length;
111
+ const passes = totals - failures;
112
+
113
+ // ── Failures by reason ────────────────────────────────────────────────────
114
+ // Unlabelled failures are bucketed, never dropped: dropping them would lower
115
+ // the failure count and push the first-pass rate UP, which is the optimistic
116
+ // direction and therefore the dangerous one.
117
+ const byReason = new Map();
118
+ for (const entry of list) {
119
+ if (entry.passed !== false) continue;
120
+ const raw = typeof entry.failure_reason === 'string' ? entry.failure_reason.trim() : '';
121
+ const key = raw === '' ? UNATTRIBUTED : raw;
122
+ byReason.set(key, (byReason.get(key) || 0) + 1);
123
+ }
124
+ const topFailures = [...byReason.entries()]
125
+ .map(([reason, count]) => ({ reason, count }))
126
+ .sort((a, b) => b.count - a.count || a.reason.localeCompare(b.reason));
127
+
128
+ // ── Durations ─────────────────────────────────────────────────────────────
129
+ // Two independent guards, because the flag alone is not enough:
130
+ //
131
+ // 1. honour `duration_anomaly` -- the writer's own verdict;
132
+ // 2. reject implausible magnitudes. Measured on the real journal: 391 records
133
+ // carry the flag, but 6 MORE epoch-sized values (up to 1.78e12) do not,
134
+ // so trusting the flag alone left the mean at 1.3e9 ms -- the very
135
+ // "average 430 million seconds" artifact this is supposed to kill.
136
+ //
137
+ // The ceiling is deliberately generous (1 hour): a real gate run that long is
138
+ // pathological but conceivable, while an epoch stamp is ~1.78e12 and cannot be
139
+ // mistaken for anything else.
140
+ const MAX_PLAUSIBLE_DURATION_MS = 60 * 60 * 1000;
141
+ const isPlausible = (e) => typeof e.duration_ms === 'number'
142
+ && Number.isFinite(e.duration_ms)
143
+ && e.duration_ms >= 0
144
+ && e.duration_ms <= MAX_PLAUSIBLE_DURATION_MS
145
+ && e.duration_anomaly !== true;
146
+ const sane = list.filter(isPlausible);
147
+ const duration = sane.length > 0
148
+ ? {
149
+ medianMs: median(sane.map((e) => e.duration_ms)),
150
+ meanMs: Math.round(mean(sane.map((e) => e.duration_ms))),
151
+ samples: sane.length,
152
+ // Every record that did NOT contribute -- flagged or implausible. Reporting
153
+ // only the flagged count would understate the exclusion and make the
154
+ // sample size look closer to the record count than it is.
155
+ excludedAnomalies: list.filter((e) => typeof e.duration_ms === 'number' && !isPlausible(e)).length,
156
+ }
157
+ : null;
158
+
159
+ // ── Rounds ────────────────────────────────────────────────────────────────
160
+ const rounds = groupRounds(list);
161
+ const reworkRounds = rounds.resolved;
162
+
163
+ // ── First-pass rate ───────────────────────────────────────────────────────
164
+ // Undefined without failures. A rate over an empty failure set is not 100%.
165
+ const measurable = failures > 0;
166
+ const firstPassRate = measurable ? Math.round((passes / totals) * 1000) / 10 : null;
167
+
168
+ return {
169
+ measurable,
170
+ reason: measurable ? null : 'no failure records in the journal -- first-pass rate is not measurable (pre-#651 journals only recorded passes)',
171
+ totalRecords: totals,
172
+ passes,
173
+ failures,
174
+ firstPassRate,
175
+ rounds: {
176
+ resolved: reworkRounds.length,
177
+ unresolved: rounds.unresolved > 0,
178
+ medianRework: median(reworkRounds),
179
+ meanRework: Math.round(mean(reworkRounds) * 100) / 100,
180
+ worstRework: reworkRounds.length > 0 ? Math.max(...reworkRounds) : 0,
181
+ },
182
+ topFailures,
183
+ duration,
184
+ };
185
+ }
186
+
187
+ /** Render a stats object for the terminal. */
188
+ function formatReworkReport(stats, { limit = 10 } = {}) {
189
+ const lines = [];
190
+ lines.push('');
191
+ lines.push('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
192
+ lines.push(' REWORK REPORT');
193
+ lines.push('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
194
+ lines.push('');
195
+ lines.push(` Journal records: ${stats.totalRecords} (${stats.passes} pass, ${stats.failures} fail)`);
196
+
197
+ if (!stats.measurable) {
198
+ // Say WHY, and say it loudly: a silent 100% would be a lie.
199
+ lines.push('');
200
+ lines.push(' ⚠️ First-pass rate: NOT MEASURABLE');
201
+ lines.push(` ${stats.reason}`);
202
+ } else {
203
+ lines.push(` First-pass rate: ${stats.firstPassRate}%`);
204
+ }
205
+
206
+ if (stats.rounds.resolved > 0) {
207
+ lines.push('');
208
+ lines.push(` Rework rounds (failures before a landing): ${stats.rounds.resolved}`);
209
+ lines.push(` median ${stats.rounds.medianRework} / mean ${stats.rounds.meanRework} / worst ${stats.rounds.worstRework}`);
210
+ if (stats.rounds.unresolved) {
211
+ lines.push(' (a trailing run of failures has not landed yet)');
212
+ }
213
+ }
214
+
215
+ if (stats.topFailures.length > 0) {
216
+ lines.push('');
217
+ lines.push(` Top ${limit} blocking reasons:`);
218
+ for (const { reason, count } of stats.topFailures.slice(0, limit)) {
219
+ lines.push(` ${String(count).padStart(5)} ${reason}`);
220
+ }
221
+ }
222
+
223
+ if (stats.duration) {
224
+ lines.push('');
225
+ lines.push(` Gate duration: median ${stats.duration.medianMs} ms / mean ${stats.duration.meanMs} ms (${stats.duration.samples} samples`);
226
+ lines.push(` ${stats.duration.excludedAnomalies} anomaly record(s) excluded)`);
227
+ }
228
+
229
+ lines.push('');
230
+ return lines.join('\n');
231
+ }
232
+
233
+ /**
234
+ * CLI entry.
235
+ * @param {string[]} argv args after `rework`
236
+ */
237
+ function main(argv = []) {
238
+ const repoRootIdx = argv.indexOf('--dir');
239
+ const repoRoot = repoRootIdx !== -1 && argv[repoRootIdx + 1] ? argv[repoRootIdx + 1] : process.cwd();
240
+ const limitIdx = argv.indexOf('--top');
241
+ const limit = limitIdx !== -1 && argv[limitIdx + 1] ? parseInt(argv[limitIdx + 1], 10) : 10;
242
+
243
+ const stats = computeReworkStats(readJournal(repoRoot));
244
+ if (argv.includes('--json')) {
245
+ process.stdout.write(`${JSON.stringify(stats, null, 2)}\n`);
246
+ return 0;
247
+ }
248
+ process.stdout.write(`${formatReworkReport(stats, { limit })}\n`);
249
+ return 0;
250
+ }
251
+
252
+ module.exports = {
253
+ readJournal,
254
+ computeReworkStats,
255
+ formatReworkReport,
256
+ main,
257
+ UNATTRIBUTED,
258
+ };
259
+
260
+ if (require.main === module) {
261
+ process.exitCode = main(process.argv.slice(2));
262
+ }
@@ -1,9 +1,9 @@
1
1
  # SRC/MOCK-POLICY KNOWLEDGE BASE
2
2
 
3
3
  **Generated:** 2026-10-30
4
- **Commit:** c8a73b2a
4
+ **Commit:** 2ce96f13
5
5
  **Branch:** main
6
- **Version:** 0.35.0.0
6
+ **Version:** 0.35.1.0
7
7
 
8
8
  ## OVERVIEW
9
9
  Mock layering policy enforcement — Gate M3 of pre-push hook. Ensures integration tests use real implementations for internal dependencies, mock external dependencies, and annotate pending mocks with removal plans. Combines project scope scanning, mock decision engine, and per-file validation into a single pipeline.
@@ -1,9 +1,9 @@
1
1
  # SRC/MUTATION KNOWLEDGE BASE
2
2
 
3
3
  **Generated:** 2026-10-30
4
- **Commit:** c8a73b2a
4
+ **Commit:** 2ce96f13
5
5
  **Branch:** main
6
- **Version:** 0.35.0.0
6
+ **Version:** 0.35.1.0
7
7
 
8
8
  ## OVERVIEW
9
9
  **Gate M** (incremental mutation testing) + **Gate M2** helpers (test-layer detection used by `src/mock-policy/`). Pre-push quality gate. TypeScript-only; uses Stryker.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@boyingliu01/xp-gate",
3
- "version": "0.35.0",
3
+ "version": "0.35.1",
4
4
  "description": "AI-driven development workflow: 6 quality gates + Delphi review + Sprint Flow",
5
5
  "bin": {
6
6
  "xp-gate": "bin/xp-gate.js"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xp-gate",
3
- "version": "0.35.0",
3
+ "version": "0.35.1",
4
4
  "displayName": "XP-Gate",
5
5
  "description": "Extreme Programming quality gates + AI workflow skills for Claude Code. Includes 12 pre-commit gates (Gate 0-11), 8 pre-push gates, Sprint Flow (6 phases), and Delphi multi-expert review (>=90% consensus).",
6
6
  "author": {
@@ -1,9 +1,9 @@
1
1
  # SKILLS/DELPHI-REVIEW KNOWLEDGE BASE
2
2
 
3
3
  **Generated:** 2026-10-30
4
- **Commit:** c8a73b2a
4
+ **Commit:** 2ce96f13
5
5
  **Branch:** main
6
- **Version:** 0.35.0.0
6
+ **Version:** 0.35.1.0
7
7
 
8
8
  ## OVERVIEW
9
9
  Delphi Consensus Review — multi-round anonymous expert review (≥90% threshold, exactly 3 experts with distinct executable model IDs). Supports design, requirements, and code-walkthrough modes.
@@ -1,9 +1,9 @@
1
1
  # SKILLS/SPRINT-FLOW KNOWLEDGE BASE
2
2
 
3
3
  **Generated:** 2026-10-30
4
- **Commit:** c8a73b2a
4
+ **Commit:** 2ce96f13
5
5
  **Branch:** main
6
- **Version:** 0.35.0.0
6
+ **Version:** 0.35.1.0
7
7
 
8
8
  ## OVERVIEW
9
9
  **6-phase** development pipeline (v2.0 compact redesign, Issue #290): PREP → DESIGN → BUILD → VERIFY → SHIP → CLOSE. Phase 3/6 BUILD default build mode is **ralph-loop** (REQ-level iteration, 40-67% token savings vs parallel). HARD-GATE between DESIGN (2/6) and BUILD (3/6): design must pass Delphi review (≥90% consensus) before any coding.
@@ -1,9 +1,9 @@
1
1
  # SKILLS/TEST-SPECIFICATION-ALIGNMENT KNOWLEDGE BASE
2
2
 
3
3
  **Generated:** 2026-10-30
4
- **Commit:** c8a73b2a
4
+ **Commit:** 2ce96f13
5
5
  **Branch:** main
6
- **Version:** 0.35.0.0
6
+ **Version:** 0.35.1.0
7
7
 
8
8
  ## OVERVIEW
9
9
  Test-Specification Alignment Engine — two-stage validation ensuring tests accurately reflect requirements and design specs.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@boyingliu01/dsh-plugin-xp-gate",
3
- "version": "0.35.0",
3
+ "version": "0.35.1",
4
4
  "description": "XP-Gate deterministic quality gates + AI workflow skills as a DeepSeek Harness (DSH) native plugin: gate-check / gate-principles / gate-arch tools + 12 SKILL.md skills",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
@@ -1,9 +1,9 @@
1
1
  # SKILLS/DELPHI-REVIEW KNOWLEDGE BASE
2
2
 
3
3
  **Generated:** 2026-10-30
4
- **Commit:** c8a73b2a
4
+ **Commit:** 2ce96f13
5
5
  **Branch:** main
6
- **Version:** 0.35.0.0
6
+ **Version:** 0.35.1.0
7
7
 
8
8
  ## OVERVIEW
9
9
  Delphi Consensus Review — multi-round anonymous expert review (≥90% threshold, exactly 3 experts with distinct executable model IDs). Supports design, requirements, and code-walkthrough modes.
@@ -1,9 +1,9 @@
1
1
  # SKILLS/SPRINT-FLOW KNOWLEDGE BASE
2
2
 
3
3
  **Generated:** 2026-10-30
4
- **Commit:** c8a73b2a
4
+ **Commit:** 2ce96f13
5
5
  **Branch:** main
6
- **Version:** 0.35.0.0
6
+ **Version:** 0.35.1.0
7
7
 
8
8
  ## OVERVIEW
9
9
  **6-phase** development pipeline (v2.0 compact redesign, Issue #290): PREP → DESIGN → BUILD → VERIFY → SHIP → CLOSE. Phase 3/6 BUILD default build mode is **ralph-loop** (REQ-level iteration, 40-67% token savings vs parallel). HARD-GATE between DESIGN (2/6) and BUILD (3/6): design must pass Delphi review (≥90% consensus) before any coding.
@@ -1,9 +1,9 @@
1
1
  # SKILLS/TEST-SPECIFICATION-ALIGNMENT KNOWLEDGE BASE
2
2
 
3
3
  **Generated:** 2026-10-30
4
- **Commit:** c8a73b2a
4
+ **Commit:** 2ce96f13
5
5
  **Branch:** main
6
- **Version:** 0.35.0.0
6
+ **Version:** 0.35.1.0
7
7
 
8
8
  ## OVERVIEW
9
9
  Test-Specification Alignment Engine — two-stage validation ensuring tests accurately reflect requirements and design specs.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@boyingliu01/opencode-plugin",
3
- "version": "0.35.0",
3
+ "version": "0.35.1",
4
4
  "type": "module",
5
5
  "main": "index.ts",
6
6
  "description": "XP-Gate quality gates + AI workflow skills + Sprint Flow TUI sidebar for OpenCode",
@@ -1,9 +1,9 @@
1
1
  # SKILLS/DELPHI-REVIEW KNOWLEDGE BASE
2
2
 
3
3
  **Generated:** 2026-10-30
4
- **Commit:** c8a73b2a
4
+ **Commit:** 2ce96f13
5
5
  **Branch:** main
6
- **Version:** 0.35.0.0
6
+ **Version:** 0.35.1.0
7
7
 
8
8
  ## OVERVIEW
9
9
  Delphi Consensus Review — multi-round anonymous expert review (≥90% threshold, exactly 3 experts with distinct executable model IDs). Supports design, requirements, and code-walkthrough modes.
@@ -1,9 +1,9 @@
1
1
  # SKILLS/SPRINT-FLOW KNOWLEDGE BASE
2
2
 
3
3
  **Generated:** 2026-10-30
4
- **Commit:** c8a73b2a
4
+ **Commit:** 2ce96f13
5
5
  **Branch:** main
6
- **Version:** 0.35.0.0
6
+ **Version:** 0.35.1.0
7
7
 
8
8
  ## OVERVIEW
9
9
  **6-phase** development pipeline (v2.0 compact redesign, Issue #290): PREP → DESIGN → BUILD → VERIFY → SHIP → CLOSE. Phase 3/6 BUILD default build mode is **ralph-loop** (REQ-level iteration, 40-67% token savings vs parallel). HARD-GATE between DESIGN (2/6) and BUILD (3/6): design must pass Delphi review (≥90% consensus) before any coding.
@@ -1,9 +1,9 @@
1
1
  # SKILLS/TEST-SPECIFICATION-ALIGNMENT KNOWLEDGE BASE
2
2
 
3
3
  **Generated:** 2026-10-30
4
- **Commit:** c8a73b2a
4
+ **Commit:** 2ce96f13
5
5
  **Branch:** main
6
- **Version:** 0.35.0.0
6
+ **Version:** 0.35.1.0
7
7
 
8
8
  ## OVERVIEW
9
9
  Test-Specification Alignment Engine — two-stage validation ensuring tests accurately reflect requirements and design specs.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xp-gate",
3
- "version": "0.35.0",
3
+ "version": "0.35.1",
4
4
  "displayName": "XP-Gate",
5
5
  "description": "Extreme Programming quality gates + AI workflow skills for Qoder. Includes 12 pre-commit gates (Gate 0-11), 8 pre-push gates, Sprint Flow (6 phases), and Delphi multi-expert review (>=90% consensus).",
6
6
  "author": {
@@ -1,9 +1,9 @@
1
1
  # SKILLS/DELPHI-REVIEW KNOWLEDGE BASE
2
2
 
3
3
  **Generated:** 2026-10-30
4
- **Commit:** c8a73b2a
4
+ **Commit:** 2ce96f13
5
5
  **Branch:** main
6
- **Version:** 0.35.0.0
6
+ **Version:** 0.35.1.0
7
7
 
8
8
  ## OVERVIEW
9
9
  Delphi Consensus Review — multi-round anonymous expert review (≥90% threshold, exactly 3 experts with distinct executable model IDs). Supports design, requirements, and code-walkthrough modes.
@@ -1,9 +1,9 @@
1
1
  # SKILLS/SPRINT-FLOW KNOWLEDGE BASE
2
2
 
3
3
  **Generated:** 2026-10-30
4
- **Commit:** c8a73b2a
4
+ **Commit:** 2ce96f13
5
5
  **Branch:** main
6
- **Version:** 0.35.0.0
6
+ **Version:** 0.35.1.0
7
7
 
8
8
  ## OVERVIEW
9
9
  **6-phase** development pipeline (v2.0 compact redesign, Issue #290): PREP → DESIGN → BUILD → VERIFY → SHIP → CLOSE. Phase 3/6 BUILD default build mode is **ralph-loop** (REQ-level iteration, 40-67% token savings vs parallel). HARD-GATE between DESIGN (2/6) and BUILD (3/6): design must pass Delphi review (≥90% consensus) before any coding.
@@ -1,9 +1,9 @@
1
1
  # SKILLS/TEST-SPECIFICATION-ALIGNMENT KNOWLEDGE BASE
2
2
 
3
3
  **Generated:** 2026-10-30
4
- **Commit:** c8a73b2a
4
+ **Commit:** 2ce96f13
5
5
  **Branch:** main
6
- **Version:** 0.35.0.0
6
+ **Version:** 0.35.1.0
7
7
 
8
8
  ## OVERVIEW
9
9
  Test-Specification Alignment Engine — two-stage validation ensuring tests accurately reflect requirements and design specs.
@@ -1,9 +1,9 @@
1
1
  # PRINCIPLES CHECKER MODULE
2
2
 
3
3
  **Generated:** 2026-10-30
4
- **Commit:** c8a73b2a
4
+ **Commit:** 2ce96f13
5
5
  **Branch:** main
6
- **Version:** 0.35.0.0
6
+ **Version:** 0.35.1.0
7
7
 
8
8
  ## OVERVIEW
9
9
  Clean Code & SOLID principles checker — **Gate 4** of pre-commit. 14 rules × 9 language adapters, SARIF 2.1.0 output. Houses the **Boy Scout Rule** enforcement engine (Gate 6) and warning-baseline storage.
@@ -0,0 +1,73 @@
1
+ /**
2
+ * @test REQ-645
3
+ * @intent `sync-package-content.test.js` used the REAL repository root as the
4
+ * spawn cwd:
5
+ *
6
+ * const WORKTREE_PATH = path.resolve(__dirname, '..', '..', '..', '..');
7
+ *
8
+ * Four levels up from `src/npm-package/scripts/__tests__/` is the repo
9
+ * root, not a temp dir. The tests then ran the real sync script with
10
+ * `cwd: WORKTREE_PATH`, so the script's writes landed in the repository
11
+ * instead of a sandbox. CI's Gate 5 failed at `vitest-worktree-guard`:
12
+ *
13
+ * Work tree polluted by the test run — 1 path(s) the run introduced:
14
+ * M src/npm-package/mock-policy/gate-m3.ts
15
+ *
16
+ * The reason this is easy to miss: the pollution is CONDITIONAL. The
17
+ * script's writes are idempotent, so when the mirror already matches the
18
+ * source nothing changes and the run looks clean. It only dirties the tree
19
+ * when the checked-out commit happens to carry a stale mirror — which is
20
+ * exactly when CI is checking out a commit that the fix was supposed to
21
+ * validate. So it presents as "flaky, just re-run it", and re-running
22
+ * hides the cause while leaving the latent defect in place.
23
+ *
24
+ * The guard catching it is good, but it catches the CONSEQUENCE. This file
25
+ * pins the CAUSE: a test may not write into the repository.
26
+ * @covers AC-670-54, AC-670-55
27
+ */
28
+ 'use strict';
29
+
30
+ const fs = require('node:fs');
31
+ const path = require('node:path');
32
+
33
+ const REPO_ROOT = path.resolve(__dirname, '..', '..', '..', '..');
34
+ const TEST_FILE = path.join(
35
+ REPO_ROOT, 'src', 'npm-package', 'scripts', '__tests__', 'sync-package-content.test.js',
36
+ );
37
+
38
+ function readSuite() {
39
+ return fs.readFileSync(TEST_FILE, 'utf8');
40
+ }
41
+
42
+ describe('sync-package-content tests must not write into the repository (#645)', () => {
43
+ it('AC-670-54: no test uses the repo root as a spawn cwd', () => {
44
+ const source = readSuite();
45
+ // The exact offending expression. `path.resolve(__dirname, '..' x4)` from the
46
+ // test file IS the repo root -- there is no temp dir anywhere in that chain.
47
+ const offenders = source
48
+ .split('\n')
49
+ .map((line, i) => ({ line: line.trim(), n: i + 1 }))
50
+ .filter((x) => /WORKTREE_PATH\s*=\s*path\.resolve\(__dirname/.test(x.line));
51
+ expect(
52
+ offenders,
53
+ `tests still anchor on the real repo root:\n${offenders.map((o) => ` :${o.n} ${o.line}`).join('\n')}`,
54
+ ).toEqual([]);
55
+ });
56
+
57
+ it('AC-670-55: the sync tests run against a temp worktree, not the checkout', () => {
58
+ const source = readSuite();
59
+ // Whatever the variable is called, the cwd handed to a spawned sync must be
60
+ // one the test CREATED. mkdtemp is the only way to guarantee that: a path
61
+ // built from __dirname is by construction inside the repository.
62
+ expect(source, 'suite must create a throwaway worktree').toMatch(/mkdtemp/);
63
+ // Every spawnSync `cwd:` must point at that temp root, never at the checkout.
64
+ const cwdLines = source
65
+ .split('\n')
66
+ .filter((line) => /^\s*cwd:\s*/.test(line))
67
+ .map((line) => line.trim());
68
+ expect(cwdLines.length, 'expected at least one spawn cwd').toBeGreaterThan(0);
69
+ for (const line of cwdLines) {
70
+ expect(line, `spawn cwd escapes the sandbox: ${line}`).not.toMatch(/cwd:\s*REPO_ROOT/);
71
+ }
72
+ });
73
+ });
@@ -27,6 +27,37 @@ function writeDoc(filePath, content) {
27
27
  fs.writeFileSync(filePath, content, 'utf8');
28
28
  }
29
29
 
30
+ /**
31
+ * Copy the canonical source trees a sync run READS into `destRoot` (#645).
32
+ *
33
+ * The sync script mirrors githooks/, skills/, plugins/ and every src/ module into
34
+ * the package. Seeding the whole of `src/` except the package itself keeps this
35
+ * list from rotting: syncModules() gains a module, this helper needs no edit.
36
+ * Only reads from the real checkout -- nothing is ever written back to it.
37
+ */
38
+ function seedCanonicalSources(realRoot, destRoot) {
39
+ const real = realRoot || path.resolve(__dirname, '..', '..', '..', '..');
40
+ const dest = destRoot;
41
+ for (const tree of ['githooks', 'skills', 'plugins']) {
42
+ const from = path.join(real, tree);
43
+ if (fs.existsSync(from)) fs.cpSync(from, path.join(dest, tree), { recursive: true });
44
+ }
45
+ const srcRoot = path.join(real, 'src');
46
+ for (const name of fs.readdirSync(srcRoot)) {
47
+ // The package itself is the DESTINATION of the sync, not a canonical source;
48
+ // copying it would let the test compare a mirror against a stale mirror.
49
+ if (name === 'npm-package') continue;
50
+ const from = path.join(srcRoot, name);
51
+ if (fs.statSync(from).isDirectory()) {
52
+ fs.cpSync(from, path.join(dest, 'src', name), { recursive: true });
53
+ }
54
+ }
55
+ fs.mkdirSync(path.join(dest, 'src', 'npm-package', 'scripts'), { recursive: true });
56
+ for (const rel of ['scripts/sync-package-content.js', 'package.json']) {
57
+ fs.copyFileSync(path.join(srcRoot, 'npm-package', rel), path.join(dest, 'src', 'npm-package', rel));
58
+ }
59
+ }
60
+
30
61
  describe('checkDocsDrift', () => {
31
62
  let tempDir;
32
63
  let preCommitPath;
@@ -269,12 +300,56 @@ describe('syncGateScripts', () => {
269
300
  * @covers AC-450-01, AC-450-02, AC-450-03, AC-450-04
270
301
  */
271
302
  describe('syncHooks', () => {
272
- const WORKTREE_PATH = path.resolve(__dirname, '..', '..', '..', '..');
303
+ // #645: run the sync against a THROWAWAY worktree, never the checkout.
304
+ //
305
+ // This block used to spawn the real sync script with
306
+ // cwd: path.resolve(__dirname, '..', '..', '..', '..')
307
+ // -- four levels up from src/npm-package/scripts/__tests__ is the REPOSITORY
308
+ // ROOT, not a temp dir -- so the script's writes landed in the repo and CI's
309
+ // Gate 5 failed at vitest-worktree-guard with
310
+ // Work tree polluted by the test run: M src/npm-package/mock-policy/gate-m3.ts
311
+ //
312
+ // The pollution was CONDITIONAL (the writes are idempotent, so it only dirties
313
+ // the tree when the checked-out commit carries a stale mirror), which made it
314
+ // look flaky and encouraged "just re-run it" -- hiding the cause. The guard
315
+ // catches the consequence; a temp cwd removes the cause.
316
+ const REAL_REPO_ROOT = path.resolve(__dirname, '..', '..', '..', '..');
317
+ const TMP_WORKTREE = fs.mkdtempSync(path.join(os.tmpdir(), 'xp-gate-sync-hooks-'));
318
+ const WORKTREE_PATH = TMP_WORKTREE;
273
319
  const PKG_ROOT = path.join(WORKTREE_PATH, 'src', 'npm-package');
274
320
  const HOOKS_DIR = path.join(PKG_ROOT, 'hooks');
275
321
  const syncScript = path.join(WORKTREE_PATH, 'src', 'npm-package', 'scripts', 'sync-package-content.js');
276
322
  const nodeExe = process.execPath;
277
323
 
324
+ /**
325
+ * Lay out just enough of the repo shape inside the temp root for the sync script
326
+ * to read its canonical sources and write its mirrors. Copies only the trees the
327
+ * script touches: githooks/ (the hook source of truth) and the packages it
328
+ * mirrors into. The REAL checkout is only ever READ.
329
+ */
330
+ function seedTempWorktree() {
331
+ seedCanonicalSources(REAL_REPO_ROOT, WORKTREE_PATH);
332
+ fs.mkdirSync(path.join(PKG_ROOT, 'scripts'), { recursive: true });
333
+ fs.copyFileSync(
334
+ path.join(REAL_REPO_ROOT, 'src', 'npm-package', 'scripts', 'sync-package-content.js'),
335
+ syncScript,
336
+ );
337
+ // The script resolves its package root relative to its own location, so it
338
+ // must sit at <temp>/src/npm-package/scripts/ exactly as in the real tree.
339
+ fs.copyFileSync(
340
+ path.join(REAL_REPO_ROOT, 'src', 'npm-package', 'package.json'),
341
+ path.join(PKG_ROOT, 'package.json'),
342
+ );
343
+ }
344
+
345
+ beforeAll(() => {
346
+ seedTempWorktree();
347
+ });
348
+
349
+ afterAll(() => {
350
+ fs.rmSync(TMP_WORKTREE, { recursive: true, force: true });
351
+ });
352
+
278
353
  // Ensure hooks are re-synced once before tests run; share the result.
279
354
  let syncOk = false;
280
355
  beforeAll(() => {
@@ -473,9 +548,41 @@ describe('checkAdapterDrift', () => {
473
548
  const { spawnSync } = require('child_process');
474
549
 
475
550
  describe('sync-package-content subprocess invocation', () => {
476
- const WORKTREE_PATH = path.resolve(__dirname, '..', '..', '..', '..');
551
+ // #645: same rule as the syncHooks block above -- a test may not write into the
552
+ // repository. `path.resolve(__dirname, '..', '..', '..', '..')` IS the repo root,
553
+ // so spawning the sync script with it as cwd copied mirrors over the checkout and
554
+ // CI's Gate 5 failed at vitest-worktree-guard.
555
+ //
556
+ // This block additionally DELETES package directories to prove the sync
557
+ // recreates them, which is precisely the operation that must never touch a real
558
+ // checkout. Everything therefore runs inside a throwaway tree seeded from the
559
+ // real sources (read-only).
560
+ const REAL_REPO_ROOT = path.resolve(__dirname, '..', '..', '..', '..');
561
+ const TMP_WORKTREE = fs.mkdtempSync(path.join(os.tmpdir(), 'xp-gate-sync-sub-'));
562
+ const WORKTREE_PATH = TMP_WORKTREE;
477
563
  const PKG_ROOT = path.join(WORKTREE_PATH, 'src', 'npm-package');
478
564
  const REPO_ROOT = path.join(WORKTREE_PATH);
565
+
566
+ function seedTempWorktree() {
567
+ seedCanonicalSources(REAL_REPO_ROOT, WORKTREE_PATH);
568
+ fs.mkdirSync(path.join(PKG_ROOT, 'scripts'), { recursive: true });
569
+ fs.copyFileSync(
570
+ path.join(REAL_REPO_ROOT, 'src', 'npm-package', 'scripts', 'sync-package-content.js'),
571
+ path.join(PKG_ROOT, 'scripts', 'sync-package-content.js'),
572
+ );
573
+ fs.copyFileSync(
574
+ path.join(REAL_REPO_ROOT, 'src', 'npm-package', 'package.json'),
575
+ path.join(PKG_ROOT, 'package.json'),
576
+ );
577
+ }
578
+
579
+ beforeAll(() => {
580
+ seedTempWorktree();
581
+ });
582
+
583
+ afterAll(() => {
584
+ fs.rmSync(TMP_WORKTREE, { recursive: true, force: true });
585
+ });
479
586
  const CORE_SKILLS = [
480
587
  'admin-template-guidelines',
481
588
  'delphi-review',
@@ -1,9 +1,9 @@
1
1
  # SKILLS/DELPHI-REVIEW KNOWLEDGE BASE
2
2
 
3
3
  **Generated:** 2026-10-30
4
- **Commit:** c8a73b2a
4
+ **Commit:** 2ce96f13
5
5
  **Branch:** main
6
- **Version:** 0.35.0.0
6
+ **Version:** 0.35.1.0
7
7
 
8
8
  ## OVERVIEW
9
9
  Delphi Consensus Review — multi-round anonymous expert review (≥90% threshold, exactly 3 experts with distinct executable model IDs). Supports design, requirements, and code-walkthrough modes.
@@ -1,9 +1,9 @@
1
1
  # SKILLS/SPRINT-FLOW KNOWLEDGE BASE
2
2
 
3
3
  **Generated:** 2026-10-30
4
- **Commit:** c8a73b2a
4
+ **Commit:** 2ce96f13
5
5
  **Branch:** main
6
- **Version:** 0.35.0.0
6
+ **Version:** 0.35.1.0
7
7
 
8
8
  ## OVERVIEW
9
9
  **6-phase** development pipeline (v2.0 compact redesign, Issue #290): PREP → DESIGN → BUILD → VERIFY → SHIP → CLOSE. Phase 3/6 BUILD default build mode is **ralph-loop** (REQ-level iteration, 40-67% token savings vs parallel). HARD-GATE between DESIGN (2/6) and BUILD (3/6): design must pass Delphi review (≥90% consensus) before any coding.
@@ -1,9 +1,9 @@
1
1
  # SKILLS/TEST-SPECIFICATION-ALIGNMENT KNOWLEDGE BASE
2
2
 
3
3
  **Generated:** 2026-10-30
4
- **Commit:** c8a73b2a
4
+ **Commit:** 2ce96f13
5
5
  **Branch:** main
6
- **Version:** 0.35.0.0
6
+ **Version:** 0.35.1.0
7
7
 
8
8
  ## OVERVIEW
9
9
  Test-Specification Alignment Engine — two-stage validation ensuring tests accurately reflect requirements and design specs.