@ai-content-space/loopx 0.2.4 → 0.2.7

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 (53) hide show
  1. package/README.md +106 -10
  2. package/README.zh-CN.md +106 -10
  3. package/docs/loopx/design/finish/345/255/246/344/271/240/345/256/241/350/256/241/351/234/200/346/261/202/350/256/276/350/256/241/346/226/207/346/241/243.md +707 -0
  4. package/docs/loopx/memory/2026-06-09-stale-archive-hook-guidance.md +15 -0
  5. package/docs/loopx/memory/README.md +25 -0
  6. package/docs/loopx/plans/2026-06-08-finish-audit-change-window.md +933 -0
  7. package/docs/loopx/plans/2026-06-08-finish-learning-audit.md +410 -0
  8. package/docs/loopx/plans/2026-06-09-cli-onboarding-install-surface.md +1277 -0
  9. package/docs/loopx/specs/installation.md +33 -0
  10. package/package.json +18 -2
  11. package/plugins/loopx/.codex-plugin/plugin.json +1 -1
  12. package/plugins/loopx/skills/clarify/SKILL.md +1 -1
  13. package/plugins/loopx/skills/debug/SKILL.md +1 -1
  14. package/plugins/loopx/skills/doc-readability/SKILL.md +1 -1
  15. package/plugins/loopx/skills/exec/SKILL.md +11 -1
  16. package/plugins/loopx/skills/final-review/SKILL.md +1 -1
  17. package/plugins/loopx/skills/finish/SKILL.md +39 -7
  18. package/plugins/loopx/skills/fix-review/SKILL.md +1 -1
  19. package/plugins/loopx/skills/go-style/SKILL.md +1 -1
  20. package/plugins/loopx/skills/kratos/SKILL.md +1 -1
  21. package/plugins/loopx/skills/plan/SKILL.md +1 -1
  22. package/plugins/loopx/skills/refactor-plan/SKILL.md +1 -1
  23. package/plugins/loopx/skills/review/SKILL.md +1 -1
  24. package/plugins/loopx/skills/spec/SKILL.md +1 -1
  25. package/plugins/loopx/skills/subagent-exec/SKILL.md +13 -1
  26. package/plugins/loopx/skills/tdd/SKILL.md +1 -1
  27. package/plugins/loopx/skills/verify/SKILL.md +1 -1
  28. package/scripts/claude-workflow-hook.mjs +50 -1
  29. package/scripts/codex-workflow-hook.mjs +33 -12
  30. package/scripts/install-skills.mjs +58 -3
  31. package/scripts/verify-skills.mjs +83 -7
  32. package/skills/clarify/SKILL.md +1 -1
  33. package/skills/debug/SKILL.md +1 -1
  34. package/skills/doc-readability/SKILL.md +1 -1
  35. package/skills/exec/SKILL.md +11 -1
  36. package/skills/final-review/SKILL.md +1 -1
  37. package/skills/finish/SKILL.md +39 -7
  38. package/skills/fix-review/SKILL.md +1 -1
  39. package/skills/go-style/SKILL.md +1 -1
  40. package/skills/kratos/SKILL.md +1 -1
  41. package/skills/plan/SKILL.md +1 -1
  42. package/skills/refactor-plan/SKILL.md +1 -1
  43. package/skills/review/SKILL.md +1 -1
  44. package/skills/spec/SKILL.md +1 -1
  45. package/skills/subagent-exec/SKILL.md +13 -1
  46. package/skills/tdd/SKILL.md +1 -1
  47. package/skills/verify/SKILL.md +1 -1
  48. package/src/cli.mjs +473 -86
  49. package/src/finish-runtime.mjs +1184 -0
  50. package/src/install-discovery.mjs +37 -0
  51. package/src/next-skill.mjs +8 -10
  52. package/src/workflow.mjs +19 -26
  53. package/skills/deepsearch/SKILL.md +0 -38
@@ -0,0 +1,933 @@
1
+ # Finish Audit Change Window Implementation Plan
2
+
3
+ > **For agentic workers:** REQUIRED SUB-SKILL: Use loopx:subagent-exec (recommended) or loopx:exec to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4
+
5
+ **Source:** Current conversation on 2026-06-08: finish learning/spec extraction misses content when plan execution already committed the code and `git diff` is empty.
6
+
7
+ **Goal:** Make finish learning/spec audit inspect a stable committed change window, not only the current uncommitted diff.
8
+
9
+ **Architecture:** Add a local finish baseline command that records the execution start commit before implementation begins. Extend `finish-audit` to load that baseline and persist a compact `baseline..HEAD` change window in the audit state and report, while keeping memory/spec promotion as an agent audit decision instead of automatic runtime mutation.
10
+
11
+ **Tech Stack:** Node.js ESM, `node:test`, `node:assert/strict`, Git CLI through `execFile`, Markdown skill docs.
12
+
13
+ ---
14
+
15
+ ## File Structure
16
+
17
+ - Modify: `src/finish-runtime.mjs`
18
+ - Own finish baseline persistence and finish audit change-window evidence.
19
+ - Export `finishStartStage(cwd, slug, options)` alongside `finishAuditStage`.
20
+ - Keep existing `finishRecordStage` behavior compatible with older audit state files.
21
+ - Modify: `src/cli.mjs`
22
+ - Add `loopx finish-start [slug] [--source <path>] [--json]`.
23
+ - Pass `--baseline <git-ref>` through to `finish-audit` for manual recovery when no baseline file exists.
24
+ - Modify: `test/trellis-hardening.test.mjs`
25
+ - Add regression coverage for committed-but-clean worktrees.
26
+ - Update existing finish audit shape assertions for the new `audit.change_window` object.
27
+ - Modify: `skills/finish/SKILL.md`
28
+ - Require agents to inspect `audit.change_window` before deciding no memory/spec candidates.
29
+ - Clarify that "already committed" and "empty git diff" are not reasons to skip extraction.
30
+ - Modify: `plugins/loopx/skills/finish/SKILL.md`
31
+ - Mirror the canonical finish skill changes.
32
+ - Modify: `skills/subagent-exec/SKILL.md`
33
+ - Run `loopx finish-start <slug> --source <plan-path>` before the first implementation dispatch.
34
+ - Modify: `plugins/loopx/skills/subagent-exec/SKILL.md`
35
+ - Mirror the canonical subagent-exec skill changes.
36
+ - Modify: `skills/exec/SKILL.md`
37
+ - Run `loopx finish-start <slug> --source <plan-path>` before the first task.
38
+ - Modify: `plugins/loopx/skills/exec/SKILL.md`
39
+ - Mirror the canonical exec skill changes.
40
+ - Modify: `README.md`
41
+ - Document the new baseline command and finish audit change-window behavior.
42
+ - Modify: `README.zh-CN.md`
43
+ - Document the same behavior in Chinese.
44
+ - Modify: `test/skill-governance.test.mjs`
45
+ - Require README and mirrored skills to mention `finish-start` and committed change windows.
46
+
47
+ ## Runtime Data Shape
48
+
49
+ `loopx finish-start learning-audit --source docs/loopx/plans/2026-06-08-finish-learning-audit.md` writes:
50
+
51
+ ```json
52
+ {
53
+ "schema_version": 1,
54
+ "slug": "learning-audit",
55
+ "created_at": "2026-06-08T00:00:00.000Z",
56
+ "worktree": "/repo",
57
+ "branch": "main",
58
+ "head": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
59
+ "head_short": "aaaaaaa",
60
+ "source": "docs/loopx/plans/2026-06-08-finish-learning-audit.md"
61
+ }
62
+ ```
63
+
64
+ Baseline files live under:
65
+
66
+ ```text
67
+ .loopx/finish/baselines/<slug>.json
68
+ .loopx/finish/baselines/latest.json
69
+ ```
70
+
71
+ `loopx finish-audit learning-audit` writes this compact evidence into `.loopx/finish/<audit-id>/finish-state.json`:
72
+
73
+ ```json
74
+ {
75
+ "audit": {
76
+ "change_window": {
77
+ "source": "baseline",
78
+ "baseline_ref": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
79
+ "baseline_ref_short": "aaaaaaa",
80
+ "range": "aaaaaaa..HEAD",
81
+ "commit_count": 1,
82
+ "commits": [
83
+ {
84
+ "sha": "bbbbbbb",
85
+ "subject": "feat: finish audit ledger and choice recording"
86
+ }
87
+ ],
88
+ "changed_files": [
89
+ {
90
+ "status": "M",
91
+ "path": "src/finish-runtime.mjs"
92
+ }
93
+ ],
94
+ "diff_stat": "src/finish-runtime.mjs | 80 +++++++++++++++++++++++++++++",
95
+ "uncommitted_status": [],
96
+ "source_artifacts": [
97
+ "docs/loopx/plans/2026-06-08-finish-learning-audit.md"
98
+ ]
99
+ }
100
+ }
101
+ }
102
+ ```
103
+
104
+ If no matching baseline exists, `finish-audit` falls back in this order:
105
+
106
+ 1. `--baseline <git-ref>`
107
+ 2. valid `.loopx/finish/baselines/latest.json` for the current branch/worktree when the slug was omitted
108
+ 3. valid `.loopx/finish/baselines/<slug>.json` for the current branch/worktree
109
+ 4. valid `.loopx/finish/baselines/latest.json` when the latest baseline slug also matches the current branch/worktree
110
+ 5. local configured `git merge-base HEAD <base_branch>` when the configured base branch is known; if it resolves to `HEAD`, stop with no committed range
111
+ 6. the exact configured remote base, such as `upstream/<base_branch>`, only when the local configured base cannot resolve; if it resolves to `HEAD`, stop with no committed range
112
+ 7. other discovered same-name remote base refs only when neither local nor exact configured remote base resolved
113
+ 8. origin HEAD, local `main`/`master`, and origin `main`/`master` merge-base candidates when the configured base branch is missing, unknown, or the current branch itself
114
+ 9. no committed range, with only `git status --short` recorded
115
+
116
+ ## Task 1: Baseline Command
117
+
118
+ **Files:**
119
+ - Modify: `src/finish-runtime.mjs`
120
+ - Modify: `src/cli.mjs`
121
+ - Test: `test/trellis-hardening.test.mjs`
122
+
123
+ - [ ] **Step 1: Import `rm` for test cleanup if needed**
124
+
125
+ In `test/trellis-hardening.test.mjs`, keep the existing imports and only add extra imports when the implementation needs them. The baseline tests can use the existing `mkdtemp`, `readFile`, `writeFile`, and `execFileAsync` helpers.
126
+
127
+ - [ ] **Step 2: Write the failing runtime test for `finishStartStage`**
128
+
129
+ Add `finishStartStage` to the existing import from `../src/finish-runtime.mjs`.
130
+
131
+ Add this test near the existing finish audit tests:
132
+
133
+ ```js
134
+ it('records a finish baseline before committed execution work begins', async () => {
135
+ const wd = await mkdtemp(join(tmpdir(), 'loopx-finish-start-'));
136
+ await execFileAsync('git', ['init'], { cwd: wd });
137
+ await execFileAsync('git', ['config', 'user.email', 'loopx@example.com'], { cwd: wd });
138
+ await execFileAsync('git', ['config', 'user.name', 'LoopX'], { cwd: wd });
139
+ await writeFile(join(wd, 'README.md'), 'baseline\n');
140
+ await execFileAsync('git', ['add', 'README.md'], { cwd: wd });
141
+ await execFileAsync('git', ['commit', '-m', 'init'], { cwd: wd });
142
+ const { stdout: headStdout } = await execFileAsync('git', ['rev-parse', 'HEAD'], { cwd: wd });
143
+ const head = headStdout.trim();
144
+
145
+ const result = await finishStartStage(wd, 'finish-baseline-flow', {
146
+ source: 'docs/loopx/plans/example.md',
147
+ date: new Date('2026-06-08T00:00:00.000Z'),
148
+ });
149
+
150
+ assert.equal(result.state.slug, 'finish-baseline-flow');
151
+ assert.equal(result.state.head, head);
152
+ assert.equal(result.state.head_short, head.slice(0, 7));
153
+ assert.equal(result.state.source, 'docs/loopx/plans/example.md');
154
+ assert.match(result.path, /\.loopx\/finish\/baselines\/finish-baseline-flow\.json$/);
155
+ assert.deepEqual(JSON.parse(await readFile(result.path, 'utf8')), result.state);
156
+ assert.deepEqual(JSON.parse(await readFile(result.latestPath, 'utf8')), result.state);
157
+ });
158
+ ```
159
+
160
+ - [ ] **Step 3: Run the focused test and verify it fails**
161
+
162
+ Run:
163
+
164
+ ```bash
165
+ node --test --test-name-pattern "records a finish baseline" test/trellis-hardening.test.mjs
166
+ ```
167
+
168
+ Expected: fail with `SyntaxError` or export error because `finishStartStage` does not exist yet.
169
+
170
+ - [ ] **Step 4: Implement baseline helpers**
171
+
172
+ In `src/finish-runtime.mjs`, extend imports:
173
+
174
+ ```js
175
+ import { basename, dirname, join, resolve } from 'node:path';
176
+ ```
177
+
178
+ Add these helpers near the existing finish path helpers:
179
+
180
+ ```js
181
+ export function resolveFinishBaselineRoot(cwd) {
182
+ return join(resolveFinishAuditRoot(cwd), 'baselines');
183
+ }
184
+
185
+ export function resolveFinishBaselinePath(cwd, slug) {
186
+ return join(resolveFinishBaselineRoot(cwd), `${normalizeSlug(slug) || 'finish-audit'}.json`);
187
+ }
188
+
189
+ export function resolveLatestFinishBaselinePath(cwd) {
190
+ return join(resolveFinishBaselineRoot(cwd), 'latest.json');
191
+ }
192
+ ```
193
+
194
+ Add a full-HEAD helper near `resolveGitEvidence`:
195
+
196
+ ```js
197
+ async function resolveFullHead(cwd) {
198
+ return readGitField(cwd, ['rev-parse', 'HEAD']);
199
+ }
200
+ ```
201
+
202
+ Add `finishStartStage` near `finishAuditStage`:
203
+
204
+ ```js
205
+ export async function finishStartStage(cwd, slug, { source = null, date = new Date() } = {}) {
206
+ const baselineDate = date instanceof Date ? date : new Date(date);
207
+ const normalizedSlug = normalizeSlug(slug) || 'finish-audit';
208
+ await mkdir(resolveFinishBaselineRoot(cwd), { recursive: true });
209
+
210
+ const evidence = await resolveGitEvidence(cwd);
211
+ const fullHead = await resolveFullHead(cwd);
212
+ const state = {
213
+ schema_version: FINISH_SCHEMA_VERSION,
214
+ slug: normalizedSlug,
215
+ created_at: baselineDate.toISOString(),
216
+ worktree: evidence.worktree,
217
+ branch: evidence.branch,
218
+ head: fullHead,
219
+ head_short: fullHead === 'unknown' ? evidence.head : fullHead.slice(0, 7),
220
+ source: source ? String(source) : null,
221
+ };
222
+
223
+ const path = resolveFinishBaselinePath(cwd, normalizedSlug);
224
+ const latestPath = resolveLatestFinishBaselinePath(cwd);
225
+ await writeFile(path, `${JSON.stringify(state, null, 2)}\n`);
226
+ await writeFile(latestPath, `${JSON.stringify(state, null, 2)}\n`);
227
+ return { path, latestPath, state };
228
+ }
229
+ ```
230
+
231
+ - [ ] **Step 5: Add CLI support**
232
+
233
+ In `src/cli.mjs`, update the import:
234
+
235
+ ```js
236
+ import { finishAuditStage, finishRecordStage, finishStartStage } from './finish-runtime.mjs';
237
+ ```
238
+
239
+ Add this usage line before `finish-audit`:
240
+
241
+ ```js
242
+ ' loopx finish-start [slug] [--source <path>] [--json]',
243
+ ```
244
+
245
+ Add a `case 'finish-start'` before `case 'finish-audit'`:
246
+
247
+ ```js
248
+ case 'finish-start': {
249
+ const result = await finishStartStage(process.cwd(), positionals[0], {
250
+ source: options.get('--source') || null,
251
+ });
252
+ if (options.get('--json')) {
253
+ console.log(JSON.stringify({
254
+ ok: true,
255
+ command,
256
+ path: result.path,
257
+ latestPath: result.latestPath,
258
+ state: result.state,
259
+ }, null, 2));
260
+ } else {
261
+ console.log(`finish baseline: ${result.state.slug}`);
262
+ console.log(`path: ${result.path}`);
263
+ console.log(`head: ${result.state.head_short}`);
264
+ console.log(`source: ${result.state.source ?? '(none)'}`);
265
+ }
266
+ return;
267
+ }
268
+ ```
269
+
270
+ - [ ] **Step 6: Run the focused test and verify it passes**
271
+
272
+ Run:
273
+
274
+ ```bash
275
+ node --test --test-name-pattern "records a finish baseline" test/trellis-hardening.test.mjs
276
+ ```
277
+
278
+ Expected: `# pass 1`.
279
+
280
+ - [ ] **Step 7: Commit Task 1**
281
+
282
+ ```bash
283
+ git add src/finish-runtime.mjs src/cli.mjs test/trellis-hardening.test.mjs
284
+ git commit -m "feat: record finish audit baseline"
285
+ ```
286
+
287
+ ## Task 2: Finish Audit Committed Change Window
288
+
289
+ **Files:**
290
+ - Modify: `src/finish-runtime.mjs`
291
+ - Modify: `src/cli.mjs`
292
+ - Test: `test/trellis-hardening.test.mjs`
293
+
294
+ - [ ] **Step 1: Write the failing clean-worktree regression test**
295
+
296
+ Add this test near the baseline test:
297
+
298
+ ```js
299
+ it('includes committed change evidence when the worktree diff is empty', async () => {
300
+ const wd = await mkdtemp(join(tmpdir(), 'loopx-finish-window-'));
301
+ await execFileAsync('git', ['init'], { cwd: wd });
302
+ await execFileAsync('git', ['config', 'user.email', 'loopx@example.com'], { cwd: wd });
303
+ await execFileAsync('git', ['config', 'user.name', 'LoopX'], { cwd: wd });
304
+ await writeFile(join(wd, 'README.md'), 'before\n');
305
+ await execFileAsync('git', ['add', 'README.md'], { cwd: wd });
306
+ await execFileAsync('git', ['commit', '-m', 'init'], { cwd: wd });
307
+ const baseline = await finishStartStage(wd, 'finish-window-flow', {
308
+ source: 'docs/loopx/plans/window.md',
309
+ date: new Date('2026-06-08T00:00:00.000Z'),
310
+ });
311
+
312
+ await writeFile(join(wd, 'README.md'), 'before\nafter\n');
313
+ await writeFile(join(wd, 'feature.txt'), 'new committed file\n');
314
+ await execFileAsync('git', ['add', 'README.md', 'feature.txt'], { cwd: wd });
315
+ await execFileAsync('git', ['commit', '-m', 'feat: committed finish evidence'], { cwd: wd });
316
+ const { stdout: statusStdout } = await execFileAsync('git', ['status', '--short'], { cwd: wd });
317
+ assert.equal(statusStdout, '');
318
+
319
+ const audit = await finishAuditStage(wd, 'finish-window-flow');
320
+
321
+ assert.equal(audit.state.audit.change_window.source, 'baseline');
322
+ assert.equal(audit.state.audit.change_window.baseline_ref, baseline.state.head);
323
+ assert.equal(audit.state.audit.change_window.commit_count, 1);
324
+ assert.deepEqual(audit.state.audit.change_window.commits, [{
325
+ sha: audit.state.audit.change_window.commits[0].sha,
326
+ subject: 'feat: committed finish evidence',
327
+ }]);
328
+ assert.match(audit.state.audit.change_window.commits[0].sha, /^[0-9a-f]{7,40}$/);
329
+ assert.deepEqual(audit.state.audit.change_window.changed_files, [
330
+ { status: 'M', path: 'README.md' },
331
+ { status: 'A', path: 'feature.txt' },
332
+ ]);
333
+ assert.deepEqual(audit.state.audit.change_window.uncommitted_status, []);
334
+ assert.deepEqual(audit.state.audit.change_window.source_artifacts, ['docs/loopx/plans/window.md']);
335
+ assert.match(audit.state.inputs.scanned.join('\n'), /change_range=/);
336
+ assert.match(audit.state.inputs.scanned.join('\n'), /committed_change_count=1/);
337
+ assert.match(await readFile(audit.reportPath, 'utf8'), /## Change Window/);
338
+ assert.match(await readFile(audit.reportPath, 'utf8'), /feat: committed finish evidence/);
339
+ assert.match(await readFile(audit.reportPath, 'utf8'), /feature\.txt/);
340
+ });
341
+ ```
342
+
343
+ - [ ] **Step 2: Run the focused test and verify it fails**
344
+
345
+ Run:
346
+
347
+ ```bash
348
+ node --test --test-name-pattern "includes committed change evidence" test/trellis-hardening.test.mjs
349
+ ```
350
+
351
+ Expected: fail because `audit.change_window` is missing.
352
+
353
+ - [ ] **Step 3: Implement baseline loading and Git range helpers**
354
+
355
+ In `src/finish-runtime.mjs`, add:
356
+
357
+ ```js
358
+ async function readJsonIfExists(path) {
359
+ if (!await pathExists(path)) {
360
+ return null;
361
+ }
362
+ try {
363
+ return JSON.parse(await readFile(path, 'utf8'));
364
+ } catch {
365
+ return null;
366
+ }
367
+ }
368
+
369
+ async function readFinishBaseline(cwd, slug) {
370
+ const normalizedSlug = normalizeSlug(slug) || 'finish-audit';
371
+ return await readJsonIfExists(resolveFinishBaselinePath(cwd, normalizedSlug))
372
+ ?? await readJsonIfExists(resolveLatestFinishBaselinePath(cwd));
373
+ }
374
+
375
+ function parseNameStatus(text) {
376
+ return String(text || '')
377
+ .split('\n')
378
+ .map((line) => line.trim())
379
+ .filter(Boolean)
380
+ .map((line) => {
381
+ const [status, firstPath, secondPath] = line.split('\t');
382
+ return {
383
+ status,
384
+ path: secondPath || firstPath,
385
+ };
386
+ });
387
+ }
388
+
389
+ function parseCommitLog(text) {
390
+ return String(text || '')
391
+ .split('\n')
392
+ .map((line) => line.trim())
393
+ .filter(Boolean)
394
+ .map((line) => {
395
+ const separator = line.indexOf(' ');
396
+ return {
397
+ sha: separator === -1 ? line : line.slice(0, separator),
398
+ subject: separator === -1 ? '' : line.slice(separator + 1),
399
+ };
400
+ });
401
+ }
402
+
403
+ function parseStatusShort(text) {
404
+ return String(text || '')
405
+ .split('\n')
406
+ .map((line) => line.trimEnd())
407
+ .filter(Boolean);
408
+ }
409
+ ```
410
+
411
+ Add a change-window resolver:
412
+
413
+ ```js
414
+ async function resolveMergeBaseRef(cwd, baseBranch) {
415
+ if (!baseBranch || baseBranch === 'unknown') {
416
+ return null;
417
+ }
418
+ const value = await gitOutputAllowFailure(cwd, ['merge-base', 'HEAD', baseBranch]);
419
+ return /^[0-9a-f]{7,40}$/.test(value) ? value : null;
420
+ }
421
+
422
+ async function resolveChangeWindow(cwd, slug, evidence, { baselineRef = null } = {}) {
423
+ const baseline = baselineRef
424
+ ? { head: String(baselineRef), head_short: String(baselineRef).slice(0, 7), source: null }
425
+ : await readFinishBaseline(cwd, slug);
426
+ const fallbackMergeBase = baseline ? null : await resolveMergeBaseRef(cwd, evidence.base_branch);
427
+ const ref = baseline?.head || fallbackMergeBase;
428
+ const source = baseline?.head ? 'baseline' : fallbackMergeBase ? 'merge-base' : 'none';
429
+ const statusText = await gitOutputAllowFailure(cwd, ['status', '--short']);
430
+ const uncommittedStatus = parseStatusShort(statusText);
431
+
432
+ if (!ref || ref === 'unknown') {
433
+ return {
434
+ source,
435
+ baseline_ref: null,
436
+ baseline_ref_short: null,
437
+ range: null,
438
+ commit_count: 0,
439
+ commits: [],
440
+ changed_files: [],
441
+ diff_stat: '',
442
+ uncommitted_status: uncommittedStatus,
443
+ source_artifacts: baseline?.source ? [baseline.source] : [],
444
+ };
445
+ }
446
+
447
+ const range = `${ref}..HEAD`;
448
+ const commits = parseCommitLog(await gitOutputAllowFailure(cwd, ['log', '--oneline', range]));
449
+ const changedFiles = parseNameStatus(await gitOutputAllowFailure(cwd, ['diff', '--name-status', range]));
450
+ const diffStat = await gitOutputAllowFailure(cwd, ['diff', '--stat', range]);
451
+ return {
452
+ source,
453
+ baseline_ref: ref,
454
+ baseline_ref_short: baseline?.head_short || ref.slice(0, 7),
455
+ range: `${ref.slice(0, 7)}..HEAD`,
456
+ commit_count: commits.length,
457
+ commits,
458
+ changed_files: changedFiles,
459
+ diff_stat: diffStat,
460
+ uncommitted_status: uncommittedStatus,
461
+ source_artifacts: baseline?.source ? [baseline.source] : [],
462
+ };
463
+ }
464
+ ```
465
+
466
+ - [ ] **Step 4: Persist the change window in audit state**
467
+
468
+ Change the `finishAuditStage` signature:
469
+
470
+ ```js
471
+ export async function finishAuditStage(cwd, slug, { env = process.env, date = new Date(), baselineRef = null } = {}) {
472
+ ```
473
+
474
+ Before `scannedInputs`, resolve the window:
475
+
476
+ ```js
477
+ const changeWindow = await resolveChangeWindow(cwd, normalizedSlug, evidence, { baselineRef });
478
+ ```
479
+
480
+ Append these scanned inputs:
481
+
482
+ ```js
483
+ `change_window_source=${changeWindow.source}`,
484
+ `change_range=${changeWindow.range ?? 'none'}`,
485
+ `committed_change_count=${changeWindow.commit_count}`,
486
+ `changed_files_count=${changeWindow.changed_files.length}`,
487
+ `uncommitted_change_count=${changeWindow.uncommitted_status.length}`,
488
+ ```
489
+
490
+ Add `change_window: changeWindow` inside `state.audit`.
491
+
492
+ - [ ] **Step 5: Render a `## Change Window` report section**
493
+
494
+ In `buildFinishReport`, add:
495
+
496
+ ```js
497
+ const changeWindow = auditChoices.change_window || {};
498
+ const commits = Array.isArray(changeWindow.commits) && changeWindow.commits.length > 0
499
+ ? changeWindow.commits.map((item) => `- ${singleLineText(item.sha)} ${singleLineText(item.subject)}`).join('\n')
500
+ : '- none';
501
+ const changedFiles = Array.isArray(changeWindow.changed_files) && changeWindow.changed_files.length > 0
502
+ ? changeWindow.changed_files.map((item) => `- ${singleLineText(item.status)} ${singleLineText(item.path)}`).join('\n')
503
+ : '- none';
504
+ const uncommitted = Array.isArray(changeWindow.uncommitted_status) && changeWindow.uncommitted_status.length > 0
505
+ ? changeWindow.uncommitted_status.map((item) => `- ${singleLineText(item)}`).join('\n')
506
+ : '- none';
507
+ const sourceArtifacts = Array.isArray(changeWindow.source_artifacts) && changeWindow.source_artifacts.length > 0
508
+ ? changeWindow.source_artifacts.map((item) => `- ${singleLineText(item)}`).join('\n')
509
+ : '- none';
510
+ ```
511
+
512
+ Insert this section after `## Scanned Inputs`:
513
+
514
+ ```js
515
+ '## Change Window',
516
+ '',
517
+ `- source: ${singleLineText(changeWindow.source)}`,
518
+ `- baseline_ref: ${singleLineText(changeWindow.baseline_ref_short ?? changeWindow.baseline_ref)}`,
519
+ `- range: ${singleLineText(changeWindow.range)}`,
520
+ `- committed_change_count: ${singleLineText(changeWindow.commit_count)}`,
521
+ '',
522
+ '### Commits',
523
+ '',
524
+ commits,
525
+ '',
526
+ '### Changed Files',
527
+ '',
528
+ changedFiles,
529
+ '',
530
+ '### Uncommitted Status',
531
+ '',
532
+ uncommitted,
533
+ '',
534
+ '### Source Artifacts',
535
+ '',
536
+ sourceArtifacts,
537
+ '',
538
+ ```
539
+
540
+ - [ ] **Step 6: Validate optional `change_window` shape without breaking old audits**
541
+
542
+ In `validateFinishRecordState`, after the accepted/rejected array checks, add:
543
+
544
+ ```js
545
+ if (state.audit.change_window !== undefined && !plainObject(state.audit.change_window)) {
546
+ throwInvalidFinishState();
547
+ }
548
+ ```
549
+
550
+ Do not require `change_window`; older audit records must remain recordable.
551
+
552
+ - [ ] **Step 7: Pass manual baseline refs through the CLI**
553
+
554
+ In `src/cli.mjs`, change `finishAuditStage` invocation:
555
+
556
+ ```js
557
+ const result = await finishAuditStage(process.cwd(), positionals[0], {
558
+ baselineRef: options.get('--baseline') || null,
559
+ });
560
+ ```
561
+
562
+ Update usage:
563
+
564
+ ```js
565
+ ' loopx finish-audit [slug] [--baseline <git-ref>] [--json]',
566
+ ```
567
+
568
+ - [ ] **Step 8: Update existing finish audit assertions**
569
+
570
+ In `creates finish audit runtime artifacts with audit state and report`, update the expected audit keys to include `change_window`:
571
+
572
+ ```js
573
+ assert.deepEqual(Object.keys(result.state.audit).sort(), [
574
+ 'accepted_candidates',
575
+ 'base_branch',
576
+ 'branch',
577
+ 'change_window',
578
+ 'head',
579
+ 'no_candidates_reason',
580
+ 'rejected_candidates',
581
+ 'report_candidates',
582
+ 'worktree',
583
+ ]);
584
+ ```
585
+
586
+ Add assertions:
587
+
588
+ ```js
589
+ assert.equal(persistedState.audit.change_window.source, 'none');
590
+ assert.deepEqual(persistedState.audit.change_window.commits, []);
591
+ assert.deepEqual(persistedState.audit.change_window.changed_files, []);
592
+ assert.match(reportText, /## Change Window/);
593
+ ```
594
+
595
+ - [ ] **Step 9: Run the finish tests**
596
+
597
+ Run:
598
+
599
+ ```bash
600
+ node --test --test-name-pattern "finish" test/trellis-hardening.test.mjs
601
+ ```
602
+
603
+ Expected: all finish tests pass.
604
+
605
+ - [ ] **Step 10: Commit Task 2**
606
+
607
+ ```bash
608
+ git add src/finish-runtime.mjs src/cli.mjs test/trellis-hardening.test.mjs
609
+ git commit -m "feat: audit committed finish change window"
610
+ ```
611
+
612
+ ## Task 3: Skill Workflow Updates
613
+
614
+ **Files:**
615
+ - Modify: `skills/finish/SKILL.md`
616
+ - Modify: `plugins/loopx/skills/finish/SKILL.md`
617
+ - Modify: `skills/subagent-exec/SKILL.md`
618
+ - Modify: `plugins/loopx/skills/subagent-exec/SKILL.md`
619
+ - Modify: `skills/exec/SKILL.md`
620
+ - Modify: `plugins/loopx/skills/exec/SKILL.md`
621
+ - Test: `test/skill-governance.test.mjs`
622
+
623
+ - [ ] **Step 1: Write failing governance assertions**
624
+
625
+ In `test/skill-governance.test.mjs`, extend the finish assertions:
626
+
627
+ ```js
628
+ assert.match(finish, /finish-start/);
629
+ assert.match(finish, /change_window/);
630
+ assert.match(finish, /baseline\.\.HEAD/);
631
+ assert.match(finish, /empty git diff/i);
632
+ ```
633
+
634
+ Extend the subagent/exec assertions:
635
+
636
+ ```js
637
+ assert.match(subagentDriven, /finish-start/);
638
+ assert.match(subagentDriven, /--source <plan-path>/);
639
+ assert.match(executingPlans, /finish-start/);
640
+ assert.match(executingPlans, /--source <plan-path>/);
641
+ ```
642
+
643
+ Add mirror checks:
644
+
645
+ ```js
646
+ assert.equal(
647
+ await readFile(join(repoRoot, 'plugins', 'loopx', 'skills', 'finish', 'SKILL.md'), 'utf8'),
648
+ finish,
649
+ );
650
+ assert.equal(
651
+ await readFile(join(repoRoot, 'plugins', 'loopx', 'skills', 'subagent-exec', 'SKILL.md'), 'utf8'),
652
+ subagentDriven,
653
+ );
654
+ assert.equal(
655
+ await readFile(join(repoRoot, 'plugins', 'loopx', 'skills', 'exec', 'SKILL.md'), 'utf8'),
656
+ executingPlans,
657
+ );
658
+ ```
659
+
660
+ - [ ] **Step 2: Run governance test and verify it fails**
661
+
662
+ Run:
663
+
664
+ ```bash
665
+ node --test --test-name-pattern "keeps workflow skill handoff commands unambiguous|bundles every loopx execution skill" test/skill-governance.test.mjs
666
+ ```
667
+
668
+ Expected: fail because the skills do not mention `finish-start` yet.
669
+
670
+ - [ ] **Step 3: Update `skills/subagent-exec/SKILL.md`**
671
+
672
+ In "The Process", insert a box before "Read plan, extract all tasks...":
673
+
674
+ ```dot
675
+ "Record finish baseline with loopx finish-start <slug> --source <plan-path>" [shape=box];
676
+ "Record finish baseline with loopx finish-start <slug> --source <plan-path>" -> "Read plan, extract all tasks with full text, note context, create update_plan";
677
+ ```
678
+
679
+ In the process prose before task dispatch, add:
680
+
681
+ ```markdown
682
+ ### Step 0: Record Finish Baseline
683
+
684
+ Before dispatching the first implementer, run:
685
+
686
+ ```bash
687
+ loopx finish-start <slug> --source <plan-path>
688
+ ```
689
+
690
+ Use the plan filename slug when no workflow slug is available. This preserves the starting `HEAD` so `finish-audit` can inspect `baseline..HEAD` even after implementers commit their work and the current `git diff` is empty.
691
+ ```
692
+
693
+ - [ ] **Step 4: Update `skills/exec/SKILL.md`**
694
+
695
+ After "Step 1: Load and Review Plan", add:
696
+
697
+ ```markdown
698
+ ### Step 1.5: Record Finish Baseline
699
+
700
+ Before editing files or running the first task, run:
701
+
702
+ ```bash
703
+ loopx finish-start <slug> --source <plan-path>
704
+ ```
705
+
706
+ Use the plan filename slug when no workflow slug is available. This preserves the starting `HEAD` for finish learning/spec audit after the execution commits code.
707
+ ```
708
+
709
+ - [ ] **Step 5: Update `skills/finish/SKILL.md`**
710
+
711
+ In "Step 4: Audit-First Learning Extraction", replace the allowed input list with:
712
+
713
+ ```markdown
714
+ Allowed inputs:
715
+ - `finish-state.json` `audit.change_window`, especially `baseline..HEAD` commits and changed files
716
+ - current uncommitted git diff and `git status --short`
717
+ - executed verification output
718
+ - plan, spec, and review artifacts used in this task
719
+ - explicit user decisions in the current conversation
720
+ - existing `.loopx/memory/MEMORY.md` and `.loopx/memory/index.jsonl`
721
+ - existing `docs/loopx/specs/*.md`
722
+ ```
723
+
724
+ Add this rule immediately after the list:
725
+
726
+ ```markdown
727
+ An empty git diff does not mean there is no learning candidate. When `audit.change_window.commit_count > 0`, inspect the committed range before deciding memory/spec candidates. "Already committed" is not a rejection reason; reject only when the committed change window contains no durable behavior, contract, invariant, pitfall, or user decision worth preserving.
728
+ ```
729
+
730
+ - [ ] **Step 6: Mirror skill changes**
731
+
732
+ Copy the updated canonical skill files into the plugin mirror:
733
+
734
+ ```bash
735
+ cp skills/finish/SKILL.md plugins/loopx/skills/finish/SKILL.md
736
+ cp skills/subagent-exec/SKILL.md plugins/loopx/skills/subagent-exec/SKILL.md
737
+ cp skills/exec/SKILL.md plugins/loopx/skills/exec/SKILL.md
738
+ ```
739
+
740
+ - [ ] **Step 7: Run governance and skill verification**
741
+
742
+ Run:
743
+
744
+ ```bash
745
+ node --test --test-name-pattern "keeps workflow skill handoff commands unambiguous|bundles every loopx execution skill" test/skill-governance.test.mjs
746
+ node scripts/verify-skills.mjs
747
+ ```
748
+
749
+ Expected: both commands pass.
750
+
751
+ - [ ] **Step 8: Commit Task 3**
752
+
753
+ ```bash
754
+ git add skills/finish/SKILL.md plugins/loopx/skills/finish/SKILL.md skills/subagent-exec/SKILL.md plugins/loopx/skills/subagent-exec/SKILL.md skills/exec/SKILL.md plugins/loopx/skills/exec/SKILL.md test/skill-governance.test.mjs
755
+ git commit -m "docs: preserve finish audit baselines in skills"
756
+ ```
757
+
758
+ ## Task 4: README and CLI Contract Coverage
759
+
760
+ **Files:**
761
+ - Modify: `README.md`
762
+ - Modify: `README.zh-CN.md`
763
+ - Modify: `test/skill-governance.test.mjs`
764
+ - Test: `test/trellis-hardening.test.mjs`
765
+
766
+ - [ ] **Step 1: Add README governance assertions**
767
+
768
+ In the README command list in `test/skill-governance.test.mjs`, add:
769
+
770
+ ```js
771
+ 'loopx finish-start',
772
+ 'loopx finish-audit',
773
+ 'loopx finish-record',
774
+ ```
775
+
776
+ Add these required terms to the README memory/spec section:
777
+
778
+ ```js
779
+ 'baseline..HEAD',
780
+ 'change_window',
781
+ ```
782
+
783
+ - [ ] **Step 2: Run README governance test and verify it fails**
784
+
785
+ Run:
786
+
787
+ ```bash
788
+ node --test --test-name-pattern "documents current CLI and workflow commands" test/skill-governance.test.mjs
789
+ ```
790
+
791
+ Expected: fail because README files do not document the new command yet.
792
+
793
+ - [ ] **Step 3: Update README command sections**
794
+
795
+ In both `README.md` and `README.zh-CN.md`, add `loopx finish-start [slug] [--source <path>]` near `finish-audit`.
796
+
797
+ Add a short finish audit paragraph:
798
+
799
+ ```markdown
800
+ `loopx finish-start` records the starting commit for plan execution. `loopx finish-audit` uses that baseline to include committed `baseline..HEAD` evidence, changed files, and uncommitted status in `.loopx/finish/<audit-id>/finish-state.json`, so finish learning/spec extraction still has input after the worktree is clean.
801
+ ```
802
+
803
+ Chinese version:
804
+
805
+ ```markdown
806
+ `loopx finish-start` 会记录计划执行开始时的提交。`loopx finish-audit` 使用这个基线把已提交的 `baseline..HEAD` 证据、变更文件和未提交状态写入 `.loopx/finish/<audit-id>/finish-state.json`,因此即使执行过程中已经 commit、当前工作区是 clean,finish 的记忆/spec 提取仍有稳定输入。
807
+ ```
808
+
809
+ - [ ] **Step 4: Add CLI JSON regression for `finish-start`**
810
+
811
+ In the existing CLI finish test in `test/trellis-hardening.test.mjs`, before the human `finish-audit` run, add:
812
+
813
+ ```js
814
+ const startRun = await execFileAsync('node', [
815
+ cliPath,
816
+ 'finish-start',
817
+ 'finish-cli-flow',
818
+ '--source',
819
+ 'docs/loopx/plans/finish-cli-flow.md',
820
+ '--json',
821
+ ], { cwd: wd });
822
+ const startJson = JSON.parse(startRun.stdout);
823
+ assert.equal(startJson.ok, true);
824
+ assert.equal(startJson.command, 'finish-start');
825
+ assert.equal(startJson.state.slug, 'finish-cli-flow');
826
+ assert.equal(startJson.state.source, 'docs/loopx/plans/finish-cli-flow.md');
827
+ ```
828
+
829
+ - [ ] **Step 5: Run focused CLI and README tests**
830
+
831
+ Run:
832
+
833
+ ```bash
834
+ node --test --test-name-pattern "exposes finish audit and finish record through the CLI|documents current CLI and workflow commands" test/trellis-hardening.test.mjs test/skill-governance.test.mjs
835
+ ```
836
+
837
+ Expected: both selected tests pass.
838
+
839
+ - [ ] **Step 6: Commit Task 4**
840
+
841
+ ```bash
842
+ git add README.md README.zh-CN.md test/skill-governance.test.mjs test/trellis-hardening.test.mjs
843
+ git commit -m "docs: document finish change windows"
844
+ ```
845
+
846
+ ## Task 5: Full Verification and Packaging
847
+
848
+ **Files:**
849
+ - No planned source edits unless verification exposes a regression.
850
+
851
+ - [ ] **Step 1: Run all tests**
852
+
853
+ ```bash
854
+ npm test
855
+ ```
856
+
857
+ Expected: all tests pass.
858
+
859
+ - [ ] **Step 2: Run skill verification**
860
+
861
+ ```bash
862
+ node scripts/verify-skills.mjs
863
+ ```
864
+
865
+ Expected: exits 0.
866
+
867
+ - [ ] **Step 3: Run package dry-run**
868
+
869
+ ```bash
870
+ npm pack --dry-run --json
871
+ ```
872
+
873
+ Expected: exits 0 and includes updated `src/`, `skills/`, `plugins/loopx/skills/`, README files, and tests are still excluded according to existing package policy.
874
+
875
+ - [ ] **Step 4: Check whitespace**
876
+
877
+ ```bash
878
+ git diff --check
879
+ ```
880
+
881
+ Expected: no output.
882
+
883
+ - [ ] **Step 5: Confirm the regression manually in a temp repo**
884
+
885
+ Run:
886
+
887
+ ```bash
888
+ tmpdir="$(mktemp -d)"
889
+ cd "$tmpdir"
890
+ git init
891
+ git config user.email loopx@example.com
892
+ git config user.name LoopX
893
+ printf 'before\n' > README.md
894
+ git add README.md
895
+ git commit -m init
896
+ node /Users/zhangyukun/project/loopx/src/cli.mjs finish-start finish-window --source docs/loopx/plans/window.md --json
897
+ printf 'after\n' > feature.txt
898
+ git add feature.txt
899
+ git commit -m 'feat: committed evidence'
900
+ test -z "$(git status --short)"
901
+ node /Users/zhangyukun/project/loopx/src/cli.mjs finish-audit finish-window --json
902
+ ```
903
+
904
+ Expected: the final JSON has `state.audit.change_window.commit_count` equal to `1`, `changed_files[0].path` equal to `feature.txt`, and `uncommitted_status` equal to `[]`.
905
+
906
+ - [ ] **Step 6: Commit any verification-only fixes**
907
+
908
+ If verification required code or docs changes, commit them with:
909
+
910
+ ```bash
911
+ git add <changed-files>
912
+ git commit -m "fix: harden finish audit change window"
913
+ ```
914
+
915
+ If no fixes were needed, do not create an empty commit.
916
+
917
+ ## Self-Review
918
+
919
+ - **Spec coverage:** The plan directly covers the reported failure: clean worktree after committed execution still needs finish learning/spec evidence. Task 1 records the baseline, Task 2 audits committed changes, Task 3 changes execution/finish skills, and Task 4 documents the CLI contract.
920
+ - **Placeholder scan:** The plan contains concrete file paths, command names, test snippets, data shapes, expected outputs, and commit messages.
921
+ - **Type consistency:** The plan consistently uses `finishStartStage`, `finishAuditStage`, `audit.change_window`, `baseline_ref`, `commit_count`, `changed_files`, `uncommitted_status`, and `source_artifacts`.
922
+ - **Design drift:** The plan does not automate memory/spec promotion. It only supplies durable audit evidence so the existing finish skill can make the extraction decision with committed changes visible.
923
+
924
+ ## Execution Handoff
925
+
926
+ Plan complete and saved to `docs/loopx/plans/2026-06-08-finish-audit-change-window.md`.
927
+
928
+ Two execution options:
929
+
930
+ 1. Subagent Exec (recommended) - dispatch a fresh subagent per task, review between tasks, fast iteration
931
+ 2. Inline Execution - execute tasks in this session using exec, batch execution with checkpoints
932
+
933
+ Which approach?