@planu/cli 5.3.51 → 5.3.55
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/CHANGELOG.md +89 -0
- package/dist/cli/commands/package-handoff.d.ts +3 -0
- package/dist/cli/commands/package-handoff.js +43 -0
- package/dist/cli/commands/spec.js +3 -8
- package/dist/cli/commands/status.d.ts +52 -1
- package/dist/cli/commands/status.js +39 -35
- package/dist/cli/router.js +5 -2
- package/dist/engine/git/canonical-branch.d.ts +2 -0
- package/dist/engine/git/canonical-branch.js +31 -0
- package/dist/engine/handoff-artifacts/schemas.d.ts +2 -0
- package/dist/engine/handoff-artifacts/schemas.js +1 -0
- package/dist/engine/human-summary.js +1 -1
- package/dist/engine/lifecycle-reconciliation.js +3 -0
- package/dist/engine/scope-boundaries/contradiction-checker.js +29 -5
- package/dist/engine/sdd-model-routing.js +7 -2
- package/dist/engine/spec-format/lean-spec-generator.js +1 -1
- package/dist/engine/staleness/stale-implementing.js +19 -58
- package/dist/engine/text-signal-boundaries.js +3 -1
- package/dist/engine/validator/spec-compliance-runner.d.ts +1 -0
- package/dist/engine/validator/spec-compliance-runner.js +13 -6
- package/dist/engine/validator/validation-report-writer.js +1 -0
- package/dist/engine/workflow-validator/worktree-protocol.js +45 -36
- package/dist/storage/transition-log.d.ts +2 -14
- package/dist/storage/transition-log.js +84 -45
- package/dist/tools/challenge-spec/challenge-report.js +42 -11
- package/dist/tools/challenge-spec/scenarios-utils.js +9 -2
- package/dist/tools/generate-orchestration-script.js +2 -1
- package/dist/tools/register-platform-tools/design-stack-tools.js +1 -1
- package/dist/tools/suggest-tooling/orchestration-generator.js +2 -2
- package/dist/tools/sync-spec-state-handler.js +20 -7
- package/dist/tools/update-status/dod-gates.js +36 -6
- package/dist/tools/update-status/file-sync.d.ts +1 -0
- package/dist/tools/update-status/file-sync.js +45 -13
- package/dist/tools/update-status/index.js +12 -1
- package/dist/tools/update-status/transition-guard.d.ts +1 -0
- package/dist/tools/update-status/transition-guard.js +50 -4
- package/dist/tools/validate.js +5 -2
- package/dist/types/handoff-artifacts.d.ts +2 -0
- package/package.json +20 -20
- package/planu-plugin.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,92 @@
|
|
|
1
|
+
## [5.3.55] - 2026-08-25
|
|
2
|
+
|
|
3
|
+
### Bug Fixes
|
|
4
|
+
- fix(update-status): classify git-status degradation in spec artifact guard
|
|
5
|
+
- fix(worktree): drop only proper ancestors when eliminating base candidates
|
|
6
|
+
- fix(challenge): stop telling users to record accepted-risk evidence
|
|
7
|
+
- fix(worktree): validate worktree base against the canonical branch
|
|
8
|
+
- fix(gates): surface schema blockers in dod-gate rejections
|
|
9
|
+
- fix(contradiction-checker): close Path B on the criterion, not the scope
|
|
10
|
+
- fix(cli): add package-handoff command and honest EISDIR blocker
|
|
11
|
+
- fix(contradiction-checker): require predicate-position action assertion
|
|
12
|
+
- fix(spec-format): anchor single-line GIVEN/WHEN/THEN criterion regex
|
|
13
|
+
- fix(reconciliation): refresh stored title on the forward reconcile route
|
|
14
|
+
- fix(challenge-spec): stop reading a negated word as risk acceptance
|
|
15
|
+
- fix(cli): forward SDD evidence flags from spec status to the canonical mapping
|
|
16
|
+
- fix(update-status): refresh stored title from rewritten frontmatter
|
|
17
|
+
- fix(challenge): close resolution bullets on blank line
|
|
18
|
+
- fix(challenge): match resolution evidence against finding text
|
|
19
|
+
- fix(challenge): scope networkApi to a proximity-guarded bare API token
|
|
20
|
+
- fix(storage): stream transition-log reads to survive oversized lines
|
|
21
|
+
- fix(challenge): stop affirming capabilities from finding-field narration
|
|
22
|
+
- fix(update-status): discard a spec whose artifact is absent
|
|
23
|
+
- fix(validate): report unresolvable test links as unverifiable, not missing
|
|
24
|
+
- fix(update-status): drop the narrative JSDoc from the spec-artifact gate
|
|
25
|
+
- fix(update-status): refuse implementing transition on uncommitted spec.md
|
|
26
|
+
- fix(update-status): name the challenge-resolution ingress in gate blockers
|
|
27
|
+
|
|
28
|
+
### Refactoring
|
|
29
|
+
- refactor(contradiction-checker): drop rationale comments in favor of names
|
|
30
|
+
|
|
31
|
+
### Chores
|
|
32
|
+
- chore(planu): file SPEC-1624, clear integrated worktrees
|
|
33
|
+
- chore(planu): refresh session context
|
|
34
|
+
- chore(planu): refresh session context and release ledger
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
## [5.3.54] - 2026-08-25
|
|
38
|
+
|
|
39
|
+
### Bug Fixes
|
|
40
|
+
- fix(worktree): drop only proper ancestors when eliminating base candidates
|
|
41
|
+
- fix(challenge): stop telling users to record accepted-risk evidence
|
|
42
|
+
- fix(worktree): validate worktree base against the canonical branch
|
|
43
|
+
- fix(gates): surface schema blockers in dod-gate rejections
|
|
44
|
+
- fix(contradiction-checker): close Path B on the criterion, not the scope
|
|
45
|
+
- fix(cli): add package-handoff command and honest EISDIR blocker
|
|
46
|
+
- fix(contradiction-checker): require predicate-position action assertion
|
|
47
|
+
- fix(spec-format): anchor single-line GIVEN/WHEN/THEN criterion regex
|
|
48
|
+
- fix(reconciliation): refresh stored title on the forward reconcile route
|
|
49
|
+
- fix(challenge-spec): stop reading a negated word as risk acceptance
|
|
50
|
+
- fix(cli): forward SDD evidence flags from spec status to the canonical mapping
|
|
51
|
+
- fix(update-status): refresh stored title from rewritten frontmatter
|
|
52
|
+
- fix(challenge): close resolution bullets on blank line
|
|
53
|
+
- fix(challenge): match resolution evidence against finding text
|
|
54
|
+
- fix(challenge): scope networkApi to a proximity-guarded bare API token
|
|
55
|
+
- fix(storage): stream transition-log reads to survive oversized lines
|
|
56
|
+
- fix(challenge): stop affirming capabilities from finding-field narration
|
|
57
|
+
- fix(update-status): discard a spec whose artifact is absent
|
|
58
|
+
- fix(validate): report unresolvable test links as unverifiable, not missing
|
|
59
|
+
- fix(update-status): drop the narrative JSDoc from the spec-artifact gate
|
|
60
|
+
- fix(update-status): refuse implementing transition on uncommitted spec.md
|
|
61
|
+
- fix(update-status): name the challenge-resolution ingress in gate blockers
|
|
62
|
+
|
|
63
|
+
### Refactoring
|
|
64
|
+
- refactor(contradiction-checker): drop rationale comments in favor of names
|
|
65
|
+
|
|
66
|
+
### Chores
|
|
67
|
+
- chore(planu): file SPEC-1624, clear integrated worktrees
|
|
68
|
+
- chore(planu): refresh session context
|
|
69
|
+
- chore(planu): refresh session context and release ledger
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
## [5.3.53] - 2026-08-25
|
|
73
|
+
|
|
74
|
+
### Bug Fixes
|
|
75
|
+
- fix(update-status): name the challenge-resolution ingress in gate blockers
|
|
76
|
+
|
|
77
|
+
### Chores
|
|
78
|
+
- chore(planu): refresh session context and release ledger
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
## [5.3.52] - 2026-08-25
|
|
82
|
+
|
|
83
|
+
### Bug Fixes
|
|
84
|
+
- fix(deps): align hono override with the upgraded dependency
|
|
85
|
+
|
|
86
|
+
### Chores
|
|
87
|
+
- chore(deps): batch patch and minor updates, hold stryker at 9.6.1
|
|
88
|
+
|
|
89
|
+
|
|
1
90
|
## [5.3.51] - 2026-08-25
|
|
2
91
|
|
|
3
92
|
### Bug Fixes
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// cli/commands/package-handoff.ts — planu package-handoff <specId> (SPEC-1621)
|
|
2
|
+
import { parseArgs } from 'node:util';
|
|
3
|
+
import { resolve } from 'node:path';
|
|
4
|
+
import { handlePackageHandoff } from '../../tools/package-handoff.js';
|
|
5
|
+
import { formatToolResult } from '../formatter.js';
|
|
6
|
+
import { detectProjectId } from '../project-detector.js';
|
|
7
|
+
import { red } from '../colors.js';
|
|
8
|
+
export const packageHandoffCommand = {
|
|
9
|
+
name: 'package-handoff',
|
|
10
|
+
description: 'Generate the operational handoff package artifact for a spec',
|
|
11
|
+
usage: 'planu package-handoff <specId> [--project-path .] [--project-id ID]',
|
|
12
|
+
async run(args, flags) {
|
|
13
|
+
const { values, positionals } = parseArgs({
|
|
14
|
+
args,
|
|
15
|
+
options: {
|
|
16
|
+
'project-id': { type: 'string' },
|
|
17
|
+
'project-path': { type: 'string' },
|
|
18
|
+
},
|
|
19
|
+
strict: false,
|
|
20
|
+
allowPositionals: true,
|
|
21
|
+
});
|
|
22
|
+
const specId = positionals[0];
|
|
23
|
+
if (!specId) {
|
|
24
|
+
process.stderr.write(`${red('Error:')} Spec ID is required.\nUsage: ${packageHandoffCommand.usage}\n`);
|
|
25
|
+
process.exitCode = 1;
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
const projectPath = values['project-path'];
|
|
29
|
+
const projectId = values['project-id'] ??
|
|
30
|
+
detectProjectId(projectPath ? resolve(projectPath) : undefined);
|
|
31
|
+
const result = await handlePackageHandoff({ projectId, specId });
|
|
32
|
+
if (result.isError) {
|
|
33
|
+
process.stderr.write(`${red(formatToolResult(result, flags))}\n`);
|
|
34
|
+
process.exitCode = 1;
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const output = formatToolResult(result, flags);
|
|
38
|
+
if (output) {
|
|
39
|
+
process.stdout.write(output + '\n');
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
//# sourceMappingURL=package-handoff.js.map
|
|
@@ -16,6 +16,7 @@ import { handleValidate } from '../../tools/validate.js';
|
|
|
16
16
|
import { formatToolResult } from '../formatter.js';
|
|
17
17
|
import { detectProjectId } from '../project-detector.js';
|
|
18
18
|
import { bold, cyan, green, red, dim } from '../colors.js';
|
|
19
|
+
import { SINGLE_STATUS_PARSE_OPTIONS, buildSingleStatusInput } from './status.js';
|
|
19
20
|
// ---------------------------------------------------------------------------
|
|
20
21
|
// Helpers
|
|
21
22
|
// ---------------------------------------------------------------------------
|
|
@@ -208,15 +209,12 @@ async function runShow(args, flags) {
|
|
|
208
209
|
// ---------------------------------------------------------------------------
|
|
209
210
|
// Subcommand: spec status
|
|
210
211
|
// ---------------------------------------------------------------------------
|
|
211
|
-
// eslint-disable-next-line complexity -- batch done requires an explicit pre-resolution fail-closed branch
|
|
212
212
|
async function runStatus(args, flags) {
|
|
213
213
|
const { positionals, values } = parseArgs({
|
|
214
214
|
args,
|
|
215
215
|
options: {
|
|
216
|
-
|
|
217
|
-
notes: { type: 'string', short: 'n' },
|
|
216
|
+
...SINGLE_STATUS_PARSE_OPTIONS,
|
|
218
217
|
batch: { type: 'boolean' },
|
|
219
|
-
set: { type: 'string', short: 's' },
|
|
220
218
|
'implementation-review-digest': { type: 'string' },
|
|
221
219
|
},
|
|
222
220
|
strict: false,
|
|
@@ -257,10 +255,7 @@ async function runStatus(args, flags) {
|
|
|
257
255
|
return;
|
|
258
256
|
}
|
|
259
257
|
const result = await handleUpdateStatus({
|
|
260
|
-
specId,
|
|
261
|
-
projectId,
|
|
262
|
-
status: status,
|
|
263
|
-
reviewNotes: values.notes ?? undefined,
|
|
258
|
+
...buildSingleStatusInput(values, specId, projectId, status),
|
|
264
259
|
implementationReviewDigest: values['implementation-review-digest'] ?? undefined,
|
|
265
260
|
});
|
|
266
261
|
if (result.isError) {
|
|
@@ -1,3 +1,54 @@
|
|
|
1
|
-
import type { CliCommand } from '../../types/cli.js';
|
|
1
|
+
import type { CliCommand, StatusCommandValues } from '../../types/cli.js';
|
|
2
|
+
import type { UpdateStatusInput } from '../../types/spec/inputs.js';
|
|
2
3
|
export declare const statusCommand: CliCommand;
|
|
4
|
+
export declare const SINGLE_STATUS_PARSE_OPTIONS: {
|
|
5
|
+
readonly set: {
|
|
6
|
+
readonly type: 'string';
|
|
7
|
+
readonly short: 's';
|
|
8
|
+
};
|
|
9
|
+
readonly 'project-id': {
|
|
10
|
+
readonly type: 'string';
|
|
11
|
+
};
|
|
12
|
+
readonly 'project-path': {
|
|
13
|
+
readonly type: 'string';
|
|
14
|
+
};
|
|
15
|
+
readonly notes: {
|
|
16
|
+
readonly type: 'string';
|
|
17
|
+
readonly short: 'n';
|
|
18
|
+
};
|
|
19
|
+
readonly 'model-id': {
|
|
20
|
+
readonly type: 'string';
|
|
21
|
+
};
|
|
22
|
+
readonly 'model-tier-used': {
|
|
23
|
+
readonly type: 'string';
|
|
24
|
+
};
|
|
25
|
+
readonly 'context-hash': {
|
|
26
|
+
readonly type: 'string';
|
|
27
|
+
};
|
|
28
|
+
readonly 'handoff-path': {
|
|
29
|
+
readonly type: 'string';
|
|
30
|
+
};
|
|
31
|
+
readonly 'handoff-artifact-id': {
|
|
32
|
+
readonly type: 'string';
|
|
33
|
+
};
|
|
34
|
+
readonly 'reviewed-by': {
|
|
35
|
+
readonly type: 'string';
|
|
36
|
+
};
|
|
37
|
+
readonly 'arbitrated-by': {
|
|
38
|
+
readonly type: 'string';
|
|
39
|
+
};
|
|
40
|
+
readonly reason: {
|
|
41
|
+
readonly type: 'string';
|
|
42
|
+
};
|
|
43
|
+
readonly force: {
|
|
44
|
+
readonly type: 'boolean';
|
|
45
|
+
};
|
|
46
|
+
readonly 'force-status': {
|
|
47
|
+
readonly type: 'boolean';
|
|
48
|
+
};
|
|
49
|
+
readonly 'force-status-reason': {
|
|
50
|
+
readonly type: 'string';
|
|
51
|
+
};
|
|
52
|
+
};
|
|
53
|
+
export declare function buildSingleStatusInput(values: StatusCommandValues, specId: string, projectId: string, status: string): UpdateStatusInput;
|
|
3
54
|
//# sourceMappingURL=status.d.ts.map
|
|
@@ -37,26 +37,47 @@ export const statusCommand = {
|
|
|
37
37
|
await runSingleStatus(specId, values, status, flags);
|
|
38
38
|
},
|
|
39
39
|
};
|
|
40
|
+
export const SINGLE_STATUS_PARSE_OPTIONS = {
|
|
41
|
+
set: { type: 'string', short: 's' },
|
|
42
|
+
'project-id': { type: 'string' },
|
|
43
|
+
'project-path': { type: 'string' },
|
|
44
|
+
notes: { type: 'string', short: 'n' },
|
|
45
|
+
'model-id': { type: 'string' },
|
|
46
|
+
'model-tier-used': { type: 'string' },
|
|
47
|
+
'context-hash': { type: 'string' },
|
|
48
|
+
'handoff-path': { type: 'string' },
|
|
49
|
+
'handoff-artifact-id': { type: 'string' },
|
|
50
|
+
'reviewed-by': { type: 'string' },
|
|
51
|
+
'arbitrated-by': { type: 'string' },
|
|
52
|
+
reason: { type: 'string' },
|
|
53
|
+
force: { type: 'boolean' },
|
|
54
|
+
'force-status': { type: 'boolean' },
|
|
55
|
+
'force-status-reason': { type: 'string' },
|
|
56
|
+
};
|
|
57
|
+
export function buildSingleStatusInput(values, specId, projectId, status) {
|
|
58
|
+
return {
|
|
59
|
+
specId,
|
|
60
|
+
projectId,
|
|
61
|
+
projectPath: values['project-path'] ? resolve(values['project-path']) : undefined,
|
|
62
|
+
status: status,
|
|
63
|
+
reviewNotes: values.notes,
|
|
64
|
+
modelId: values['model-id'],
|
|
65
|
+
modelTierUsed: values['model-tier-used'],
|
|
66
|
+
contextHash: values['context-hash'],
|
|
67
|
+
handoffPath: values['handoff-path'],
|
|
68
|
+
handoffArtifactId: values['handoff-artifact-id'],
|
|
69
|
+
reviewedBy: values['reviewed-by'],
|
|
70
|
+
arbitratedBy: values['arbitrated-by'],
|
|
71
|
+
reason: values.reason,
|
|
72
|
+
force: values.force === true ? true : undefined,
|
|
73
|
+
forceStatus: values['force-status'] === true ? true : undefined,
|
|
74
|
+
forceStatusReason: values['force-status-reason'],
|
|
75
|
+
};
|
|
76
|
+
}
|
|
40
77
|
function parseStatusArgs(args) {
|
|
41
78
|
const parsed = parseArgs({
|
|
42
79
|
args,
|
|
43
|
-
options:
|
|
44
|
-
set: { type: 'string', short: 's' },
|
|
45
|
-
'project-id': { type: 'string' },
|
|
46
|
-
'project-path': { type: 'string' },
|
|
47
|
-
notes: { type: 'string', short: 'n' },
|
|
48
|
-
'model-id': { type: 'string' },
|
|
49
|
-
'model-tier-used': { type: 'string' },
|
|
50
|
-
'context-hash': { type: 'string' },
|
|
51
|
-
'handoff-path': { type: 'string' },
|
|
52
|
-
'handoff-artifact-id': { type: 'string' },
|
|
53
|
-
'reviewed-by': { type: 'string' },
|
|
54
|
-
'arbitrated-by': { type: 'string' },
|
|
55
|
-
reason: { type: 'string' },
|
|
56
|
-
force: { type: 'boolean' },
|
|
57
|
-
'force-status': { type: 'boolean' },
|
|
58
|
-
'force-status-reason': { type: 'string' },
|
|
59
|
-
},
|
|
80
|
+
options: SINGLE_STATUS_PARSE_OPTIONS,
|
|
60
81
|
strict: false,
|
|
61
82
|
allowPositionals: true,
|
|
62
83
|
});
|
|
@@ -94,24 +115,7 @@ async function runBatchStatus(specIds, values, status, flags) {
|
|
|
94
115
|
writeToolResult(result, flags, false);
|
|
95
116
|
}
|
|
96
117
|
async function runSingleStatus(specId, values, status, flags) {
|
|
97
|
-
const result = await handleUpdateStatus(
|
|
98
|
-
specId,
|
|
99
|
-
projectId: values['project-id'] ?? detectProjectId(),
|
|
100
|
-
projectPath: values['project-path'],
|
|
101
|
-
status: status,
|
|
102
|
-
reviewNotes: values.notes,
|
|
103
|
-
modelId: values['model-id'],
|
|
104
|
-
modelTierUsed: values['model-tier-used'],
|
|
105
|
-
contextHash: values['context-hash'],
|
|
106
|
-
handoffPath: values['handoff-path'],
|
|
107
|
-
handoffArtifactId: values['handoff-artifact-id'],
|
|
108
|
-
reviewedBy: values['reviewed-by'],
|
|
109
|
-
arbitratedBy: values['arbitrated-by'],
|
|
110
|
-
reason: values.reason,
|
|
111
|
-
force: values.force === true ? true : undefined,
|
|
112
|
-
forceStatus: values['force-status'] === true ? true : undefined,
|
|
113
|
-
forceStatusReason: values['force-status-reason'],
|
|
114
|
-
});
|
|
118
|
+
const result = await handleUpdateStatus(buildSingleStatusInput(values, specId, values['project-id'] ?? detectProjectId(), status));
|
|
115
119
|
writeToolResult(result, flags, true);
|
|
116
120
|
}
|
|
117
121
|
function writeToolResult(result, flags, includeSuccessPrefix) {
|
package/dist/cli/router.js
CHANGED
|
@@ -4,7 +4,7 @@ import { PLANU_VERSION } from '../config/version.js';
|
|
|
4
4
|
import { red, bold, cyan, dim } from './colors.js';
|
|
5
5
|
// Lazy-load commands to avoid importing tool handlers when not needed
|
|
6
6
|
async function loadCommands() {
|
|
7
|
-
const [{ initCommand }, { createCommand }, { listCommand }, { statusCommand }, { estimateCommand }, { validateCommand }, { dashboardCommand }, { auditCommand }, { serveCommand }, { handoffCommand }, { watchCommand }, { installCommand }, { doctorCommand }, { uninstallCommand }, { specCommand }, { healthCommand }, { releaseCommand }, { storageCommand }, { inspectCommand }, { offlineCommand }, { jobCommand },] = await Promise.all([
|
|
7
|
+
const [{ initCommand }, { createCommand }, { listCommand }, { statusCommand }, { estimateCommand }, { validateCommand }, { dashboardCommand }, { auditCommand }, { serveCommand }, { handoffCommand }, { packageHandoffCommand }, { watchCommand }, { installCommand }, { doctorCommand }, { uninstallCommand }, { specCommand }, { healthCommand }, { releaseCommand }, { storageCommand }, { inspectCommand }, { offlineCommand }, { jobCommand },] = await Promise.all([
|
|
8
8
|
import('./commands/init.js'),
|
|
9
9
|
import('./commands/create.js'),
|
|
10
10
|
import('./commands/list.js'),
|
|
@@ -15,6 +15,7 @@ async function loadCommands() {
|
|
|
15
15
|
import('./commands/audit.js'),
|
|
16
16
|
import('./commands/serve.js'),
|
|
17
17
|
import('./commands/handoff.js'),
|
|
18
|
+
import('./commands/package-handoff.js'),
|
|
18
19
|
import('./commands/watch.js'),
|
|
19
20
|
import('./commands/install.js'),
|
|
20
21
|
import('./commands/doctor.js'),
|
|
@@ -38,6 +39,7 @@ async function loadCommands() {
|
|
|
38
39
|
auditCommand,
|
|
39
40
|
serveCommand,
|
|
40
41
|
handoffCommand,
|
|
42
|
+
packageHandoffCommand,
|
|
41
43
|
watchCommand,
|
|
42
44
|
installCommand,
|
|
43
45
|
doctorCommand,
|
|
@@ -64,8 +66,9 @@ function printHelp(commands) {
|
|
|
64
66
|
'',
|
|
65
67
|
cyan('Commands:'),
|
|
66
68
|
];
|
|
69
|
+
const nameColumnWidth = Math.max(...[...commands.values()].map((cmd) => cmd.name.length));
|
|
67
70
|
for (const cmd of commands.values()) {
|
|
68
|
-
lines.push(` ${cmd.name.padEnd(
|
|
71
|
+
lines.push(` ${cmd.name.padEnd(nameColumnWidth)} ${dim(cmd.description)}`);
|
|
69
72
|
}
|
|
70
73
|
lines.push('');
|
|
71
74
|
lines.push(cyan('Global options:'));
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process';
|
|
2
|
+
function runCmd(cmd, cwd) {
|
|
3
|
+
try {
|
|
4
|
+
const output = execSync(cmd, {
|
|
5
|
+
cwd,
|
|
6
|
+
encoding: 'utf-8',
|
|
7
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
8
|
+
}).trim();
|
|
9
|
+
return { output, exitCode: 0 };
|
|
10
|
+
}
|
|
11
|
+
catch (err) {
|
|
12
|
+
const error = err;
|
|
13
|
+
return {
|
|
14
|
+
output: (error.stdout ?? '') + (error.stderr ?? ''),
|
|
15
|
+
exitCode: error.status ?? 1,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export function resolveCanonicalBranch(cwd) {
|
|
20
|
+
const originHead = runCmd('git symbolic-ref --short refs/remotes/origin/HEAD', cwd);
|
|
21
|
+
if (originHead.exitCode === 0) {
|
|
22
|
+
const branch = originHead.output.replace(/^origin\//, '');
|
|
23
|
+
const verified = runCmd(`git rev-parse --verify ${branch}`, cwd);
|
|
24
|
+
if (verified.exitCode === 0) {
|
|
25
|
+
return branch;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
const survivors = ['main', 'master', 'develop'].filter((branch) => runCmd(`git rev-parse --verify ${branch}`, cwd).exitCode === 0);
|
|
29
|
+
return survivors.length === 1 ? (survivors[0] ?? '') : '';
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=canonical-branch.js.map
|
|
@@ -119,6 +119,7 @@ export declare const ValidationReportV1Schema: z.ZodObject<{
|
|
|
119
119
|
pass: "pass";
|
|
120
120
|
}>;
|
|
121
121
|
evidence: z.ZodArray<z.ZodString>;
|
|
122
|
+
unverifiable: z.ZodOptional<z.ZodLiteral<true>>;
|
|
122
123
|
}, z.core.$strip>>;
|
|
123
124
|
}, z.core.$strip>>;
|
|
124
125
|
minimalityReport: z.ZodOptional<z.ZodObject<{
|
|
@@ -276,6 +277,7 @@ export declare const ARTIFACT_SCHEMAS: {
|
|
|
276
277
|
pass: "pass";
|
|
277
278
|
}>;
|
|
278
279
|
evidence: z.ZodArray<z.ZodString>;
|
|
280
|
+
unverifiable: z.ZodOptional<z.ZodLiteral<true>>;
|
|
279
281
|
}, z.core.$strip>>;
|
|
280
282
|
}, z.core.$strip>>;
|
|
281
283
|
minimalityReport: z.ZodOptional<z.ZodObject<{
|
|
@@ -46,7 +46,7 @@ export function buildCheckReadinessSummary(score, blockerCount) {
|
|
|
46
46
|
export function buildChallengeSpecSummary(blockingScenarios, gateStatus, advisoryCount) {
|
|
47
47
|
const list = describeBlockingScenarios(blockingScenarios);
|
|
48
48
|
if (gateStatus === 'blocked-critical') {
|
|
49
|
-
return `BLOCKED — ${String(blockingScenarios.length)} unresolved critical finding(s) must be resolved before this passes the challenge gate: ${list}. Record
|
|
49
|
+
return `BLOCKED — ${String(blockingScenarios.length)} unresolved critical finding(s) must be resolved before this passes the challenge gate: ${list}. Record resolution evidence, then run challenge_spec again.`;
|
|
50
50
|
}
|
|
51
51
|
if (gateStatus === 'blocked-evidence' || gateStatus === 'refresh-required') {
|
|
52
52
|
if (gateStatus === 'refresh-required') {
|
|
@@ -72,6 +72,9 @@ async function completeForwardReconciliation(args) {
|
|
|
72
72
|
if (syncResult.warning) {
|
|
73
73
|
throw new Error(`portable spec sync failed: ${syncResult.warning.reason}`);
|
|
74
74
|
}
|
|
75
|
+
if (syncResult.title) {
|
|
76
|
+
await specStore.updateSpec(args.projectId, args.spec.id, { title: syncResult.title });
|
|
77
|
+
}
|
|
75
78
|
next = { ...next, transitionId: args.transitionId, phase: 'spec-synced', auditPending: true };
|
|
76
79
|
await args.persistReceipt(args.receiptPath, next);
|
|
77
80
|
}
|
|
@@ -101,6 +101,26 @@ function splitClauses(text) {
|
|
|
101
101
|
.filter(Boolean);
|
|
102
102
|
}
|
|
103
103
|
const AFFIRMATIVE_ACTION_VERBS = /\b(?:build(?:s|ing)?|chang(?:e|es|ing)|modif(?:y|ies|ying)|alter(?:s|ing)?|add(?:s|ing)?|introduc(?:e|es|ing)|implement(?:s|ing)?|enabl(?:e|es|ing)|expos(?:e|es|ing|ed)|mutat(?:e|es|ing)|rewrit(?:e|es|ing|ten)|remov(?:e|es|ing|ed)|relax(?:es|ing|ed)?|forc(?:e|es|ing)|suppress(?:es|ing|ed)?|weaken(?:s|ing|ed)?|degrad(?:e|es|ing|ed)|loosen(?:s|ing|ed)?|undermin(?:e|es|ing|ed)|bypass(?:es|ing|ed)?|disabl(?:e|es|ing|ed)|circumvent(?:s|ing|ed)?)\b/;
|
|
104
|
+
const DETERMINERS_AND_PREPOSITIONS_HEADING_A_NOUN_PHRASE = new Set([
|
|
105
|
+
'a',
|
|
106
|
+
'an',
|
|
107
|
+
'the',
|
|
108
|
+
'this',
|
|
109
|
+
'these',
|
|
110
|
+
'those',
|
|
111
|
+
'in',
|
|
112
|
+
'on',
|
|
113
|
+
'at',
|
|
114
|
+
'of',
|
|
115
|
+
'into',
|
|
116
|
+
'onto',
|
|
117
|
+
'within',
|
|
118
|
+
'per',
|
|
119
|
+
]);
|
|
120
|
+
const QUOTED_OR_BACKTICKED_SPAN_RE = /`[^`\n]+`|"[^"\n]+"|'[^'\n]+'/g;
|
|
121
|
+
function stripQuotedSpans(text) {
|
|
122
|
+
return text.replace(QUOTED_OR_BACKTICKED_SPAN_RE, ' ');
|
|
123
|
+
}
|
|
104
124
|
function assertsForbiddenAction(text) {
|
|
105
125
|
return AFFIRMATIVE_ACTION_VERBS.test(normalize(text));
|
|
106
126
|
}
|
|
@@ -157,7 +177,7 @@ function clauseMentionsScope(clause, outOfScopeItem) {
|
|
|
157
177
|
*/
|
|
158
178
|
const ACTION_SCOPE_WINDOW = 4;
|
|
159
179
|
function assertsActionNearScope(clause, outOfScopeItem) {
|
|
160
|
-
const tokens = normalize(clause).split(' ');
|
|
180
|
+
const tokens = normalize(stripQuotedSpans(clause)).split(' ');
|
|
161
181
|
const scopeKeywords = keywords(outOfScopeItem);
|
|
162
182
|
const mentionCandidates = scopeKeywords.length > 0 ? scopeKeywords : [normalize(outOfScopeItem)];
|
|
163
183
|
for (let i = 0; i < tokens.length; i++) {
|
|
@@ -165,6 +185,11 @@ function assertsActionNearScope(clause, outOfScopeItem) {
|
|
|
165
185
|
if (!AFFIRMATIVE_ACTION_VERBS.test(token)) {
|
|
166
186
|
continue;
|
|
167
187
|
}
|
|
188
|
+
const preceder = i > 0 ? tokens[i - 1] : undefined;
|
|
189
|
+
if (preceder !== undefined &&
|
|
190
|
+
DETERMINERS_AND_PREPOSITIONS_HEADING_A_NOUN_PHRASE.has(preceder)) {
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
168
193
|
const before = tokens.slice(Math.max(0, i - 6), i).join(' ');
|
|
169
194
|
if (endsWithNegation(before)) {
|
|
170
195
|
continue;
|
|
@@ -201,10 +226,9 @@ function contradicts(criterionText, outOfScopeItem) {
|
|
|
201
226
|
}
|
|
202
227
|
const scopeAssertsForbiddenAction = assertsForbiddenAction(outOfScopeItem);
|
|
203
228
|
return clauses.some((clause) => clauseMatchesScope(clause, outOfScopeItem) &&
|
|
204
|
-
(
|
|
205
|
-
(
|
|
206
|
-
|
|
207
|
-
: assertsActionNearScope(clause, outOfScopeItem))));
|
|
229
|
+
(hasConditionalException && scopeAssertsForbiddenAction
|
|
230
|
+
? assertsForbiddenAction(clause)
|
|
231
|
+
: assertsActionNearScope(clause, outOfScopeItem)));
|
|
208
232
|
}
|
|
209
233
|
/**
|
|
210
234
|
* Check acceptance criteria against the spec's outOfScope declarations.
|
|
@@ -121,8 +121,13 @@ async function contextCanBeReconstructed(params) {
|
|
|
121
121
|
blockers.push('handoffPath exists but is too small to be an operational package.');
|
|
122
122
|
}
|
|
123
123
|
}
|
|
124
|
-
catch {
|
|
125
|
-
|
|
124
|
+
catch (error) {
|
|
125
|
+
if (error.code === 'EISDIR') {
|
|
126
|
+
blockers.push('handoffPath is a directory, not the handoff package file.');
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
blockers.push('handoffPath is missing or unreadable.');
|
|
130
|
+
}
|
|
126
131
|
}
|
|
127
132
|
}
|
|
128
133
|
else if (!params.handoffArtifactId) {
|
|
@@ -166,7 +166,7 @@ function extractCheckboxCriteria(description) {
|
|
|
166
166
|
function extractGivenWhenThenCriteria(description) {
|
|
167
167
|
const criteria = [];
|
|
168
168
|
// Single-line: GIVEN ... WHEN ... THEN ...
|
|
169
|
-
const singleLineRegex =
|
|
169
|
+
const singleLineRegex = /^[ \t]*(?:[-*>]\s*)?GIVEN\s+.+\s+WHEN\s+.+\s+THEN\s+(.+)$/gim;
|
|
170
170
|
let match = singleLineRegex.exec(description);
|
|
171
171
|
while (match) {
|
|
172
172
|
const thenText = match[1]?.trim() ?? '';
|
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
//
|
|
3
3
|
// Finds specs in 'implementing' status that have been there for >N days and have
|
|
4
4
|
// no progress.md file. These are candidates for "done but untransitioned".
|
|
5
|
-
import {
|
|
5
|
+
import { access, readdir } from 'node:fs/promises';
|
|
6
6
|
import { join } from 'node:path';
|
|
7
7
|
import { constants } from 'node:fs';
|
|
8
|
-
import { hashProjectPath
|
|
8
|
+
import { hashProjectPath } from '../../storage/base-store.js';
|
|
9
|
+
import { readTransitionLog } from '../../storage/transition-log.js';
|
|
9
10
|
// ---------------------------------------------------------------------------
|
|
10
11
|
// Helpers
|
|
11
12
|
// ---------------------------------------------------------------------------
|
|
@@ -19,69 +20,29 @@ async function fileExists(p) {
|
|
|
19
20
|
}
|
|
20
21
|
}
|
|
21
22
|
async function getLastTransitionForSpec(projectId, specId) {
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
for (const line of lines) {
|
|
28
|
-
try {
|
|
29
|
-
const entry = JSON.parse(line);
|
|
30
|
-
if (typeof entry === 'object' &&
|
|
31
|
-
entry !== null &&
|
|
32
|
-
'specId' in entry &&
|
|
33
|
-
entry.specId === specId) {
|
|
34
|
-
const at = entry.timestamp;
|
|
35
|
-
if (typeof at === 'string') {
|
|
36
|
-
lastAt = at;
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
catch {
|
|
41
|
-
// Skip malformed lines
|
|
42
|
-
}
|
|
23
|
+
const entries = await readTransitionLog(projectId, specId);
|
|
24
|
+
let lastAt = null;
|
|
25
|
+
for (const entry of entries) {
|
|
26
|
+
if (typeof entry.timestamp === 'string') {
|
|
27
|
+
lastAt = entry.timestamp;
|
|
43
28
|
}
|
|
44
|
-
return lastAt;
|
|
45
|
-
}
|
|
46
|
-
catch {
|
|
47
|
-
return null;
|
|
48
29
|
}
|
|
30
|
+
return lastAt;
|
|
49
31
|
}
|
|
50
32
|
async function getDaysInImplementing(projectId, specId) {
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
for (const line of lines) {
|
|
57
|
-
try {
|
|
58
|
-
const entry = JSON.parse(line);
|
|
59
|
-
if (typeof entry === 'object' &&
|
|
60
|
-
entry !== null &&
|
|
61
|
-
'specId' in entry &&
|
|
62
|
-
'to' in entry &&
|
|
63
|
-
entry.specId === specId &&
|
|
64
|
-
entry.to === 'implementing') {
|
|
65
|
-
const at = entry.timestamp;
|
|
66
|
-
if (typeof at === 'string') {
|
|
67
|
-
implementingAt = at;
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
catch {
|
|
72
|
-
// Skip
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
if (implementingAt) {
|
|
76
|
-
const then = new Date(implementingAt).getTime();
|
|
77
|
-
const now = Date.now();
|
|
78
|
-
return Math.floor((now - then) / (1000 * 60 * 60 * 24));
|
|
33
|
+
const entries = await readTransitionLog(projectId, specId);
|
|
34
|
+
let implementingAt = null;
|
|
35
|
+
for (const entry of entries) {
|
|
36
|
+
if (entry.to === 'implementing' && typeof entry.timestamp === 'string') {
|
|
37
|
+
implementingAt = entry.timestamp;
|
|
79
38
|
}
|
|
80
39
|
}
|
|
81
|
-
|
|
82
|
-
|
|
40
|
+
if (!implementingAt) {
|
|
41
|
+
return -1;
|
|
83
42
|
}
|
|
84
|
-
|
|
43
|
+
const then = new Date(implementingAt).getTime();
|
|
44
|
+
const now = Date.now();
|
|
45
|
+
return Math.floor((now - then) / (1000 * 60 * 60 * 24));
|
|
85
46
|
}
|
|
86
47
|
// ---------------------------------------------------------------------------
|
|
87
48
|
// Public API
|
|
@@ -26,6 +26,7 @@ const QUALIFIED_EVIDENCE_SUBJECT_RE = /^\s*(?:[-*+>]\s+|\d+[.)]\s+)?(?:(?:given|
|
|
|
26
26
|
const CONTRACT_ANALYSIS_SUBJECT_RE = /^\s*(?:[-*+>]\s+|\d+[.)]\s+)?(?:(?:given|when|then|and)\s+)?(?:(?:the|this|that|a|an|any|each|our|existing|current)\s+)?(?:[\p{L}\p{N}_-]+\s+){0,2}contract\s+(?:evidence|inference|requirements?)\b/iu;
|
|
27
27
|
const CONTRACT_ANALYSIS_PREDICATE_RE = /\b(?:(?:is|are)\s+(?:inferred|expected|selected|classified|matched|detected|discussed|reported|recorded)|expects?|records?|describes?)\b|\b(?:detector|classifier|matcher)\s+evidence\b|\b(?:positive[ -]?control|expected\s+absence|meta-analysis(?:-only)?)\b/iu;
|
|
28
28
|
const META_ANALYSIS_TOOL_OUTPUT_RE = /\b(?:challenge[_\s]spec|challenge\s+tool)\b[^.!?\n]{0,120}?\bcall\b[^.!?\n]{0,120}?\b(?:emitted|returned|produced|reported|surfaced)\b[^.!?\n]{0,120}?\bscenarios?\b/iu;
|
|
29
|
+
const CHALLENGE_FINDING_FIELD_RE = /\b(?:requiredHandling|currentHandling|dataConsistency|userExperience|relevanceScore)\b/u;
|
|
29
30
|
function stripExcludedMarkdownSections(text) {
|
|
30
31
|
const lines = text.split('\n');
|
|
31
32
|
const kept = [];
|
|
@@ -73,7 +74,8 @@ export function stripMetaAnalysisText(text) {
|
|
|
73
74
|
!EXPECTED_META_ANALYSIS_RE.test(clause) &&
|
|
74
75
|
!QUALIFIED_EVIDENCE_SUBJECT_RE.test(clause) &&
|
|
75
76
|
!(CONTRACT_ANALYSIS_SUBJECT_RE.test(clause) && CONTRACT_ANALYSIS_PREDICATE_RE.test(clause)) &&
|
|
76
|
-
!META_ANALYSIS_TOOL_OUTPUT_RE.test(clause)
|
|
77
|
+
!META_ANALYSIS_TOOL_OUTPUT_RE.test(clause) &&
|
|
78
|
+
!CHALLENGE_FINDING_FIELD_RE.test(clause))
|
|
77
79
|
.join('\n');
|
|
78
80
|
}
|
|
79
81
|
function clauseBefore(text, index) {
|