@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.
- package/README.md +106 -10
- package/README.zh-CN.md +106 -10
- 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
- package/docs/loopx/memory/2026-06-09-stale-archive-hook-guidance.md +15 -0
- package/docs/loopx/memory/README.md +25 -0
- package/docs/loopx/plans/2026-06-08-finish-audit-change-window.md +933 -0
- package/docs/loopx/plans/2026-06-08-finish-learning-audit.md +410 -0
- package/docs/loopx/plans/2026-06-09-cli-onboarding-install-surface.md +1277 -0
- package/docs/loopx/specs/installation.md +33 -0
- package/package.json +18 -2
- package/plugins/loopx/.codex-plugin/plugin.json +1 -1
- package/plugins/loopx/skills/clarify/SKILL.md +1 -1
- package/plugins/loopx/skills/debug/SKILL.md +1 -1
- package/plugins/loopx/skills/doc-readability/SKILL.md +1 -1
- package/plugins/loopx/skills/exec/SKILL.md +11 -1
- package/plugins/loopx/skills/final-review/SKILL.md +1 -1
- package/plugins/loopx/skills/finish/SKILL.md +39 -7
- package/plugins/loopx/skills/fix-review/SKILL.md +1 -1
- package/plugins/loopx/skills/go-style/SKILL.md +1 -1
- package/plugins/loopx/skills/kratos/SKILL.md +1 -1
- package/plugins/loopx/skills/plan/SKILL.md +1 -1
- package/plugins/loopx/skills/refactor-plan/SKILL.md +1 -1
- package/plugins/loopx/skills/review/SKILL.md +1 -1
- package/plugins/loopx/skills/spec/SKILL.md +1 -1
- package/plugins/loopx/skills/subagent-exec/SKILL.md +13 -1
- package/plugins/loopx/skills/tdd/SKILL.md +1 -1
- package/plugins/loopx/skills/verify/SKILL.md +1 -1
- package/scripts/claude-workflow-hook.mjs +50 -1
- package/scripts/codex-workflow-hook.mjs +33 -12
- package/scripts/install-skills.mjs +58 -3
- package/scripts/verify-skills.mjs +83 -7
- package/skills/clarify/SKILL.md +1 -1
- package/skills/debug/SKILL.md +1 -1
- package/skills/doc-readability/SKILL.md +1 -1
- package/skills/exec/SKILL.md +11 -1
- package/skills/final-review/SKILL.md +1 -1
- package/skills/finish/SKILL.md +39 -7
- package/skills/fix-review/SKILL.md +1 -1
- package/skills/go-style/SKILL.md +1 -1
- package/skills/kratos/SKILL.md +1 -1
- package/skills/plan/SKILL.md +1 -1
- package/skills/refactor-plan/SKILL.md +1 -1
- package/skills/review/SKILL.md +1 -1
- package/skills/spec/SKILL.md +1 -1
- package/skills/subagent-exec/SKILL.md +13 -1
- package/skills/tdd/SKILL.md +1 -1
- package/skills/verify/SKILL.md +1 -1
- package/src/cli.mjs +473 -86
- package/src/finish-runtime.mjs +1184 -0
- package/src/install-discovery.mjs +37 -0
- package/src/next-skill.mjs +8 -10
- package/src/workflow.mjs +19 -26
- 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?
|