@llman-sdd/core 0.7.0 → 0.7.2
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/package.json +1 -1
- package/src/archive/freeze.ts +2 -0
- package/src/change/lifecycle.ts +76 -40
- package/src/config/schema.ts +1 -1
- package/src/git/spawnGit.ts +43 -2
- package/src/index.ts +5 -2
- package/src/spec/migrateNative.ts +1 -2
- package/src/spec/parser.ts +2 -1
- package/src/validation/harness.ts +3 -1
- package/templates/en/skills/llman-sdd-apply.md +9 -2
- package/templates/en/skills/llman-sdd-archive.md +1 -1
- package/templates/en/skills/llman-sdd-explore.md +1 -1
- package/templates/en/skills/llman-sdd-propose.md +2 -2
- package/templates/en/skills/llman-sdd-validate.md +1 -1
- package/templates/en/skills/llman-sdd-verify.md +4 -4
- package/templates/en/units/skills/git-native-flow-brief.md +1 -1
- package/templates/en/units/skills/git-native-flow.md +3 -3
- package/templates/zh-Hans/skills/llman-sdd-apply.md +9 -2
- package/templates/zh-Hans/skills/llman-sdd-archive.md +1 -1
- package/templates/zh-Hans/skills/llman-sdd-explore.md +1 -1
- package/templates/zh-Hans/skills/llman-sdd-propose.md +2 -2
- package/templates/zh-Hans/skills/llman-sdd-validate.md +1 -1
- package/templates/zh-Hans/skills/llman-sdd-verify.md +4 -4
- package/templates/zh-Hans/units/skills/git-native-flow-brief.md +1 -1
- package/templates/zh-Hans/units/skills/git-native-flow.md +3 -3
- package/src/ports.ts +0 -14
package/package.json
CHANGED
package/src/archive/freeze.ts
CHANGED
|
@@ -147,6 +147,8 @@ export async function runList(io: FreezeIo, sz: SevenZipPort, rootAbs: string):
|
|
|
147
147
|
if (io.exists(archiveAbs)) {
|
|
148
148
|
// Legacy/unexpected entries present only inside the 7z (no card on disk).
|
|
149
149
|
const entries = (await sz.listEntries(archiveAbs)).map((n) => n.replace(/\/$/u, ''));
|
|
150
|
+
// 7z entry names always use '/' per archive contract — POSIX by contract, not
|
|
151
|
+
// platform-native; do not switch to node:path here.
|
|
150
152
|
const top = (n: string): string => n.split('/')[0] ?? n;
|
|
151
153
|
for (const e of entries.map(top).filter((n) => DATED_RE.test(n))) {
|
|
152
154
|
if (!names.has(e)) names.add(e);
|
package/src/change/lifecycle.ts
CHANGED
|
@@ -5,7 +5,9 @@ import {
|
|
|
5
5
|
defaultBranch,
|
|
6
6
|
dirtyCount,
|
|
7
7
|
isCleanTree,
|
|
8
|
+
localBranchExists,
|
|
8
9
|
mergeBase,
|
|
10
|
+
probeForkSource,
|
|
9
11
|
revParseHead,
|
|
10
12
|
worktreeList,
|
|
11
13
|
type GitLike,
|
|
@@ -64,11 +66,42 @@ export function newChange(
|
|
|
64
66
|
export interface StartResult {
|
|
65
67
|
branch: string;
|
|
66
68
|
baseBranch: string;
|
|
69
|
+
/** Where the recorded base_branch came from (r95 deviation warning input). */
|
|
70
|
+
baseSource: 'flag' | 'worktree' | 'default';
|
|
67
71
|
baseSha: string;
|
|
68
72
|
/** Absolute worktree path when started with --worktree (r68); undefined on the classic path. */
|
|
69
73
|
worktreePath?: string;
|
|
70
74
|
}
|
|
71
75
|
|
|
76
|
+
/** r95: where attach's recorded base_branch came from. */
|
|
77
|
+
export type AttachBaseSource = 'flag' | 'config' | 'upstream' | 'default';
|
|
78
|
+
|
|
79
|
+
export interface AttachResult {
|
|
80
|
+
branch: string;
|
|
81
|
+
baseBranch: string;
|
|
82
|
+
baseSource: AttachBaseSource;
|
|
83
|
+
baseSha: string;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** r95: shared --base gate — local branches only (finalize cannot switch to a
|
|
87
|
+
* remote-tracking ref; recording one is an end-to-end dead end). */
|
|
88
|
+
function assertLocalBase(
|
|
89
|
+
git: GitLike,
|
|
90
|
+
base: string,
|
|
91
|
+
forbidden: string,
|
|
92
|
+
kind: 'new' | 'bound',
|
|
93
|
+
): void {
|
|
94
|
+
if (!localBranchExists(git, base)) {
|
|
95
|
+
throw new LifecycleError(
|
|
96
|
+
`base branch \`${base}\` does not exist as a local branch; --base must name a local branch ` +
|
|
97
|
+
'(remote-tracking refs are rejected — use the local branch name)',
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
if (base === forbidden) {
|
|
101
|
+
throw new LifecycleError(`--base must differ from the ${kind} branch \`${forbidden}\``);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
72
105
|
/**
|
|
73
106
|
* r68 worktree path (design D2): `<root>/<repo-basename>-<name>` where root is
|
|
74
107
|
* `sdd.worktree_root` (absolute, or repo-root-relative; default = the repo
|
|
@@ -84,6 +117,8 @@ function resolveWorktreePath(
|
|
|
84
117
|
naming: 'id' | 'hash' | undefined,
|
|
85
118
|
): string {
|
|
86
119
|
const toplevel = git.run(['rev-parse', '--show-toplevel']);
|
|
120
|
+
// git rev-parse --show-toplevel emits forward-slash paths even on win32
|
|
121
|
+
// (git-for-Windows normalizes) — POSIX by contract; keep '/'-splitting.
|
|
87
122
|
const cut = toplevel.lastIndexOf('/');
|
|
88
123
|
const basename = toplevel.slice(cut + 1);
|
|
89
124
|
const name =
|
|
@@ -145,29 +180,23 @@ export function startChange(
|
|
|
145
180
|
const branch = `${branchPrefix}${id}`;
|
|
146
181
|
|
|
147
182
|
if (opts.base !== undefined) {
|
|
148
|
-
|
|
149
|
-
git.runOpt(['show-ref', '--verify', '--quiet', `refs/heads/${opts.base}`]) === null &&
|
|
150
|
-
git.runOpt(['show-ref', '--verify', '--quiet', `refs/remotes/${opts.base}`]) === null
|
|
151
|
-
) {
|
|
152
|
-
throw new LifecycleError(
|
|
153
|
-
`base branch \`${opts.base}\` does not exist; --base records the fork source branch for merge-target resolution`,
|
|
154
|
-
);
|
|
155
|
-
}
|
|
156
|
-
if (opts.base === branch) {
|
|
157
|
-
throw new LifecycleError(`--base must differ from the new branch \`${branch}\``);
|
|
158
|
-
}
|
|
183
|
+
assertLocalBase(git, opts.base, branch, 'new');
|
|
159
184
|
}
|
|
160
185
|
|
|
161
186
|
// r68 fork-source resolution: --base explicit > current branch (worktree
|
|
162
187
|
// mode records the actual source, which may be a non-default branch) >
|
|
163
188
|
// default branch. The classic path keeps the r14 default-branch gate.
|
|
164
189
|
let baseBranch: string;
|
|
190
|
+
let baseSource: StartResult['baseSource'];
|
|
165
191
|
if (opts.base !== undefined) {
|
|
166
192
|
baseBranch = opts.base;
|
|
193
|
+
baseSource = 'flag';
|
|
167
194
|
} else if (opts.worktree) {
|
|
168
195
|
baseBranch = here;
|
|
196
|
+
baseSource = 'worktree';
|
|
169
197
|
} else {
|
|
170
198
|
baseBranch = defaultBranch(git);
|
|
199
|
+
baseSource = 'default';
|
|
171
200
|
if (here !== baseBranch) {
|
|
172
201
|
throw new LifecycleError(
|
|
173
202
|
`already on non-default branch \`${here}\`; use \`change attach\` to bind it, or switch to the default branch before \`change start\``,
|
|
@@ -199,13 +228,13 @@ export function startChange(
|
|
|
199
228
|
}
|
|
200
229
|
const baseSha = mergeBase(gitAt(git, worktreePath), 'HEAD', baseBranch);
|
|
201
230
|
wtIo.writeText(path, writeBinding(wtIo.readText(path), { branch, baseBranch, baseSha }));
|
|
202
|
-
return { branch, baseBranch, baseSha, worktreePath };
|
|
231
|
+
return { branch, baseBranch, baseSource, baseSha, worktreePath };
|
|
203
232
|
}
|
|
204
233
|
git.run(['switch', '-c', branch]);
|
|
205
234
|
const baseSha =
|
|
206
235
|
currentBranch(git) !== null ? mergeBase(git, 'HEAD', baseBranch) : revParseHead(git);
|
|
207
236
|
io.writeText(path, writeBinding(io.readText(path), { branch, baseBranch, baseSha }));
|
|
208
|
-
return { branch, baseBranch, baseSha };
|
|
237
|
+
return { branch, baseBranch, baseSource, baseSha };
|
|
209
238
|
}
|
|
210
239
|
|
|
211
240
|
/** `change attach`: bind the current branch — same branch gate family as start (r31). */
|
|
@@ -214,7 +243,7 @@ export function attachChange(
|
|
|
214
243
|
io: FsIo,
|
|
215
244
|
id: string,
|
|
216
245
|
opts: { force?: boolean; base?: string } = {},
|
|
217
|
-
):
|
|
246
|
+
): AttachResult {
|
|
218
247
|
const path = proposalPath(id);
|
|
219
248
|
if (!io.exists(path)) throw new LifecycleError(`proposal not found: ${path}`);
|
|
220
249
|
const existing = readBinding(io.readText(path));
|
|
@@ -223,35 +252,31 @@ export function attachChange(
|
|
|
223
252
|
`change \`${id}\` already attached to branch \`${existing.branch}\` (base ${existing.baseSha}); pass --force to rebind`,
|
|
224
253
|
);
|
|
225
254
|
}
|
|
226
|
-
const configuredBase = opts.base ?? defaultBranch(git);
|
|
227
255
|
const branch = currentBranch(git);
|
|
228
256
|
if (branch === null || branch === '') {
|
|
229
257
|
throw new LifecycleError(detachedHead('change attach'));
|
|
230
258
|
}
|
|
231
259
|
if (opts.base !== undefined) {
|
|
232
|
-
|
|
233
|
-
git.runOpt(['show-ref', '--verify', '--quiet', `refs/heads/${opts.base}`]) === null &&
|
|
234
|
-
git.runOpt(['show-ref', '--verify', '--quiet', `refs/remotes/${opts.base}`]) === null
|
|
235
|
-
) {
|
|
236
|
-
throw new LifecycleError(
|
|
237
|
-
`base branch \`${opts.base}\` does not exist; --base records the fork source branch for merge-target resolution`,
|
|
238
|
-
);
|
|
239
|
-
}
|
|
240
|
-
if (opts.base === branch) {
|
|
241
|
-
throw new LifecycleError(`--base must differ from the bound branch \`${branch}\``);
|
|
242
|
-
}
|
|
260
|
+
assertLocalBase(git, opts.base, branch, 'bound');
|
|
243
261
|
}
|
|
244
|
-
|
|
262
|
+
// r95 fork-source resolution: --base explicit > probeForkSource
|
|
263
|
+
// (branch.<name>.base > local upstream) > default branch. No signal means
|
|
264
|
+
// status quo (issue #7 compatibility: "无法推导维持现状").
|
|
265
|
+
const fork =
|
|
266
|
+
opts.base !== undefined
|
|
267
|
+
? { branch: opts.base, source: 'flag' as const }
|
|
268
|
+
: (probeForkSource(git, branch) ?? {
|
|
269
|
+
branch: defaultBranch(git),
|
|
270
|
+
source: 'default' as const,
|
|
271
|
+
});
|
|
272
|
+
if (branch === fork.branch) {
|
|
245
273
|
throw new LifecycleError(
|
|
246
274
|
`${onDefaultBranch('change attach', branch)}; create or switch to a feature branch, or use \`change start\``,
|
|
247
275
|
);
|
|
248
276
|
}
|
|
249
|
-
const baseSha = mergeBase(git, branch,
|
|
250
|
-
io.writeText(
|
|
251
|
-
|
|
252
|
-
writeBinding(io.readText(path), { branch, baseBranch: configuredBase, baseSha }),
|
|
253
|
-
);
|
|
254
|
-
return { branch, baseBranch: configuredBase, baseSha };
|
|
277
|
+
const baseSha = mergeBase(git, branch, fork.branch);
|
|
278
|
+
io.writeText(path, writeBinding(io.readText(path), { branch, baseBranch: fork.branch, baseSha }));
|
|
279
|
+
return { branch, baseBranch: fork.branch, baseSource: fork.source, baseSha };
|
|
255
280
|
}
|
|
256
281
|
|
|
257
282
|
export interface FinalizeResult {
|
|
@@ -387,14 +412,25 @@ function mergeRenameCommit(
|
|
|
387
412
|
execIo = ioAt(io, holder);
|
|
388
413
|
executedIn = holder;
|
|
389
414
|
}
|
|
415
|
+
// r96: in-place close-out — merging a branch into itself is always a no-op
|
|
416
|
+
// ("Already up to date."), so skip the merge and say so instead of running a
|
|
417
|
+
// silent self-merge. The switch still runs (a no-op when already there) so
|
|
418
|
+
// escape hatches like `archive --force` from a foreign branch land the
|
|
419
|
+
// rename and close-out commit on the bound branch, not the current one.
|
|
390
420
|
execGit.run(['switch', target]);
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
421
|
+
if (target === featureBranch) {
|
|
422
|
+
warnings.push('in-place close-out: merge target is the bound branch itself; merge skipped');
|
|
423
|
+
} else {
|
|
424
|
+
const mergeArgs =
|
|
425
|
+
method === 'ff'
|
|
426
|
+
? ['merge', '--ff-only', featureBranch]
|
|
427
|
+
: ['merge', '--squash', featureBranch];
|
|
428
|
+
if (execGit.runOpt(mergeArgs) === null) {
|
|
429
|
+
execGit.runOpt(['merge', '--abort']);
|
|
430
|
+
warnings.push(
|
|
431
|
+
`merge ${method} failed — resolve manually, e.g. \`git merge ${method === 'ff' ? '--ff-only' : '--squash'} ${featureBranch}\``,
|
|
432
|
+
);
|
|
433
|
+
}
|
|
398
434
|
}
|
|
399
435
|
|
|
400
436
|
const date = today;
|
package/src/config/schema.ts
CHANGED
|
@@ -26,7 +26,7 @@ export const specsSchema = z.object({
|
|
|
26
26
|
.string()
|
|
27
27
|
.nullish()
|
|
28
28
|
.describe(
|
|
29
|
-
'Spec verification command executed by validate for spec targets (
|
|
29
|
+
'Spec verification command executed by validate for spec targets (opt-in via --check; default skips). Placeholders: {feature_path}, {feature_dir}, {feature_name}; without placeholders it runs once per validate invocation (batch-once). Legacy `bdd.run_command` is elevated onto this key at load time.',
|
|
30
30
|
),
|
|
31
31
|
verify_prompt: z.string().nullish().describe('Extra prompt text injected during verify phase.'),
|
|
32
32
|
});
|
package/src/git/spawnGit.ts
CHANGED
|
@@ -54,12 +54,27 @@ export function isCleanTree(git: GitLike): boolean {
|
|
|
54
54
|
return git.run(['status', '--porcelain']) === '';
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
/**
|
|
57
|
+
/** True when the named LOCAL branch (refs/heads/<name>) exists. */
|
|
58
|
+
export function localBranchExists(git: GitLike, name: string): boolean {
|
|
59
|
+
return git.runOpt(['show-ref', '--verify', '--quiet', `refs/heads/${name}`]) !== null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Local-first default branch resolution: init.defaultBranch (only when it
|
|
64
|
+
* names an existing local branch) → main → master → origin/HEAD → origin/*.
|
|
65
|
+
* A valid preference wins outright — including over an existing local main
|
|
66
|
+
* (r16 resolution order, not a tie-breaker) — because the user explicitly
|
|
67
|
+
* configured it; anything else falls through to the chain untouched.
|
|
68
|
+
*/
|
|
58
69
|
// Deliberately NOT shared with defaultBranchNameFn() in validation/staleness.ts
|
|
59
70
|
// (that probe is local-only with a 'main' fallback — predecessor staleness parity). Do not merge.
|
|
60
71
|
export function defaultBranch(git: GitLike): string {
|
|
72
|
+
const preferred = git.runOpt(['config', '--get', 'init.defaultBranch']);
|
|
73
|
+
if (preferred !== null && preferred !== '' && localBranchExists(git, preferred)) {
|
|
74
|
+
return preferred;
|
|
75
|
+
}
|
|
61
76
|
for (const candidate of ['main', 'master']) {
|
|
62
|
-
if (git
|
|
77
|
+
if (localBranchExists(git, candidate)) {
|
|
63
78
|
return candidate;
|
|
64
79
|
}
|
|
65
80
|
}
|
|
@@ -78,6 +93,32 @@ export function defaultBranch(git: GitLike): string {
|
|
|
78
93
|
);
|
|
79
94
|
}
|
|
80
95
|
|
|
96
|
+
export interface ForkSource {
|
|
97
|
+
branch: string;
|
|
98
|
+
source: 'config' | 'upstream';
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* r95: read-only fork-source probes for attach — explicit signals only, local
|
|
103
|
+
* branches only. Order: `branch.<name>.base` (stacked-PR tool convention, the
|
|
104
|
+
* value must name an existing local branch) > local upstream
|
|
105
|
+
* (branch.<name>.remote = "." → @{upstream} resolves to refs/heads/*). A
|
|
106
|
+
* remote-tracking upstream is the branch's own push destination, not a fork
|
|
107
|
+
* source. null means "not derivable" — the caller falls back to defaultBranch.
|
|
108
|
+
*/
|
|
109
|
+
export function probeForkSource(git: GitLike, branch: string): ForkSource | null {
|
|
110
|
+
const configured = git.runOpt(['config', '--get', `branch.${branch}.base`]);
|
|
111
|
+
if (configured !== null && configured !== branch && localBranchExists(git, configured)) {
|
|
112
|
+
return { branch: configured, source: 'config' };
|
|
113
|
+
}
|
|
114
|
+
const upstream = git.runOpt(['rev-parse', '--symbolic-full-name', `${branch}@{upstream}`]);
|
|
115
|
+
if (upstream !== null && upstream.startsWith('refs/heads/')) {
|
|
116
|
+
const name = upstream.slice('refs/heads/'.length);
|
|
117
|
+
if (name !== branch) return { branch: name, source: 'upstream' };
|
|
118
|
+
}
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
|
|
81
122
|
export function revParseHead(git: GitLike): string {
|
|
82
123
|
return git.run(['rev-parse', 'HEAD']);
|
|
83
124
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
export * from './ports.ts';
|
|
2
|
-
|
|
3
1
|
export {
|
|
4
2
|
EXTRA_SKILLS,
|
|
5
3
|
archiveSchema,
|
|
@@ -136,14 +134,17 @@ export {
|
|
|
136
134
|
GitError,
|
|
137
135
|
defaultBranch,
|
|
138
136
|
isCleanTree,
|
|
137
|
+
localBranchExists,
|
|
139
138
|
makeSpawnGit,
|
|
140
139
|
currentBranch,
|
|
141
140
|
worktreeList,
|
|
142
141
|
probeMainCheckout,
|
|
142
|
+
probeForkSource,
|
|
143
143
|
nonMainCheckoutWarning,
|
|
144
144
|
type GitLike,
|
|
145
145
|
type WorktreeEntry,
|
|
146
146
|
type MainCheckoutProbe,
|
|
147
|
+
type ForkSource,
|
|
147
148
|
} from './git/spawnGit.ts';
|
|
148
149
|
export { DRAFT_PROPOSAL_TEMPLATE, deriveChangeId } from './change/id.ts';
|
|
149
150
|
export { parseTaskCheckboxes, type ParsedTaskCheckboxes } from './change/tasks.ts';
|
|
@@ -164,6 +165,8 @@ export {
|
|
|
164
165
|
finalizeChange,
|
|
165
166
|
newChange,
|
|
166
167
|
startChange,
|
|
168
|
+
type AttachBaseSource,
|
|
169
|
+
type AttachResult,
|
|
167
170
|
type FinalizeResult,
|
|
168
171
|
type FsIo,
|
|
169
172
|
archiveChange,
|
|
@@ -141,8 +141,7 @@ export function migrateNativeSource(source: string): MigrateResult {
|
|
|
141
141
|
for (const b of blocks) {
|
|
142
142
|
if (!b.isRule) continue;
|
|
143
143
|
rules++;
|
|
144
|
-
out.push(` @req:${b.reqIds[0] ?? ''}`);
|
|
145
|
-
out.push(` ${kw.rule}: ${b.title}`);
|
|
144
|
+
out.push(` @req:${b.reqIds[0] ?? ''}`, ` ${kw.rule}: ${b.title}`);
|
|
146
145
|
for (const line of b.descriptionLines) {
|
|
147
146
|
const text = stripBullet(line);
|
|
148
147
|
if (text !== '') out.push(` ${text}`);
|
package/src/spec/parser.ts
CHANGED
|
@@ -57,6 +57,8 @@ export interface HarnessRunOutcome {
|
|
|
57
57
|
|
|
58
58
|
/** Plain-text placeholder expansion — capability ids are kebab-constrained. */
|
|
59
59
|
export function expandRunCommand(command: string, target: HarnessTarget): string {
|
|
60
|
+
// featurePath is a repo-relative spec path (llmanspec/specs/x.feature), POSIX by
|
|
61
|
+
// contract on every host — keep '/'-splitting, do not switch to node:path here.
|
|
60
62
|
const dirEnd = target.featurePath.lastIndexOf('/');
|
|
61
63
|
const featureDir = dirEnd === -1 ? target.featurePath : target.featurePath.slice(0, dirEnd);
|
|
62
64
|
return command
|
|
@@ -74,7 +76,7 @@ interface CacheEntry {
|
|
|
74
76
|
const OUTPUT_TAIL = 200;
|
|
75
77
|
|
|
76
78
|
/**
|
|
77
|
-
* Trigger matrix (r13):
|
|
79
|
+
* Trigger matrix (r13): default skips; a nested invocation skips
|
|
78
80
|
* with a per-spec INFO; an explicit --check without a configured check_command
|
|
79
81
|
* yields a single INFO on the first spec; otherwise every expanded command
|
|
80
82
|
* executes at most once (cache keyed by the expanded string).
|
|
@@ -75,9 +75,16 @@ Run the project gates as appropriate:
|
|
|
75
75
|
- Edit `llmanspec/specs/<capability>.feature` on the branch as needed (flat or directory main file; canonical native layout: `@req:<id>` on the `规则:` block header, nested `场景:` as executable examples); run `llman-sdd validate --specs` after spec edits; commit on the branch freely.
|
|
76
76
|
- SDD validation: `llman-sdd validate <id> --strict`
|
|
77
77
|
|
|
78
|
+
**Verification ladder (opt-in discipline, escalate by cost)**:
|
|
79
|
+
- L1 minimal unit: `bun test tests/unit/<relevant>` (<1s, direct import), covers only the current change point.
|
|
80
|
+
- L2 targeted behavior: `bun test tests/bdd -t "<scenario/rule title pattern>"` — exercises only the relevant real-CLI path.
|
|
81
|
+
- L3 explicit full harness: `llman-sdd validate --check` (with `specs.check_command` configured; plain `validate` no longer runs the harness).
|
|
82
|
+
- L4 close-out backstop: `change finalize` runs the real harness pre-merge — at least one full run per close-out.
|
|
83
|
+
- Self-heal loops default to L1/L2 for fast reproduction; use L3 only when full-acceptance evidence is needed; avoid paying full-suite cost needlessly.
|
|
84
|
+
|
|
78
85
|
**Gate evidence**:
|
|
79
|
-
-
|
|
80
|
-
- Gate verdicts MUST come from the real harness:
|
|
86
|
+
- Plain `validate` is structure/state-only and claims no harness evidence; harness evidence MUST come from explicit `--check` or close-out acceptance.
|
|
87
|
+
- Gate verdicts MUST come from the real harness: claiming a harness pass requires `--check` (or close-out); the default structural gate is not a pass; on harness failure, find the root cause first (leaked env vars, nested-invocation guards, wrong cwd …) — MUST NOT label it an "inherent/self-referential property" and bypass it.
|
|
81
88
|
- Before/after criteria (counts, baselines) MUST be measured on the change branch (against the freshly computed merge-base); a value measured on the default branch is usually trivially the baseline and proves nothing.
|
|
82
89
|
- Refactors and bulk replacements: MUST compare the test count before and after; all-green gates with fewer tests is a failure.
|
|
83
90
|
|
|
@@ -7,7 +7,7 @@ metadata:
|
|
|
7
7
|
|
|
8
8
|
# LLMAN SDD Archive
|
|
9
9
|
|
|
10
|
-
Archive completed changes. Prerequisites: verify all-green, and the change is branch-bound with specs landed (or `needs_specs_change: false`). `change finalize` **auto-merges** into the base branch (target: `--into` > binding `base_branch` > default branch; method: `--method` > config `sdd.merge_method`, squash by default — feature diff + rename collapse into ONE commit on the target), **renames** change docs into `changes/archive/`, then **auto-commits** `archive(sdd): <change-id>` (`--no-commit` skips). `git push` / PR are optional.
|
|
10
|
+
Archive completed changes. Prerequisites: verify all-green, and the change is branch-bound with specs landed (or `needs_specs_change: false`). `change finalize` **auto-merges** into the base branch (target: `--into` > binding `base_branch` > default branch; when the target is the bound branch itself the merge is skipped for an in-place close-out with a notice in the output; method: `--method` > config `sdd.merge_method`, squash by default — feature diff + rename collapse into ONE commit on the target), **renames** change docs into `changes/archive/`, then **auto-commits** `archive(sdd): <change-id>` (`--no-commit` skips). `git push` / PR are optional.
|
|
11
11
|
|
|
12
12
|
## Pipeline Position
|
|
13
13
|
|
|
@@ -46,7 +46,7 @@ flowchart LR
|
|
|
46
46
|
- Write decisions back: resolved decisions go into the change's `proposal.md` "Open Questions" section.
|
|
47
47
|
- Completion criterion: every pending decision is resolved or explicitly deferred. When not triggered, the default (ask 1–3 questions) behavior is unchanged.
|
|
48
48
|
4. If a change id is relevant, read its artifacts under `llmanspec/changes/<id>/`.
|
|
49
|
-
- When diagnosing validation errors, run `llman-sdd validate <spec> --strict` first for the structural gates (Gherkin / `@req` linkage / dual-write / req_id uniqueness); when `specs.check_command` is configured, validate executes that harness
|
|
49
|
+
- When diagnosing validation errors, run `llman-sdd validate <spec> --strict` first for the structural gates (Gherkin / `@req` linkage / dual-write / req_id uniqueness); when `specs.check_command` is configured, validate only executes that harness with explicit `--check` (default skips). Failing items are pinned down in the default TOON output's `items[].issues[]`; `--output human` prints `FAIL <item_type>/<id>` lines.
|
|
50
50
|
5. Explore options and tradeoffs (2–3 options).
|
|
51
51
|
6. Assess change scale to determine if full SDD is needed.
|
|
52
52
|
7. When something crystallizes, offer to capture it (don't auto-write):
|
|
@@ -72,7 +72,7 @@ If the user just wants to **capture an idea** ("draft a proposal", "note down X"
|
|
|
72
72
|
- Flesh out `proposal.md` (Why / What Changes / Capabilities / Impact); write `design.md` only when tradeoffs/migrations matter.
|
|
73
73
|
- **Confirm seams before writing tasks.md**: list the seams to be tested and confirm with the user. A seam = the public boundary driven by `*.feature` GWT steps (CLI subprocess or public interface) — MUST reuse existing harness seams, MUST NOT invent seams detached from `.feature`; without `.feature`, the seam is the CLI subcommand or public function boundary under test.
|
|
74
74
|
- `tasks.md`: split into **vertical slices** (each task cuts a narrow but complete path through schema→API→UI→tests, independently verifiable), with `[blocked-by: <task-id>]` dependency markers. **Wide-refactor exception** (one mechanical change sweeping the codebase, a single edit breaking many call sites): sequence as expand-contract (add new beside old → migrate call sites in batches → delete old); don't force vertical slices. **tasks.md lists implementation and verification tasks only**: close-out (`change finalize` / `change archive`) is a pipeline step and MUST NOT be listed as a task — its task gate requires every task checked, so a close-out task is self-contradictory (checking it lies, leaving it blocks close-out, and `validate --strict` stays red during implementation). Before/after completion criteria (counts, baselines) MUST state they are measured on the change branch (against merge-base) — a value taken on the default branch is usually trivially the baseline.
|
|
75
|
-
- **First** `llman-sdd change start <change-id>` (recommended; clean tree on the default branch; `--worktree` to keep the current checkout, `--base <branch>` for a non-default fork source) or manually create a branch then `change attach <change-id
|
|
75
|
+
- **First** `llman-sdd change start <change-id>` (recommended; clean tree on the default branch; `--worktree` to keep the current checkout, `--base <branch>` (local branches only) for a non-default fork source) or manually create a branch then `change attach <change-id>` (attach derives the fork source from runtime signals by default and warns on deviation).
|
|
76
76
|
- **Then** edit `llmanspec/specs/<capability>.feature` (flat, or directory `llmanspec/specs/<capability>/` main file) on the bound non-default branch and commit (land specs). **Do not** edit specs before start; **do not** commit specs to the default branch just to satisfy the clean-tree gate. If already attached, do not re-run `start` (recover lost specs by checkout/recreate + `attach --force`).
|
|
77
77
|
- For changes with no contract edits, set frontmatter `needs_specs_change: false`. Enter apply when `llman-sdd show <id> --output json` shows `stage=full` with the specs-landed gate green; `readyToImplement=true` (all gates) is the completion signal gating verify/finalize.
|
|
78
78
|
- **Breaking contract changes** (removed/renamed fields, commands, tags, or stage values) MUST plan the upgrade path: write a `migrations/v<from>-v<to>/README` (upgrade guidance; a one-shot script SHALL ship with the repo when feasible) — include it in the proposal's What Changes.
|
|
@@ -85,7 +85,7 @@ This MUST pass before proceeding; failing items are listed one by one in the val
|
|
|
85
85
|
|
|
86
86
|
### 4a) Optional BDD runner (`specs:` block)
|
|
87
87
|
- Read `llmanspec/config.yaml`. Is there a `specs:` block?
|
|
88
|
-
- **Yes**: `specs.check_command` declares the project's BDD execution entry; validate
|
|
88
|
+
- **Yes**: `specs.check_command` declares the project's BDD execution entry; validate runs it only via explicit `--check` (default skips — structure-only). Authoring follows 4b regardless.
|
|
89
89
|
- **No**: if this change involves executable behavior scenarios (Given/When/Then the user will want to run), ask **once, up front** whether to enable a `specs:` verification runner block (adds a `specs:` block to `config.yaml` — runner only, does not change the lifecycle). If **yes**: show the exact `specs:` block to add (pick a `check_command` matching the project's test framework — `cargo test --features bdd` for rstest-bdd, `pytest {feature_dir} -k {feature_name} -v` for pytest-bdd), let the user confirm or edit, write it to `config.yaml`, then proceed with 4b. If **no**: features still validate structurally; BDD execution responsibility stays with the project test suite.
|
|
90
90
|
- **Do NOT silently add the `specs:` block** — always ask first. Adding it declares the project-wide BDD execution entry.
|
|
91
91
|
|
|
@@ -16,7 +16,7 @@ Validate change/spec format and staleness.
|
|
|
16
16
|
3. **BDD checks**:
|
|
17
17
|
- Validate `.feature` Gherkin and `@req` / dual-write gates on the **bound branch**; `.feature` is the harness authority — executable GWT lives only there.
|
|
18
18
|
- Lifecycle gates: `change start` / `attach` (bind branch), `finalize` (close-out; auto commit `archive(sdd): <id>`, `--no-commit` to skip) / `diff` (read-only).
|
|
19
|
-
- `llman-sdd validate --specs` enforces structural and contract gates; when `specs.check_command` is configured
|
|
19
|
+
- `llman-sdd validate --specs` enforces structural and contract gates; when `specs.check_command` is configured the full harness runs only with explicit `--check` (default skips — structure-only); a placeholder-free command runs at most once per invocation.
|
|
20
20
|
- `list --specs --json` shows `morphology` (requirementCount / requirementBoundCount / requirementUnboundCount / acceptanceCount / featureScenarioCount).
|
|
21
21
|
- Change JSON status fields: `stage` (draft/designed/planned/full) / `specsLanded` / `needsSpecsChange` / `readyToImplement` (`show --output json`).
|
|
22
22
|
{% endif %}
|
|
@@ -25,8 +25,8 @@ flowchart LR
|
|
|
25
25
|
|
|
26
26
|
- **Apply must be all-green first**: don't verify unimplemented changes.
|
|
27
27
|
- **CRITICAL must be fixed**: zero CRITICAL before archive.
|
|
28
|
-
- **Rerun the gates yourself**: MUST rerun `llman-sdd validate <id> --strict` (real harness) and the project gates; MUST NOT trust gate verdicts in the implementer's report — a mismatch is CRITICAL.
|
|
29
|
-
-
|
|
28
|
+
- **Rerun the gates yourself**: MUST rerun `llman-sdd validate <id> --check --strict` (real harness evidence) and the project gates; MUST NOT trust gate verdicts in the implementer's report — a mismatch is CRITICAL.
|
|
29
|
+
- **Harness evidence requires `--check`**: plain `validate` does not run the harness; claiming full-harness evidence MUST use explicit `--check` — the default structural gate is not a pass. Close-out runs the configured `specs.check_command`, so do not run it again just before close-out.
|
|
30
30
|
- **Don't ask "should I continue?"**: run the full verification flow and output a complete report.
|
|
31
31
|
|
|
32
32
|
{{ unit("skills/stage-guard") }}
|
|
@@ -34,7 +34,7 @@ flowchart LR
|
|
|
34
34
|
## Steps
|
|
35
35
|
1. Select the change id (or ask the user to pick from `llman-sdd list --json`).
|
|
36
36
|
2. Fast validation gate: `llman-sdd validate <id> --strict`.
|
|
37
|
-
- When diagnosing structural issues (Gherkin parse / `@req` linkage / dual-write / req_id uniqueness), run the structural validation first (
|
|
37
|
+
- When diagnosing structural issues (Gherkin parse / `@req` linkage / dual-write / req_id uniqueness), run the structural validation first (the full harness only runs via explicit `--check`; plain `validate` no longer runs it by default; a harness failure lands as an ERROR on its spec item). Failing items are listed one by one in the default TOON output's `items[].issues[]` (`--output human` prints `FAIL <item_type>/<id>` lines above the `Totals` line).
|
|
38
38
|
3. Read: `llmanspec/specs/**` (`<capability>.feature`, the single source of truth) on the branch, `proposal.md` and `design.md` (if present), `tasks.md`; ignore residual old docs under `changes/<id>/specs/`.
|
|
39
39
|
4. **Dual-axis review (kept separate so neither masks the other)** — diff against `git diff <merge-base>...HEAD` (merge-base is COMPUTED via `git merge-base <local-default> HEAD`; the stored base_sha is audit-only and MUST NOT feed range math):
|
|
40
40
|
- **Spec axis**: does the implementation satisfy the `规则:` block requirement statement (free-text description, judged by its semantics) and the nested `场景:` GWT steps? Missing/partial behaviors, wrong implementations, and scope creep not asked for by the spec → suggest minimal fixes or artifact updates. Check where before/after evidence (counts, baselines) was taken: it MUST be measured on the change branch (against the freshly computed merge-base); a value measured on the default branch is usually trivially the baseline and proves nothing.
|
|
@@ -57,7 +57,7 @@ flowchart LR
|
|
|
57
57
|
- The two axes may be reviewed in parallel (sub-agents); the report MUST present them separately, MUST NOT merge or cross-rerank (one axis passing must not mask the other failing).
|
|
58
58
|
5. **BDD verification** — only when `config.yaml` has a `specs:` block:
|
|
59
59
|
- Confirm the change is branch-bound and you are on that branch.
|
|
60
|
-
- `llman-sdd validate --specs`: Gherkin + `@req`/dual-write gates;
|
|
60
|
+
- `llman-sdd validate --specs`: Gherkin + `@req`/dual-write gates; the full harness only runs via explicit `--check` (default skips) and a failure maps to an ERROR on the matching spec item.
|
|
61
61
|
- Optional read-only review: `llman-sdd change diff <id>` (or `--export-patch <path>`) — review/export only, never an apply step.
|
|
62
62
|
- Next step after verify passes: `llman-sdd-archive` (not inline finalize here).
|
|
63
63
|
{% if specs_verify_prompt %}
|
|
@@ -7,4 +7,4 @@ Hard rules:
|
|
|
7
7
|
2. No contract edits → `needs_specs_change: false`. Enter apply when `stage=full` and the specs-landed gate passes; `readyToImplement=true` (all gates green) is the completion signal gating verify/finalize.
|
|
8
8
|
3. Close-out: `change finalize` (auto commit `archive(sdd): <id>`; `--no-commit` to skip).
|
|
9
9
|
4. **Do not** commit specs on the default branch; if already attached, do not re-run `start`.
|
|
10
|
-
5. Worktree (optional): `change start --worktree` creates the branch in a dedicated worktree without hijacking the current checkout (`--base <branch
|
|
10
|
+
5. Worktree (optional): `change start --worktree` creates the branch in a dedicated worktree without hijacking the current checkout (`--base <branch>`, local branches only) records a non-default fork source; attach derives the fork source from runtime signals by default and warns on deviation from the default branch; finalize skips the merge for an in-place close-out when the target is the bound branch, and runs in place when the target is held by another worktree (location annotated in output).
|
|
@@ -44,7 +44,7 @@ Worktree decision table:
|
|
|
44
44
|
| Working style | Command | Criteria |
|
|
45
45
|
|---|---|---|
|
|
46
46
|
| Single checkout | `llman-sdd change start <id>` | On the default branch with a clean tree; switches this checkout to the new branch |
|
|
47
|
-
| Keep current checkout / parallel changes | `llman-sdd change start <id> --worktree` | Branch lives in a dedicated worktree (`sdd.worktree_root` / `sdd.worktree_naming` config; default sibling of the repo root), current checkout untouched, output includes the worktree path; pair with `--base <branch>` for a non-default fork source |
|
|
48
|
-
| Already on a feature branch (incl. manual wt/git-worktree) | `llman-sdd change attach <id>` | Branch already exists; `--base
|
|
47
|
+
| Keep current checkout / parallel changes | `llman-sdd change start <id> --worktree` | Branch lives in a dedicated worktree (`sdd.worktree_root` / `sdd.worktree_naming` config; default sibling of the repo root), current checkout untouched, output includes the worktree path; pair with `--base <branch>` (local branches only) for a non-default fork source |
|
|
48
|
+
| Already on a feature branch (incl. manual wt/git-worktree) | `llman-sdd change attach <id>` | Branch already exists; fork source resolves as `--base` > `branch.<name>.base` / local upstream probe > default branch, with a WARNING when it deviates from the default; `--base` accepts local branches only |
|
|
49
49
|
|
|
50
|
-
finalize target location: when the target branch is held by another worktree, `llman-sdd change finalize <id>` / `llman-sdd change archive <id>` run the merge, rename and commit inside that worktree (output includes `executed in target worktree <path>`); a dirty holding worktree aborts with disposal options and zero writes.
|
|
50
|
+
finalize target location: when the target is the bound branch itself the merge is skipped for an in-place close-out (output carries an in-place close-out notice); when the target branch is held by another worktree, `llman-sdd change finalize <id>` / `llman-sdd change archive <id>` run the merge, rename and commit inside that worktree (output includes `executed in target worktree <path>`); a dirty holding worktree aborts with disposal options and zero writes.
|
|
@@ -75,9 +75,16 @@ flowchart LR
|
|
|
75
75
|
- 分支上按需编辑 `llmanspec/specs/<capability>.feature`(扁平或目录主文件;统一原生分层:`@req:<id>` 挂 `规则:` 块头、嵌套 `场景:` 为可执行示例),spec 改动后跑 `llman-sdd validate --specs`;分支上可自由提交。
|
|
76
76
|
- SDD 校验:`llman-sdd validate <id> --strict`
|
|
77
77
|
|
|
78
|
+
**验证阶梯(opt-in 纪律,按成本逐级扩大)**:
|
|
79
|
+
- L1 最小单元:`bun test tests/unit/<相关>`(<1s,直接导入),仅覆盖当前改动点。
|
|
80
|
+
- L2 定向行为:`bun test tests/bdd -t "<场景/规则标题模式>"`——只跑相关场景的真实 CLI 链路。
|
|
81
|
+
- L3 显式全量:`llman-sdd validate --check`(配置 `specs.check_command` 时执行全量 harness;缺省 validate 不再执行 harness)。
|
|
82
|
+
- L4 收口兜底:`change finalize` 预合并强制真实 harness——每次收口至少 1 次全量。
|
|
83
|
+
- 自修复循环默认用 L1/L2 快速复现;只有需要全量验收时才 L3;避免无谓白付全量成本。
|
|
84
|
+
|
|
78
85
|
**门禁证据**:
|
|
79
|
-
-
|
|
80
|
-
- 门禁结论 MUST 来自真实 harness
|
|
86
|
+
- 缺省 `validate` 只做结构/状态门,不声明 harness 证据;harness 证据 MUST 经显式 `--check` 或收口验收取得。
|
|
87
|
+
- 门禁结论 MUST 来自真实 harness:声称 harness 合格时 MUST 已用 `--check`(或收口);缺省结构门的通过不是通过;harness 失败 MUST 先查根因(环境变量泄漏、嵌套调用守卫、工作目录错误等),MUST NOT 以「固有/自指属性」定性后绕过。
|
|
81
88
|
- 前后对比类判据(计数、基线)MUST 在 change 分支上测量(相对现算 merge-base);默认分支测得的值通常恒为基线,不构成证据。
|
|
82
89
|
- 重构或批量替换类 task:MUST 对比改动前后测试用例数;门禁全绿但用例数下降视为失败。
|
|
83
90
|
|
|
@@ -7,7 +7,7 @@ metadata:
|
|
|
7
7
|
|
|
8
8
|
# LLMAN SDD 归档
|
|
9
9
|
|
|
10
|
-
归档已完成的变更。前置:verify 全绿,且 change 已绑定分支、specs 已落地(或 `needs_specs_change: false`)。`change finalize` **自动合并**到基准分支(目标:`--into` > 绑定 `base_branch` >
|
|
10
|
+
归档已完成的变更。前置:verify 全绿,且 change 已绑定分支、specs 已落地(或 `needs_specs_change: false`)。`change finalize` **自动合并**到基准分支(目标:`--into` > 绑定 `base_branch` > 默认分支;目标即绑定分支时跳过合并就地收口,输出含 in-place close-out 提示;方式:`--method` > 配置 `sdd.merge_method`,默认 squash——feature diff + 改名收敛为目标分支单个 commit)、**改名** change 文档到 `changes/archive/`、**自动提交** `archive(sdd): <change-id>`(`--no-commit` 跳过)。`git push` / PR 仅可选。
|
|
11
11
|
|
|
12
12
|
## Pipeline 位置
|
|
13
13
|
|
|
@@ -46,7 +46,7 @@ flowchart LR
|
|
|
46
46
|
- 决策回写:已解决的决策写进该 change 的 `proposal.md`「Open Questions」段。
|
|
47
47
|
- 完成判据:每个待定决策都已解决或显式推迟。未触发时保持默认(问 1–3 个问题)。
|
|
48
48
|
4. 涉及某个 change id 时,读 `llmanspec/changes/<id>/` 下的工件。
|
|
49
|
-
- 诊断校验错误先跑 `llman-sdd validate <spec> --strict` 过结构门禁(Gherkin / `@req` 链接 / 双写 / req_id 唯一性);配置了 `specs.check_command` 时 validate
|
|
49
|
+
- 诊断校验错误先跑 `llman-sdd validate <spec> --strict` 过结构门禁(Gherkin / `@req` 链接 / 双写 / req_id 唯一性);配置了 `specs.check_command` 时 validate 仅经显式 `--check` 执行该 harness(缺省跳过)。失败项在缺省 TOON 输出的 `items[].issues[]` 逐条指明;`--output human` 输出人读 `FAIL <item_type>/<id>` 行。
|
|
50
50
|
5. 探索 2–3 个选项与权衡。
|
|
51
51
|
6. 判断变更规模,确定是否走完整 SDD。
|
|
52
52
|
7. 结论清晰时建议用户记录(勿自动写):
|
|
@@ -72,7 +72,7 @@ flowchart LR
|
|
|
72
72
|
- 充实 `proposal.md`(Why / What Changes / Capabilities / Impact);仅当有权衡/迁移时写 `design.md`。
|
|
73
73
|
- **写 tasks.md 前确认测试边界(seam)**:列出要测的 seam 并与用户确认。seam = 由 `*.feature` GWT 步骤驱动的公共边界(CLI 子进程或公共接口)——MUST 复用既有 harness seam,MUST NOT 脱离 `.feature` 凭空发明;没有 `.feature` 时,seam = 被测的 CLI 子命令或公共函数边界。
|
|
74
74
|
- `tasks.md` 按**垂直切片**拆(每个 task 打穿 schema→API→UI→tests 一条窄而完整的路径,可独立验证),带 `[blocked-by: <task-id>]` 依赖标记。**大范围重构例外**(一个机械改动扫全库、单点编辑牵动大量调用处):按先加后删排序(新的加在旧的旁边 → 分批迁移调用处 → 删旧的),不强拆垂直切片。**tasks.md 只列实现与验证任务**:收口(`change finalize` / `change archive`)是流水线步骤,MUST NOT 列为任务——收口的任务门要求全部任务已勾,列了必然自相矛盾(勾选即虚报、不勾则收口被拒,实施期 `validate --strict` 永红)。前后对比类完成判据(计数、基线)MUST 注明在 change 分支上测量(相对 merge-base)——默认分支测得的值通常恒为基线。
|
|
75
|
-
- **先** `llman-sdd change start <change-id>`(推荐;默认分支上工作树干净时;保留当前检出用 `--worktree`,非默认分叉源用 `--base <branch
|
|
75
|
+
- **先** `llman-sdd change start <change-id>`(推荐;默认分支上工作树干净时;保留当前检出用 `--worktree`,非默认分叉源用 `--base <branch>`(仅本地分支))或手动建分支后 `change attach <change-id>`(attach 缺省按运行时信号推导分叉源,偏离默认分支时输出 WARNING)。
|
|
76
76
|
- **再**在绑定的非默认分支编辑 `llmanspec/specs/<capability>.feature`(扁平,或目录 `llmanspec/specs/<capability>/` 内主文件)并 commit(落地 specs)。**不要**在 start 前改 specs;**不要**为过干净树门禁把 specs commit 到默认分支。已 attach 勿重复 `start`(specs 丢失用 checkout/重建 + `attach --force` 恢复)。
|
|
77
77
|
- 无合约编辑的 change 设 frontmatter `needs_specs_change: false`。`llman-sdd show <id> --output json` 显示 `stage=full` 且 specs-landed 门通过即可进 apply;`readyToImplement=true`(全门)是 verify/finalize 的完成信号。
|
|
78
78
|
- **破坏性合约变更**(移除/重命名字段、命令、tag 或 stage 值域)MUST 规划升级路径:`migrations/v<from>-v<to>/` 下写 README(升级提示;一次性脚本可行时随仓库提供)——写进提案 What Changes。
|
|
@@ -85,7 +85,7 @@ MUST 通过才能继续;失败项在 validate 输出的 `items[].issues[]` 逐
|
|
|
85
85
|
|
|
86
86
|
### 4a) 可选 BDD runner(`specs:` 段)
|
|
87
87
|
- 读 `llmanspec/config.yaml` 是否含 `specs:` 段:
|
|
88
|
-
- **有**:`specs.check_command` 是项目的 BDD 执行入口;validate
|
|
88
|
+
- **有**:`specs.check_command` 是项目的 BDD 执行入口;validate 仅在显式 `--check` 时执行它(缺省跳过,仅结构/状态门)。撰写仍按 4b。
|
|
89
89
|
- **无**:若本次 change 含可执行行为场景(用户会想运行的 Given/When/Then),**一次性前置**询问是否启用 `specs:` 验证 runner 段(会向 `config.yaml` 加一个 `specs:` 段——仅 runner,不改生命周期)。**是**:展示要加的精确 `specs:` 段(`check_command` 选匹配项目测试框架的——rstest-bdd 用 `cargo test --features bdd`,pytest-bdd 用 `pytest {feature_dir} -k {feature_name} -v`),用户确认或修改后写入 `config.yaml`,再按 4b 继续。**否**:feature 仍做结构校验;BDD 执行责任始终在项目测试套件。
|
|
90
90
|
- **MUST NOT 静默添加 `specs:` 段**——总是先问。添加它会向全项目声明 BDD 执行入口。
|
|
91
91
|
|
|
@@ -16,7 +16,7 @@ metadata:
|
|
|
16
16
|
3. **Spec 校验**:
|
|
17
17
|
- 在**绑定分支**上验证 `.feature` Gherkin 与 `@req` / 双写门禁;`.feature` 是 harness 权威——可执行 GWT 只在其中维护。
|
|
18
18
|
- 生命周期门禁:`change start` / `attach`(绑定分支)、`finalize`(收口;自动提交 `archive(sdd): <id>`,`--no-commit` 跳过)/ `diff`(只读)。
|
|
19
|
-
- `llman-sdd validate --specs` 做结构与合约门禁;配置 `specs.check_command`
|
|
19
|
+
- `llman-sdd validate --specs` 做结构与合约门禁;配置 `specs.check_command` 时全量 harness 仅经显式 `--check` 执行(缺省跳过,仅结构/状态门),无占位符的命令每次调用至多执行一次。
|
|
20
20
|
- `list --specs --json` 查看 `morphology`(requirementCount / requirementBoundCount / requirementUnboundCount / acceptanceCount / featureScenarioCount)。
|
|
21
21
|
- change JSON 状态字段:`stage`(draft/designed/planned/full)/ `specsLanded` / `needsSpecsChange` / `readyToImplement`(`show --output json`)。
|
|
22
22
|
{% endif %}
|
|
@@ -25,8 +25,8 @@ flowchart LR
|
|
|
25
25
|
|
|
26
26
|
- **必须先 apply 全绿**:未完成实现的 change 跳过验证。
|
|
27
27
|
- **CRITICAL 必须修复**:归档前清零。
|
|
28
|
-
- **亲自复跑门禁**:MUST 亲自重跑 `llman-sdd validate <id> --strict`(真实 harness
|
|
29
|
-
-
|
|
28
|
+
- **亲自复跑门禁**:MUST 亲自重跑 `llman-sdd validate <id> --check --strict`(真实 harness 证据)与项目门禁,MUST NOT 采信实现者报告的门禁结论;复跑结果与报告不符 → CRITICAL。
|
|
29
|
+
- **harness 证据必须 `--check`**:缺省 `validate` 不执行 harness,声称全量 harness 证据 MUST 经显式 `--check`(缺省结构门的通过不是通过);收口会执行已配置的 `specs.check_command`,收口前不必再跑一遍。
|
|
30
30
|
- **不要问「要不要继续」**:跑完整验证流程,输出完整报告。
|
|
31
31
|
|
|
32
32
|
{{ unit("skills/stage-guard") }}
|
|
@@ -34,7 +34,7 @@ flowchart LR
|
|
|
34
34
|
## 步骤
|
|
35
35
|
1. 确定 change id(不明确时让用户从 `llman-sdd list --json` 选)。
|
|
36
36
|
2. 快速校验门禁:`llman-sdd validate <id> --strict`。
|
|
37
|
-
- 诊断结构问题(Gherkin 解析 / `@req` 链接 / 双写 / req_id
|
|
37
|
+
- 诊断结构问题(Gherkin 解析 / `@req` 链接 / 双写 / req_id 唯一性)先跑结构校验(全量 harness 仅经显式 `--check` 执行,缺省不再自动执行;harness 失败以 ERROR 落在对应 spec 条目)。失败项在缺省 TOON 输出的 `items[].issues[]` 逐条列出(`--output human` 输出 `FAIL <item_type>/<id>` 行,位于 `Totals` 上方)。
|
|
38
38
|
3. 阅读:分支上的 `llmanspec/specs/**`(`<capability>.feature`,唯一事实来源)、`proposal.md` 与 `design.md`(如有)、`tasks.md`;`changes/<id>/specs/` 若有残留旧文档可忽略。
|
|
39
39
|
4. **双轴审查(两轴分离,互不掩盖)**——对比 diff(`git diff <merge-base>...HEAD`,merge-base 现算 `git merge-base <本地默认分支> HEAD`;存储的 base_sha 仅审计、MUST NOT 参与范围计算):
|
|
40
40
|
- **合约轴**:实现是否满足 `规则:` 块的需求表述(描述为自由文本,以其语义为准)与嵌套 `场景:` 的 GWT 步骤?缺失/部分实现、错误实现、spec 未要求的超范围改动 → 给最小修复建议或建议更新工件。前后对比类证据(计数、基线)核对测量位置:MUST 在 change 分支上测量(相对现算 merge-base);默认分支测得的值通常恒为基线,不构成证据。
|
|
@@ -57,7 +57,7 @@ flowchart LR
|
|
|
57
57
|
- 两轴可并行(sub-agent)审查;报告 MUST 分离呈现,MUST NOT 合并或交叉重排(一轴通过不能掩盖另一轴失败)。
|
|
58
58
|
5. **Spec 验证**——仅当 `config.yaml` 含 `specs:` 段:
|
|
59
59
|
- 确认 change 已绑定分支且当前在该分支上。
|
|
60
|
-
- `llman-sdd validate --specs`:Gherkin + `@req
|
|
60
|
+
- `llman-sdd validate --specs`:Gherkin + `@req`/双写门禁;全量 harness 仅经 `--check` 显式执行(缺省跳过),失败映射为对应 spec 条目的 ERROR。
|
|
61
61
|
- 可选只读审查:`llman-sdd change diff <id>`(或 `--export-patch <path>`)——仅审查/导出,绝不当作 apply 步骤。
|
|
62
62
|
- verify 通过后下一步 `llman-sdd-archive`(勿在此 inline finalize)。
|
|
63
63
|
{% if specs_verify_prompt %}
|
|
@@ -7,4 +7,4 @@
|
|
|
7
7
|
2. 无合约编辑 → `needs_specs_change: false`。`stage=full` 且 specs-landed 门通过即可进 apply;`readyToImplement=true`(全门绿)是 verify/finalize 前的完成信号。
|
|
8
8
|
3. 收口用 `change finalize`(自动提交 `archive(sdd): <id>`;`--no-commit` 跳过)。
|
|
9
9
|
4. **禁止**在默认分支 commit specs;已 attach 勿重复 `start`。
|
|
10
|
-
5. worktree(可选):`change start --worktree` 在独立 worktree 建分支、不动当前检出(`--base <branch>`
|
|
10
|
+
5. worktree(可选):`change start --worktree` 在独立 worktree 建分支、不动当前检出(`--base <branch>` 仅本地分支)记录分叉源;attach 缺省按运行时信号推导分叉源、偏离默认分支时输出 WARNING;finalize 目标即绑定分支时跳过合并就地收口,目标被其他 worktree 持有时自动在该 worktree 内执行(输出标注位置)。
|
|
@@ -44,7 +44,7 @@ Worktree 决策表:
|
|
|
44
44
|
| 工作形态 | 命令 | 判据 |
|
|
45
45
|
|---|---|---|
|
|
46
46
|
| 单检出 | `llman-sdd change start <id>` | 在默认分支且树干净;直接切到新分支 |
|
|
47
|
-
| 保留当前检出 / 并行 change | `llman-sdd change start <id> --worktree` | 分支建于独立 worktree(`sdd.worktree_root` / `sdd.worktree_naming` 可调,缺省仓库根兄弟目录),当前检出不动,输出含 worktree 路径;配 `--base <branch
|
|
48
|
-
| 已在 feature 分支(含手工 wt/git-worktree) | `llman-sdd change attach <id>` |
|
|
47
|
+
| 保留当前检出 / 并行 change | `llman-sdd change start <id> --worktree` | 分支建于独立 worktree(`sdd.worktree_root` / `sdd.worktree_naming` 可调,缺省仓库根兄弟目录),当前检出不动,输出含 worktree 路径;配 `--base <branch>`(仅本地分支)记录非默认分叉源 |
|
|
48
|
+
| 已在 feature 分支(含手工 wt/git-worktree) | `llman-sdd change attach <id>` | 分支已存在;分叉源按 `--base` > `branch.<name>.base` / 本地 upstream 推导 > 默认分支,偏离默认分支时输出 WARNING;`--base` 仅接受本地分支 |
|
|
49
49
|
|
|
50
|
-
finalize
|
|
50
|
+
finalize 目标定位:目标即绑定分支时跳过合并就地收口(输出含 in-place close-out 提示);目标分支被其他 worktree 持有时,`llman-sdd change finalize <id>` / `llman-sdd change archive <id>` 自动在该 worktree 内完成合并、改名与提交(输出含 `executed in target worktree <path>`);持有 worktree 脏时中止报错并列出处置选项(零写入)。
|
package/src/ports.ts
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Ports (spec monorepo-structure r3): domain logic must not touch the
|
|
3
|
-
* filesystem, git subprocesses, or the terminal directly — side effects are
|
|
4
|
-
* injected through these interfaces. Nothing under packages/core may import
|
|
5
|
-
* `node:fs`, `Bun.$`, or prompt libraries; runtimes wire adapters in.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
/** Interaction port. predecessor ships an @inquirer/prompts adapter; a future ink TUI
|
|
9
|
-
* ships its own adapter (the two must never run in the same process). */
|
|
10
|
-
export interface PromptDriver {
|
|
11
|
-
select<T extends string>(message: string, choices: readonly T[]): Promise<T>;
|
|
12
|
-
multiselect<T extends string>(message: string, choices: readonly T[]): Promise<T[]>;
|
|
13
|
-
confirm(message: string): Promise<boolean>;
|
|
14
|
-
}
|