gsdd-cli 0.3.1 → 0.18.0
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 +131 -67
- package/agents/DISTILLATION.md +15 -13
- package/agents/README.md +1 -1
- package/agents/planner.md +2 -0
- package/bin/adapters/agents.mjs +1 -0
- package/bin/adapters/claude.mjs +20 -4
- package/bin/adapters/codex.mjs +9 -1
- package/bin/adapters/opencode.mjs +20 -5
- package/bin/gsdd.mjs +24 -7
- package/bin/lib/cli-utils.mjs +1 -1
- package/bin/lib/evidence-contract.mjs +112 -0
- package/bin/lib/file-ops.mjs +161 -0
- package/bin/lib/health-truth.mjs +186 -0
- package/bin/lib/health.mjs +72 -67
- package/bin/lib/init-flow.mjs +50 -3
- package/bin/lib/init-prompts.mjs +22 -83
- package/bin/lib/init-runtime.mjs +47 -25
- package/bin/lib/init.mjs +3 -3
- package/bin/lib/lifecycle-preflight.mjs +333 -0
- package/bin/lib/lifecycle-state.mjs +293 -0
- package/bin/lib/models.mjs +19 -4
- package/bin/lib/phase.mjs +159 -18
- package/bin/lib/plan-constants.mjs +30 -0
- package/bin/lib/provenance.mjs +165 -0
- package/bin/lib/rendering.mjs +8 -0
- package/bin/lib/runtime-freshness.mjs +239 -0
- package/bin/lib/session-fingerprint.mjs +106 -0
- package/bin/lib/templates.mjs +17 -0
- package/distilled/DESIGN.md +733 -49
- package/distilled/EVIDENCE-INDEX.md +402 -0
- package/distilled/README.md +73 -33
- package/distilled/SKILL.md +89 -85
- package/distilled/templates/agents.block.md +13 -84
- package/distilled/templates/agents.md +0 -7
- package/distilled/templates/delegates/plan-checker.md +6 -3
- package/distilled/workflows/audit-milestone.md +56 -6
- package/distilled/workflows/complete-milestone.md +333 -0
- package/distilled/workflows/execute.md +201 -19
- package/distilled/workflows/map-codebase.md +17 -4
- package/distilled/workflows/new-milestone.md +262 -0
- package/distilled/workflows/new-project.md +7 -6
- package/distilled/workflows/pause.md +40 -6
- package/distilled/workflows/plan-milestone-gaps.md +183 -0
- package/distilled/workflows/plan.md +77 -11
- package/distilled/workflows/progress.md +107 -29
- package/distilled/workflows/quick.md +23 -12
- package/distilled/workflows/resume.md +135 -12
- package/distilled/workflows/verify-work.md +260 -0
- package/distilled/workflows/verify.md +159 -33
- package/docs/BROWNFIELD-PROOF.md +95 -0
- package/docs/RUNTIME-SUPPORT.md +77 -0
- package/docs/USER-GUIDE.md +439 -0
- package/docs/VERIFICATION-DISCIPLINE.md +59 -0
- package/docs/claude/context-monitor.md +98 -0
- package/docs/proof/consumer-node-cli/README.md +37 -0
- package/docs/proof/consumer-node-cli/ROADMAP.md +14 -0
- package/docs/proof/consumer-node-cli/SPEC.md +17 -0
- package/docs/proof/consumer-node-cli/brief.md +9 -0
- package/docs/proof/consumer-node-cli/phases/01-foundation/01-01-PLAN.md +34 -0
- package/docs/proof/consumer-node-cli/phases/01-foundation/01-01-SUMMARY.md +10 -0
- package/docs/proof/consumer-node-cli/phases/01-foundation/01-VERIFICATION.md +30 -0
- package/package.json +38 -29
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
|
|
2
2
|
import os from 'os';
|
|
3
3
|
import { join } from 'path';
|
|
4
|
+
import {
|
|
5
|
+
PLAN_CHECK_DIMENSIONS,
|
|
6
|
+
MAX_CHECKER_CYCLES,
|
|
7
|
+
CHECKER_STATUSES,
|
|
8
|
+
} from '../lib/plan-constants.mjs';
|
|
4
9
|
|
|
5
10
|
function expandHome(filePath) {
|
|
6
11
|
if (!filePath) return filePath;
|
|
@@ -115,7 +120,7 @@ function renderOpenCodeApproachExplorer(delegateContent, modelId = null) {
|
|
|
115
120
|
const modelLine = modelId ? `model: ${modelId}\n` : '';
|
|
116
121
|
return `---
|
|
117
122
|
description: Explores implementation approaches for a phase and aligns with the user through structured questioning before planning begins.
|
|
118
|
-
mode:
|
|
123
|
+
mode: subagent
|
|
119
124
|
${modelLine}tools:
|
|
120
125
|
bash: false
|
|
121
126
|
---
|
|
@@ -183,7 +188,7 @@ Execution flow:
|
|
|
183
188
|
"summary": "One sentence overall assessment",
|
|
184
189
|
"issues": [
|
|
185
190
|
{
|
|
186
|
-
"dimension": "
|
|
191
|
+
"dimension": "${PLAN_CHECK_DIMENSIONS.join(' | ')}",
|
|
187
192
|
"severity": "blocker | warning",
|
|
188
193
|
"description": "What is wrong",
|
|
189
194
|
"plan": "01-PLAN",
|
|
@@ -192,10 +197,10 @@ Execution flow:
|
|
|
192
197
|
}
|
|
193
198
|
]
|
|
194
199
|
}
|
|
195
|
-
Status must be either "
|
|
200
|
+
Status must be either "${CHECKER_STATUSES[0]}" or "${CHECKER_STATUSES[1]}".
|
|
196
201
|
9. If the checker returns \`passed\`, finish and summarize.
|
|
197
202
|
10. If the checker returns \`issues_found\`, revise the existing plan files only where needed, then run the checker again.
|
|
198
|
-
11. Maximum
|
|
203
|
+
11. Maximum ${MAX_CHECKER_CYCLES} checker cycles total. If blockers remain after cycle ${MAX_CHECKER_CYCLES}, stop and escalate to the user instead of pretending the plan is ready.
|
|
199
204
|
|
|
200
205
|
Return a concise orchestration summary:
|
|
201
206
|
- target phase
|
|
@@ -223,6 +228,10 @@ function createOpenCodeAdapter({
|
|
|
223
228
|
id: 'opencode',
|
|
224
229
|
name: 'opencode',
|
|
225
230
|
kind: 'native_capable',
|
|
231
|
+
subagentFiles: [
|
|
232
|
+
'.opencode/agents/gsdd-plan-checker.md',
|
|
233
|
+
'.opencode/agents/gsdd-approach-explorer.md',
|
|
234
|
+
],
|
|
226
235
|
detect() {
|
|
227
236
|
return existsSync(join(cwd, '.opencode'));
|
|
228
237
|
},
|
|
@@ -260,4 +269,10 @@ function createOpenCodeAdapter({
|
|
|
260
269
|
};
|
|
261
270
|
}
|
|
262
271
|
|
|
263
|
-
export {
|
|
272
|
+
export {
|
|
273
|
+
createOpenCodeAdapter,
|
|
274
|
+
detectOpenCodeConfiguredModel,
|
|
275
|
+
renderOpenCodeApproachExplorer,
|
|
276
|
+
renderOpenCodePlanChecker,
|
|
277
|
+
renderOpenCodePlanCommand,
|
|
278
|
+
};
|
package/bin/gsdd.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
// gsdd -
|
|
3
|
+
// gsdd - Workspine CLI
|
|
4
4
|
|
|
5
5
|
import { realpathSync } from 'fs';
|
|
6
6
|
import { join, dirname } from 'path';
|
|
@@ -16,8 +16,10 @@ import {
|
|
|
16
16
|
} from './lib/rendering.mjs';
|
|
17
17
|
import { loadProjectModelConfig, getRuntimeModelOverride, resolveRuntimeAgentModel, cmdModels } from './lib/models.mjs';
|
|
18
18
|
import { createCmdInit, createCmdUpdate, cmdHelp } from './lib/init.mjs';
|
|
19
|
-
import { cmdFindPhase, cmdVerify, cmdScaffold } from './lib/phase.mjs';
|
|
19
|
+
import { cmdFindPhase, cmdVerify, cmdScaffold, cmdPhaseStatus } from './lib/phase.mjs';
|
|
20
|
+
import { cmdFileOp } from './lib/file-ops.mjs';
|
|
20
21
|
import { createCmdHealth } from './lib/health.mjs';
|
|
22
|
+
import { cmdLifecyclePreflight } from './lib/lifecycle-preflight.mjs';
|
|
21
23
|
|
|
22
24
|
const __filename = fileURLToPath(import.meta.url);
|
|
23
25
|
const __dirname = dirname(__filename);
|
|
@@ -45,14 +47,18 @@ const WORKFLOWS = [
|
|
|
45
47
|
defineWorkflow({ name: 'gsdd-plan', workflow: 'plan.md', description: 'Plan a phase - research check, backward planning, task creation' }),
|
|
46
48
|
defineWorkflow({ name: 'gsdd-execute', workflow: 'execute.md', description: 'Execute a phase plan - implement tasks, verify changes, follow repo git conventions' }),
|
|
47
49
|
defineWorkflow({ name: 'gsdd-verify', workflow: 'verify.md', description: 'Verify a completed phase - 3-level checks, anti-pattern scan' }),
|
|
50
|
+
defineWorkflow({ name: 'gsdd-verify-work', workflow: 'verify-work.md', description: 'Conversational UAT testing - validate user-facing behavior with structured gap tracking' }),
|
|
48
51
|
defineWorkflow({ name: 'gsdd-audit-milestone', workflow: 'audit-milestone.md', description: 'Audit a completed milestone - cross-phase integration, requirements coverage, E2E flows' }),
|
|
52
|
+
defineWorkflow({ name: 'gsdd-complete-milestone', workflow: 'complete-milestone.md', description: 'Complete milestone - archive, evolve spec, collapse roadmap' }),
|
|
53
|
+
defineWorkflow({ name: 'gsdd-new-milestone', workflow: 'new-milestone.md', description: 'New milestone - gather goals, define requirements, create roadmap phases' }),
|
|
54
|
+
defineWorkflow({ name: 'gsdd-plan-milestone-gaps', workflow: 'plan-milestone-gaps.md', description: 'Plan gap closure phases from audit results' }),
|
|
49
55
|
defineWorkflow({ name: 'gsdd-quick', workflow: 'quick.md', description: 'Quick task - plan and execute a sub-hour task outside the phase cycle' }),
|
|
50
56
|
defineWorkflow({ name: 'gsdd-pause', workflow: 'pause.md', description: 'Pause work - save session context for seamless resumption' }),
|
|
51
57
|
defineWorkflow({ name: 'gsdd-resume', workflow: 'resume.md', description: 'Resume work - restore context and route to next action' }),
|
|
52
58
|
defineWorkflow({ name: 'gsdd-progress', workflow: 'progress.md', description: 'Check progress - show project status and route to next action', mutatesArtifacts: false }),
|
|
53
59
|
];
|
|
54
60
|
|
|
55
|
-
const FRAMEWORK_VERSION = 'v1.
|
|
61
|
+
const FRAMEWORK_VERSION = 'v1.4';
|
|
56
62
|
|
|
57
63
|
function createCliContext(cwd = process.cwd()) {
|
|
58
64
|
return {
|
|
@@ -82,31 +88,42 @@ const INIT_CONTEXT = createCliContext(CWD);
|
|
|
82
88
|
|
|
83
89
|
const cmdInit = createCmdInit(INIT_CONTEXT);
|
|
84
90
|
const cmdUpdate = createCmdUpdate(INIT_CONTEXT);
|
|
85
|
-
const cmdHealth = createCmdHealth(
|
|
91
|
+
const cmdHealth = createCmdHealth(INIT_CONTEXT);
|
|
86
92
|
|
|
87
93
|
const COMMANDS = {
|
|
88
94
|
init: cmdInit,
|
|
89
95
|
update: cmdUpdate,
|
|
90
96
|
models: cmdModels,
|
|
91
97
|
health: cmdHealth,
|
|
98
|
+
'file-op': cmdFileOp,
|
|
99
|
+
'lifecycle-preflight': cmdLifecyclePreflight,
|
|
92
100
|
'find-phase': cmdFindPhase,
|
|
101
|
+
'phase-status': cmdPhaseStatus,
|
|
93
102
|
verify: cmdVerify,
|
|
94
103
|
scaffold: cmdScaffold,
|
|
95
104
|
help: cmdHelp,
|
|
96
105
|
};
|
|
97
106
|
|
|
98
|
-
async function runCli(cliCommand = command, cliArgs
|
|
107
|
+
async function runCli(cliCommand = command, ...cliArgs) {
|
|
108
|
+
const normalizedArgs = cliArgs.length === 0
|
|
109
|
+
? args
|
|
110
|
+
: cliArgs.length === 1 && Array.isArray(cliArgs[0])
|
|
111
|
+
? cliArgs[0]
|
|
112
|
+
: cliArgs;
|
|
113
|
+
|
|
114
|
+
process.exitCode = 0;
|
|
115
|
+
|
|
99
116
|
if (!cliCommand || !COMMANDS[cliCommand]) {
|
|
100
117
|
cmdHelp();
|
|
101
118
|
if (cliCommand) process.exitCode = 1;
|
|
102
119
|
return;
|
|
103
120
|
}
|
|
104
121
|
|
|
105
|
-
await COMMANDS[cliCommand](...
|
|
122
|
+
await COMMANDS[cliCommand](...normalizedArgs);
|
|
106
123
|
}
|
|
107
124
|
|
|
108
125
|
if (IS_MAIN) {
|
|
109
126
|
await runCli();
|
|
110
127
|
}
|
|
111
128
|
|
|
112
|
-
export { cmdHelp, cmdInit, cmdUpdate, cmdModels, cmdHealth, cmdFindPhase, cmdVerify, cmdScaffold, runCli, FRAMEWORK_VERSION, createCliContext };
|
|
129
|
+
export { cmdHelp, cmdInit, cmdUpdate, cmdModels, cmdHealth, cmdFileOp, cmdLifecyclePreflight, cmdFindPhase, cmdPhaseStatus, cmdVerify, cmdScaffold, runCli, FRAMEWORK_VERSION, createCliContext };
|
package/bin/lib/cli-utils.mjs
CHANGED
|
@@ -15,7 +15,7 @@ export function parseFlagValue(flagArgs, flagName) {
|
|
|
15
15
|
export function parseToolsFlag(flagArgs) {
|
|
16
16
|
const { value } = parseFlagValue(flagArgs, '--tools');
|
|
17
17
|
if (!value) return [];
|
|
18
|
-
if (value === 'all') return ['claude', 'opencode', 'codex', 'agents'];
|
|
18
|
+
if (value === 'all') return ['claude', 'opencode', 'codex', 'agents', 'cursor', 'copilot', 'gemini'];
|
|
19
19
|
return value.split(',').map((v) => v.trim()).filter(Boolean);
|
|
20
20
|
}
|
|
21
21
|
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
const EVIDENCE_KINDS = Object.freeze(['code', 'test', 'runtime', 'delivery', 'human']);
|
|
2
|
+
const DELIVERY_POSTURES = Object.freeze(['repo_only', 'delivery_sensitive']);
|
|
3
|
+
const CLOSURE_SURFACES = Object.freeze(['verify', 'audit-milestone', 'complete-milestone']);
|
|
4
|
+
|
|
5
|
+
const LEGACY_EVIDENCE_ALIASES = Object.freeze({
|
|
6
|
+
code: 'code',
|
|
7
|
+
test: 'test',
|
|
8
|
+
runtime: 'runtime',
|
|
9
|
+
delivery: 'delivery',
|
|
10
|
+
human: 'human',
|
|
11
|
+
'code-evidence': 'code',
|
|
12
|
+
'repo-test': 'test',
|
|
13
|
+
'runtime-check': 'runtime',
|
|
14
|
+
'user-confirmation': 'human',
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const EVIDENCE_MATRIX = Object.freeze({
|
|
18
|
+
verify: Object.freeze({
|
|
19
|
+
repo_only: Object.freeze({
|
|
20
|
+
requiredKinds: Object.freeze(['code']),
|
|
21
|
+
recommendedKinds: Object.freeze(['test']),
|
|
22
|
+
blockedSoloKinds: Object.freeze(['human', 'delivery']),
|
|
23
|
+
}),
|
|
24
|
+
delivery_sensitive: Object.freeze({
|
|
25
|
+
requiredKinds: Object.freeze(['code', 'runtime']),
|
|
26
|
+
recommendedKinds: Object.freeze(['test', 'delivery', 'human']),
|
|
27
|
+
blockedSoloKinds: Object.freeze(['code', 'human']),
|
|
28
|
+
}),
|
|
29
|
+
}),
|
|
30
|
+
'audit-milestone': Object.freeze({
|
|
31
|
+
repo_only: Object.freeze({
|
|
32
|
+
requiredKinds: Object.freeze(['code', 'test']),
|
|
33
|
+
recommendedKinds: Object.freeze(['runtime', 'human']),
|
|
34
|
+
blockedSoloKinds: Object.freeze(['human', 'delivery']),
|
|
35
|
+
}),
|
|
36
|
+
delivery_sensitive: Object.freeze({
|
|
37
|
+
requiredKinds: Object.freeze(['code', 'test', 'runtime', 'delivery']),
|
|
38
|
+
recommendedKinds: Object.freeze(['human']),
|
|
39
|
+
blockedSoloKinds: Object.freeze(['code', 'human']),
|
|
40
|
+
}),
|
|
41
|
+
}),
|
|
42
|
+
'complete-milestone': Object.freeze({
|
|
43
|
+
repo_only: Object.freeze({
|
|
44
|
+
requiredKinds: Object.freeze(['code', 'test']),
|
|
45
|
+
recommendedKinds: Object.freeze(['runtime']),
|
|
46
|
+
blockedSoloKinds: Object.freeze(['human', 'delivery']),
|
|
47
|
+
}),
|
|
48
|
+
delivery_sensitive: Object.freeze({
|
|
49
|
+
requiredKinds: Object.freeze(['code', 'test', 'runtime', 'delivery']),
|
|
50
|
+
recommendedKinds: Object.freeze(['human']),
|
|
51
|
+
blockedSoloKinds: Object.freeze(['code', 'human']),
|
|
52
|
+
}),
|
|
53
|
+
}),
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
export { CLOSURE_SURFACES, DELIVERY_POSTURES, EVIDENCE_KINDS };
|
|
57
|
+
|
|
58
|
+
export function normalizeEvidenceKind(kind) {
|
|
59
|
+
if (!kind) {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return LEGACY_EVIDENCE_ALIASES[kind] ?? null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function normalizeEvidenceKinds(kinds = []) {
|
|
67
|
+
const normalized = [];
|
|
68
|
+
for (const kind of kinds) {
|
|
69
|
+
const resolved = normalizeEvidenceKind(kind);
|
|
70
|
+
if (resolved && !normalized.includes(resolved)) {
|
|
71
|
+
normalized.push(resolved);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return normalized;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function isClosureSurface(surface) {
|
|
78
|
+
return CLOSURE_SURFACES.includes(surface);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function getEvidenceContract(surface, deliveryPosture) {
|
|
82
|
+
const matrix = EVIDENCE_MATRIX[surface];
|
|
83
|
+
if (!matrix) {
|
|
84
|
+
throw new Error(`Unsupported closure evidence surface: ${surface}`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const posture = matrix[deliveryPosture];
|
|
88
|
+
if (!posture) {
|
|
89
|
+
throw new Error(`Unsupported delivery posture for ${surface}: ${deliveryPosture}`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
surface,
|
|
94
|
+
deliveryPosture,
|
|
95
|
+
supportedKinds: [...EVIDENCE_KINDS],
|
|
96
|
+
requiredKinds: [...posture.requiredKinds],
|
|
97
|
+
recommendedKinds: [...posture.recommendedKinds],
|
|
98
|
+
blockedSoloKinds: [...posture.blockedSoloKinds],
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function describeEvidenceSurface(surface) {
|
|
103
|
+
if (!isClosureSurface(surface)) {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
surface,
|
|
109
|
+
supportedKinds: [...EVIDENCE_KINDS],
|
|
110
|
+
deliveryPostures: DELIVERY_POSTURES.map((deliveryPosture) => getEvidenceContract(surface, deliveryPosture)),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { cpSync, existsSync, mkdirSync, readFileSync, statSync, unlinkSync, writeFileSync } from 'fs';
|
|
2
|
+
import { dirname, isAbsolute, relative, resolve } from 'path';
|
|
3
|
+
import { output, parseFlagValue } from './cli-utils.mjs';
|
|
4
|
+
|
|
5
|
+
class FileOpError extends Error {}
|
|
6
|
+
|
|
7
|
+
function fail(message) {
|
|
8
|
+
console.error(message);
|
|
9
|
+
throw new FileOpError(message);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function resolveWorkspacePath(cwd, target) {
|
|
13
|
+
const workspaceRoot = resolve(cwd);
|
|
14
|
+
const resolved = resolve(workspaceRoot, target);
|
|
15
|
+
const rel = relative(workspaceRoot, resolved);
|
|
16
|
+
|
|
17
|
+
if (rel === '' || (!rel.startsWith('..') && !isAbsolute(rel))) {
|
|
18
|
+
return resolved;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
fail(`Path must stay inside the workspace: ${target}`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function getMissingBehavior(args) {
|
|
25
|
+
const parsed = parseFlagValue(args, '--missing');
|
|
26
|
+
if (!parsed.present) return 'error';
|
|
27
|
+
if (parsed.invalid) fail('Usage: --missing <error|ok>');
|
|
28
|
+
if (!['error', 'ok'].includes(parsed.value)) fail('Usage: --missing <error|ok>');
|
|
29
|
+
return parsed.value;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function cmdCopy(cwd, args) {
|
|
33
|
+
const [sourceArg, destinationArg, ...flags] = args;
|
|
34
|
+
if (!sourceArg || !destinationArg) {
|
|
35
|
+
fail('Usage: gsdd file-op copy <source> <destination> [--missing <error|ok>]');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const missingBehavior = getMissingBehavior(flags);
|
|
39
|
+
const source = resolveWorkspacePath(cwd, sourceArg);
|
|
40
|
+
const destination = resolveWorkspacePath(cwd, destinationArg);
|
|
41
|
+
|
|
42
|
+
if (!existsSync(source)) {
|
|
43
|
+
if (missingBehavior === 'ok') {
|
|
44
|
+
output({ operation: 'copy', source: sourceArg, destination: destinationArg, changed: false, reason: 'missing_source' });
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
fail(`Source file does not exist: ${sourceArg}`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (statSync(source).isDirectory()) {
|
|
51
|
+
fail(`Copy only supports files in this phase: ${sourceArg}`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
mkdirSync(dirname(destination), { recursive: true });
|
|
55
|
+
cpSync(source, destination, { force: true });
|
|
56
|
+
output({ operation: 'copy', source: sourceArg, destination: destinationArg, changed: true });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function cmdDelete(cwd, args) {
|
|
60
|
+
const [targetArg, ...flags] = args;
|
|
61
|
+
if (!targetArg) {
|
|
62
|
+
fail('Usage: gsdd file-op delete <target> [--missing <error|ok>]');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const missingBehavior = getMissingBehavior(flags);
|
|
66
|
+
const target = resolveWorkspacePath(cwd, targetArg);
|
|
67
|
+
|
|
68
|
+
if (!existsSync(target)) {
|
|
69
|
+
if (missingBehavior === 'ok') {
|
|
70
|
+
output({ operation: 'delete', target: targetArg, changed: false, reason: 'missing_target' });
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
fail(`Target file does not exist: ${targetArg}`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (statSync(target).isDirectory()) {
|
|
77
|
+
fail(`Delete only supports files in this phase: ${targetArg}`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
unlinkSync(target);
|
|
81
|
+
output({ operation: 'delete', target: targetArg, changed: true });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function cmdRegexSub(cwd, args) {
|
|
85
|
+
const [targetArg, pattern, replacement, ...flags] = args;
|
|
86
|
+
if (!targetArg || pattern === undefined || replacement === undefined) {
|
|
87
|
+
fail('Usage: gsdd file-op regex-sub <target> <pattern> <replacement> [--flags <flags>] [--missing <error|ok>]');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const regexFlags = parseFlagValue(flags, '--flags');
|
|
91
|
+
if (regexFlags.present && regexFlags.invalid) {
|
|
92
|
+
fail('Usage: --flags <regex-flags>');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const missingBehavior = getMissingBehavior(flags);
|
|
96
|
+
const target = resolveWorkspacePath(cwd, targetArg);
|
|
97
|
+
|
|
98
|
+
if (!existsSync(target)) {
|
|
99
|
+
if (missingBehavior === 'ok') {
|
|
100
|
+
output({ operation: 'regex-sub', target: targetArg, changed: false, reason: 'missing_target' });
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
fail(`Target file does not exist: ${targetArg}`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (statSync(target).isDirectory()) {
|
|
107
|
+
fail(`regex-sub only supports files in this phase: ${targetArg}`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
let regex;
|
|
111
|
+
try {
|
|
112
|
+
regex = new RegExp(pattern, regexFlags.value || 'g');
|
|
113
|
+
} catch (error) {
|
|
114
|
+
fail(`Invalid regex pattern: ${error.message}`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const source = readFileSync(target, 'utf-8');
|
|
118
|
+
let replacementCount = 0;
|
|
119
|
+
if (regex.global) {
|
|
120
|
+
const matches = source.match(regex);
|
|
121
|
+
replacementCount = matches ? matches.length : 0;
|
|
122
|
+
} else {
|
|
123
|
+
replacementCount = regex.test(source) ? 1 : 0;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (replacementCount === 0) {
|
|
127
|
+
fail(`Pattern did not match any text in ${targetArg}`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const updated = source.replace(regex, replacement);
|
|
131
|
+
const changed = updated !== source;
|
|
132
|
+
writeFileSync(target, updated);
|
|
133
|
+
output({ operation: 'regex-sub', target: targetArg, changed, replacements: replacementCount });
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function cmdFileOp(...args) {
|
|
137
|
+
const cwd = process.cwd();
|
|
138
|
+
const [operation, ...rest] = args;
|
|
139
|
+
|
|
140
|
+
try {
|
|
141
|
+
switch (operation) {
|
|
142
|
+
case 'copy':
|
|
143
|
+
cmdCopy(cwd, rest);
|
|
144
|
+
return;
|
|
145
|
+
case 'delete':
|
|
146
|
+
cmdDelete(cwd, rest);
|
|
147
|
+
return;
|
|
148
|
+
case 'regex-sub':
|
|
149
|
+
cmdRegexSub(cwd, rest);
|
|
150
|
+
return;
|
|
151
|
+
default:
|
|
152
|
+
fail('Usage: gsdd file-op <copy|delete|regex-sub> ...');
|
|
153
|
+
}
|
|
154
|
+
} catch (error) {
|
|
155
|
+
if (error instanceof FileOpError) {
|
|
156
|
+
process.exitCode = 1;
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
throw error;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { evaluateLifecycleState } from './lifecycle-state.mjs';
|
|
4
|
+
import {
|
|
5
|
+
getRuntimeFreshnessRepairGuidance,
|
|
6
|
+
summarizeRuntimeFreshnessIssues,
|
|
7
|
+
} from './runtime-freshness.mjs';
|
|
8
|
+
import { checkDrift } from './session-fingerprint.mjs';
|
|
9
|
+
|
|
10
|
+
export const TRUTH_CHECK_IDS = ['W7', 'W8', 'W9', 'W10', 'W11', 'W12'];
|
|
11
|
+
|
|
12
|
+
export function runTruthChecks(planningDir, frameworkDir, actualCheckIds, options = {}) {
|
|
13
|
+
const warnings = [];
|
|
14
|
+
const designPath = join(frameworkDir, 'distilled', 'DESIGN.md');
|
|
15
|
+
const readmePath = join(frameworkDir, 'distilled', 'README.md');
|
|
16
|
+
const workflowsDir = join(frameworkDir, 'distilled', 'workflows');
|
|
17
|
+
const gapsPath = join(frameworkDir, '.internal-research', 'gaps.md');
|
|
18
|
+
const specPath = join(planningDir, 'SPEC.md');
|
|
19
|
+
const roadmapPath = join(planningDir, 'ROADMAP.md');
|
|
20
|
+
const lifecycle = evaluateLifecycleState({ planningDir });
|
|
21
|
+
|
|
22
|
+
if (existsSync(designPath)) {
|
|
23
|
+
const documentedIds = extractHealthTableIds(readFileSync(designPath, 'utf-8'));
|
|
24
|
+
const missingFromDesign = actualCheckIds.filter((id) => !documentedIds.includes(id));
|
|
25
|
+
const extraInDesign = documentedIds.filter((id) => !actualCheckIds.includes(id));
|
|
26
|
+
if (missingFromDesign.length > 0 || extraInDesign.length > 0) {
|
|
27
|
+
const parts = [];
|
|
28
|
+
if (missingFromDesign.length > 0) parts.push(`missing in DESIGN.md: ${missingFromDesign.join(', ')}`);
|
|
29
|
+
if (extraInDesign.length > 0) parts.push(`extra in DESIGN.md: ${extraInDesign.join(', ')}`);
|
|
30
|
+
warnings.push({
|
|
31
|
+
id: 'W7',
|
|
32
|
+
severity: 'WARN',
|
|
33
|
+
message: `DESIGN.md health check table is out of sync (${parts.join('; ')})`,
|
|
34
|
+
fix: 'Update distilled/DESIGN.md section 20 to match the implemented health checks',
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (existsSync(readmePath) && existsSync(workflowsDir)) {
|
|
40
|
+
const readme = readFileSync(readmePath, 'utf-8');
|
|
41
|
+
const workflowFiles = readdirSync(workflowsDir).filter((entry) => entry.endsWith('.md')).sort();
|
|
42
|
+
const statusEntries = extractReadmeStatusEntries(readme);
|
|
43
|
+
const treeEntries = extractReadmeWorkflowTreeEntries(readme);
|
|
44
|
+
const issues = [];
|
|
45
|
+
if (statusEntries.length !== workflowFiles.length) {
|
|
46
|
+
issues.push(`status table ${statusEntries.length} != workflows dir ${workflowFiles.length}`);
|
|
47
|
+
}
|
|
48
|
+
if (treeEntries.length !== workflowFiles.length) {
|
|
49
|
+
issues.push(`framework tree ${treeEntries.length} != workflows dir ${workflowFiles.length}`);
|
|
50
|
+
}
|
|
51
|
+
const missingStatus = workflowFiles.filter((name) => !statusEntries.includes(name));
|
|
52
|
+
const missingTree = workflowFiles.filter((name) => !treeEntries.includes(name));
|
|
53
|
+
if (missingStatus.length > 0) issues.push(`missing from status table: ${missingStatus.join(', ')}`);
|
|
54
|
+
if (missingTree.length > 0) issues.push(`missing from framework tree: ${missingTree.join(', ')}`);
|
|
55
|
+
if (issues.length > 0) {
|
|
56
|
+
warnings.push({
|
|
57
|
+
id: 'W8',
|
|
58
|
+
severity: 'WARN',
|
|
59
|
+
message: `distilled/README.md workflow inventory is out of sync (${issues.join('; ')})`,
|
|
60
|
+
fix: 'Update distilled/README.md workflow status table and framework file tree to match distilled/workflows/',
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (existsSync(gapsPath)) {
|
|
66
|
+
const gapRefs = extractRepoLocalPaths(readFileSync(gapsPath, 'utf-8'));
|
|
67
|
+
const missingRefs = gapRefs.filter((ref) => !existsSync(join(frameworkDir, ref)));
|
|
68
|
+
if (missingRefs.length > 0) {
|
|
69
|
+
warnings.push({
|
|
70
|
+
id: 'W9',
|
|
71
|
+
severity: 'WARN',
|
|
72
|
+
message: `gaps.md references missing repo-local paths (${missingRefs.join(', ')})`,
|
|
73
|
+
fix: 'Annotate stale gap references as resolved or update them to current repo truth',
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (existsSync(specPath) && existsSync(roadmapPath)) {
|
|
79
|
+
const mismatches = lifecycle.requirementAlignment.mismatches;
|
|
80
|
+
if (mismatches.length > 0) {
|
|
81
|
+
warnings.push({
|
|
82
|
+
id: 'W10',
|
|
83
|
+
severity: 'WARN',
|
|
84
|
+
message: `ROADMAP/SPEC requirement status drift (${mismatches.join('; ')})`,
|
|
85
|
+
fix: 'Reconcile .planning/ROADMAP.md phase completion markers with .planning/SPEC.md requirement checkboxes',
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (options.runtimeFreshnessReport?.issueCount > 0) {
|
|
91
|
+
warnings.push({
|
|
92
|
+
id: 'W11',
|
|
93
|
+
severity: 'WARN',
|
|
94
|
+
message: `Installed generated runtime surfaces drift from current render output (${summarizeRuntimeFreshnessIssues(options.runtimeFreshnessReport)})`,
|
|
95
|
+
fix: getRuntimeFreshnessRepairGuidance(options.runtimeFreshnessReport),
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const drift = checkDrift(planningDir);
|
|
100
|
+
if (drift.drifted) {
|
|
101
|
+
warnings.push({
|
|
102
|
+
id: 'W12',
|
|
103
|
+
severity: 'WARN',
|
|
104
|
+
message: `Planning state drifted since last recorded session (${drift.details.join('; ')})`,
|
|
105
|
+
fix: 'Review the changes, then run a lifecycle workflow to update the fingerprint',
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return warnings;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function extractHealthTableIds(content) {
|
|
113
|
+
const section = extractSection(content, '## 20. Workspace Health Diagnostics', '## 21.');
|
|
114
|
+
if (!section) return [];
|
|
115
|
+
return [...normalizeContent(section).matchAll(/^\|\s*([EWI]\d+)\s*\|/gm)].map((result) => result[1]);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function extractReadmeStatusEntries(content) {
|
|
119
|
+
const section = extractSection(content, '## Current Status', 'Architecture notes:');
|
|
120
|
+
if (!section) return [];
|
|
121
|
+
return [...normalizeContent(section).matchAll(/\|\s*`([^`]+\.md)`\s*\|/g)].map((result) => result[1]);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function extractReadmeWorkflowTreeEntries(content) {
|
|
125
|
+
const section = extractSection(content, '## Files In This Framework', '## ');
|
|
126
|
+
const match = normalizeContent(section || '').match(/```[\s\S]*?workflows\/\n([\s\S]*?)\n\s*templates\//);
|
|
127
|
+
if (!match) return [];
|
|
128
|
+
return match[1]
|
|
129
|
+
.split('\n')
|
|
130
|
+
.map((line) => line.trim())
|
|
131
|
+
.filter((line) => line.endsWith('.md'));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function extractRepoLocalPaths(content) {
|
|
135
|
+
const allowedRoots = [
|
|
136
|
+
'AGENTS.md',
|
|
137
|
+
'README.md',
|
|
138
|
+
'CHANGELOG.md',
|
|
139
|
+
'SPEC.md',
|
|
140
|
+
'package.json',
|
|
141
|
+
'.planning/',
|
|
142
|
+
'.internal-research/',
|
|
143
|
+
'.agents/',
|
|
144
|
+
'.claude/',
|
|
145
|
+
'.opencode/',
|
|
146
|
+
'.codex/',
|
|
147
|
+
'.worktrees/',
|
|
148
|
+
'bin/',
|
|
149
|
+
'distilled/',
|
|
150
|
+
'tests/',
|
|
151
|
+
'fixtures/',
|
|
152
|
+
'agents/',
|
|
153
|
+
];
|
|
154
|
+
const refs = new Set();
|
|
155
|
+
for (const result of content.matchAll(/`([^`]+)`/g)) {
|
|
156
|
+
const value = result[1].trim();
|
|
157
|
+
if (!looksLikeRepoLocalPath(value, allowedRoots)) continue;
|
|
158
|
+
refs.add(value.replace(/\\/g, '/').replace(/\/$/, ''));
|
|
159
|
+
}
|
|
160
|
+
return [...refs];
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function looksLikeRepoLocalPath(value, allowedRoots) {
|
|
164
|
+
if (value.includes(' ')) return false;
|
|
165
|
+
if (value.startsWith('/')) return false;
|
|
166
|
+
if (value.startsWith('$')) return false;
|
|
167
|
+
if (value.startsWith('..')) return false;
|
|
168
|
+
if (value.includes('://')) return false;
|
|
169
|
+
if (value.startsWith('feat/') || value.startsWith('fix/') || value.startsWith('pr') || value.startsWith('origin/')) return false;
|
|
170
|
+
if (allowedRoots.some((root) => value === root || value.startsWith(root))) return true;
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function extractSection(content, startMarker, endMarker) {
|
|
175
|
+
const normalized = normalizeContent(content);
|
|
176
|
+
const start = normalized.indexOf(startMarker);
|
|
177
|
+
if (start === -1) return '';
|
|
178
|
+
const rest = normalized.slice(start);
|
|
179
|
+
if (!endMarker) return rest;
|
|
180
|
+
const end = rest.indexOf(endMarker, startMarker.length);
|
|
181
|
+
return end === -1 ? rest : rest.slice(0, end);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function normalizeContent(content) {
|
|
185
|
+
return content.replace(/\r\n/g, '\n');
|
|
186
|
+
}
|