codex-genesis-harness 0.1.7 → 0.1.9
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/.codebase/COMPRESSED_CONTEXT.md +80 -0
- package/.codebase/CURRENT_STATE.md +10 -10
- package/.codebase/DEPENDENCY_GRAPH.md +14 -1
- package/.codebase/IMPLEMENTATION_HANDOFF.md +34 -336
- package/.codebase/KNOWN_PROBLEMS.md +73 -3
- package/.codebase/MODULE_INDEX.md +23 -2
- package/.codebase/PIPELINE_FLOW.md +16 -6
- package/.codebase/RECOVERY_POINTS.md +80 -78
- package/.codebase/TECH_DEBT.md +6 -0
- package/.codebase/TEST_MATRIX.md +8 -3
- package/.codebase/VISUAL_GRAPH.md +127 -0
- package/.codebase/context-policy.json +68 -0
- package/.codebase/memories/lessons_learned.md +63 -0
- package/.codebase/memories/preferences.md +17 -0
- package/.codebase/state.json +156 -17
- package/.codex/skills/genesis-architecture/SKILL.md +5 -0
- package/.codex/skills/genesis-debug-guide/SKILL.md +10 -4
- package/.codex/skills/genesis-docs-automation/SKILL.md +52 -973
- package/.codex/skills/genesis-executing-plans/SKILL.md +54 -0
- package/.codex/skills/genesis-executing-plans/agents/openai.yaml +6 -0
- package/.codex/skills/genesis-executing-plans/checklists/.gitkeep +0 -0
- package/.codex/skills/genesis-executing-plans/examples/.gitkeep +0 -0
- package/.codex/skills/genesis-executing-plans/templates/.gitkeep +0 -0
- package/.codex/skills/genesis-harness/SKILL.md +73 -1385
- package/.codex/skills/genesis-harness/agents/openai.yaml +1 -2
- package/.codex/skills/genesis-harness/references/state-machine.md +4 -1
- package/.codex/skills/genesis-harness/references/workflows.md +7 -1
- package/.codex/skills/genesis-harness/scripts/check-docs-sync.sh +3 -3
- package/.codex/skills/genesis-harness/scripts/init-planning.sh +246 -14
- package/.codex/skills/genesis-new-design/SKILL.md +4 -1
- package/.codex/skills/genesis-new-design/agents/openai.yaml +2 -0
- package/.codex/skills/genesis-observability-automation/SKILL.md +69 -303
- package/.codex/skills/genesis-observability-automation/references/common-mistakes-and-recovery.md +84 -0
- package/.codex/skills/genesis-observability-automation/references/workflow-phases.md +78 -0
- package/.codex/skills/genesis-performance-profiling/SKILL.md +1 -22
- package/.codex/skills/genesis-performance-profiling/agents/openai.yaml +1 -1
- package/.codex/skills/genesis-pipeline-orchestration/SKILL.md +15 -3
- package/.codex/skills/genesis-planning/SKILL.md +6 -1
- package/.codex/skills/genesis-release/SKILL.md +5 -0
- package/.codex/skills/genesis-research-first/SKILL.md +6 -0
- package/.codex/skills/genesis-spec-propagation/SKILL.md +52 -504
- package/.codex/skills/genesis-test-driven-development/SKILL.md +55 -0
- package/.codex/skills/genesis-test-driven-development/agents/openai.yaml +6 -0
- package/.codex/skills/genesis-test-driven-development/checklists/.gitkeep +0 -0
- package/.codex/skills/genesis-test-driven-development/examples/.gitkeep +0 -0
- package/.codex/skills/genesis-test-driven-development/templates/.gitkeep +0 -0
- package/.codex/skills/genesis-upgrade-design/SKILL.md +4 -2
- package/.codex/skills/genesis-upgrade-design/agents/openai.yaml +2 -0
- package/.codex/skills/genesis-using-git-worktrees/SKILL.md +54 -0
- package/.codex/skills/genesis-using-git-worktrees/agents/openai.yaml +6 -0
- package/.codex/skills/genesis-using-git-worktrees/checklists/.gitkeep +0 -0
- package/.codex/skills/genesis-using-git-worktrees/examples/.gitkeep +0 -0
- package/.codex/skills/genesis-using-git-worktrees/templates/.gitkeep +0 -0
- package/.codex/skills/genesis-verification-before-completion/SKILL.md +53 -0
- package/.codex/skills/genesis-verification-before-completion/agents/openai.yaml +6 -0
- package/.codex/skills/genesis-verification-before-completion/checklists/.gitkeep +0 -0
- package/.codex/skills/genesis-verification-before-completion/examples/.gitkeep +0 -0
- package/.codex/skills/genesis-verification-before-completion/templates/.gitkeep +0 -0
- package/.codex/skills/spec-impact-engine/SKILL.md +77 -500
- package/.codex/skills/spec-impact-engine/checklists/checklist.md +10 -0
- package/.codex-plugin/plugin.json +6 -5
- package/CHANGELOG.md +25 -1
- package/README.EN.md +74 -17
- package/README.VI.md +77 -19
- package/README.md +126 -10
- package/VERSION +1 -2
- package/bin/genesis-harness.js +2979 -149
- package/contracts/features/project-registry-schema.json +37 -0
- package/contracts/features/registry-schema.json +15 -0
- package/contracts/observability/agent-run-schema.json +39 -0
- package/contracts/observability/failure-schema.json +35 -0
- package/contracts/ui/auth/login-screen-contract.json +43 -0
- package/features/REGISTRY.md +65 -0
- package/features/SCOPE-template.md +65 -0
- package/fixtures/pipeline/end-to-end-project-lifecycle-fixture.md +39 -0
- package/fixtures/pipeline/feature-completion-fixture.md +26 -0
- package/fixtures/pipeline/run-to-feature-execution-fixture.md +20 -0
- package/fixtures/planning/MOCKUP_PROMPT_TEMPLATE.md +16 -0
- package/observability/agent-runs/sample-run.json +13 -0
- package/observability/decision-logs/sample-decision.md +43 -0
- package/observability/failures/sample-failure.json +12 -0
- package/package.json +15 -4
- package/playwright/e2e/app-template.spec.js +37 -0
- package/playwright/e2e/auth/login-screen.spec.js +65 -0
- package/playwright/e2e/web-template.spec.js +28 -0
- package/scripts/check-repository-hygiene.js +48 -0
- package/scripts/check-scope.sh +100 -0
- package/scripts/cold-start-check.js +133 -0
- package/scripts/install.sh +4 -0
- package/scripts/prompt_sentinel.js +35 -4
- package/scripts/run-evals.sh +152 -3
- package/scripts/schema/001-init.sql +129 -0
- package/scripts/schema/002-story-verify.sql +9 -0
- package/scripts/schema/003-tool-registry.sql +15 -0
- package/scripts/schema/004-intervention.sql +15 -0
- package/scripts/scratch_parser.js +49 -0
- package/scripts/spec_visual_sync.js +1 -1
- package/scripts/test_generator.js +2 -2
- package/scripts/transition_state.sh +32 -8
- package/scripts/uninstall.sh +4 -0
- package/scripts/validation_gates.sh +2 -80
- package/scripts/verify.sh +19 -2
- package/tests/fixtures/fixture-index.md +5 -0
- package/tests/integration/cli-smoke.test.js +506 -0
- package/tests/unit/feature_registry.test.js +152 -0
- package/tests/unit/prompt_sentinel.test.js +1 -1
- package/tests/unit/repository_hygiene.test.js +17 -0
- package/tests/unit/spec_visual_sync.test.js +1 -1
- package/tests/unit/state_metadata.test.js +76 -0
- package/tests/unit/test_generator.test.js +1 -1
- package/tests/unit/verify_gate.test.js +25 -0
- package/tests/unit/workflow_contracts.test.js +90 -0
- package/fixtures/tts/tts-fixture-template.md +0 -14
- package/fixtures/videos/video-fixture-template.md +0 -14
- package/playwright/e2e/e2e-template.md +0 -4
package/bin/genesis-harness.js
CHANGED
|
@@ -27,7 +27,11 @@ const skillNames = [
|
|
|
27
27
|
"genesis-observability-automation",
|
|
28
28
|
"genesis-research-first",
|
|
29
29
|
"genesis-release",
|
|
30
|
-
"spec-impact-engine"
|
|
30
|
+
"spec-impact-engine",
|
|
31
|
+
"genesis-executing-plans",
|
|
32
|
+
"genesis-test-driven-development",
|
|
33
|
+
"genesis-verification-before-completion",
|
|
34
|
+
"genesis-using-git-worktrees"
|
|
31
35
|
];
|
|
32
36
|
const legacySkillNames = ["project-genesis-harness"];
|
|
33
37
|
const sourceRoot = path.join(packageRoot, ".codex", "skills");
|
|
@@ -47,16 +51,35 @@ Usage:
|
|
|
47
51
|
genesis-harness path
|
|
48
52
|
genesis-harness status Show implementation status & skills inventory
|
|
49
53
|
genesis-harness docs Show API contracts & documentation sync report
|
|
54
|
+
genesis-harness docs-gate Run pre-commit documentation drift checks
|
|
55
|
+
genesis-harness verify-gate Run ALL verification gates before claiming done (L09 blocker)
|
|
56
|
+
genesis-harness cold-start Run automated cold-start L03 checklist
|
|
50
57
|
genesis-harness remember [cat] "<msg>" Remember a persistent project fact/insight (Bead)
|
|
51
58
|
genesis-harness recall [query] Recall and search remembered project facts
|
|
52
59
|
genesis-harness forget <id> Forget/delete a fact by its unique 6-char ID
|
|
53
60
|
genesis-harness prime Generate the token-minimized Agent Priming Prompt
|
|
61
|
+
genesis-harness leanctx Show token budget policy and portable command guidance
|
|
54
62
|
genesis-harness view-mockup [slug] Interactive console UI to search & view mockups
|
|
63
|
+
genesis-harness mcp Interactive MCP installer
|
|
64
|
+
genesis-harness init [--platform codex|antigravity] [--yes] [--idea "<user brief>"]
|
|
65
|
+
genesis-harness run --idea "<user brief>" [--platform codex|antigravity] [--yes] [--product-approach "..."] [--primary-user "..."] [--v1-outcome "..."] [--qa-owner "..."] [--backend "..."] [--frontend "..."] [--database "..."] [--deployment "..."] [--test-strategy "..."] [--stack-owner "..."]
|
|
66
|
+
genesis-harness resume Show the active resumable run summary for this repo
|
|
67
|
+
genesis-harness next Show the next executable lifecycle action
|
|
68
|
+
genesis-harness add-feature --title "<title>" --slug "<slug>" --verify-cmd "<command>"
|
|
69
|
+
genesis-harness complete-feature --verify-cmd "<command>" --evidence "<summary>"
|
|
70
|
+
genesis-harness verify-project --verify-cmd "<command>" --evidence "<summary>"
|
|
71
|
+
genesis-harness complete-project --evidence "<summary>"
|
|
72
|
+
genesis-harness pipeline-audit Validate lifecycle state, proof, and artifacts
|
|
73
|
+
genesis-harness sync Compress and sync codebase context (AST/Regex)
|
|
74
|
+
genesis-harness setup-hooks Install auto-sync git pre-commit hook
|
|
75
|
+
genesis-harness heal <command> Run test & print agent directive on failure
|
|
55
76
|
|
|
56
77
|
Environment:
|
|
57
78
|
CODEX_HOME=/custom/.codex Override Codex home
|
|
58
79
|
GENESIS_HARNESS_HOME=/custom/.agents Override modern skills home
|
|
59
80
|
GENESIS_HARNESS_SKIP_POSTINSTALL=1 Skip npm postinstall auto-install
|
|
81
|
+
GENESIS_HARNESS_COMMAND_WRAPPER=rtk Optional local command wrapper override
|
|
82
|
+
GENESIS_HARNESS_DISABLE_RTK=1 Disable automatic rtk detection
|
|
60
83
|
`;
|
|
61
84
|
console.log(text.trim());
|
|
62
85
|
process.exit(exitCode);
|
|
@@ -107,7 +130,11 @@ function copySkills({ quiet = false, target = "both" } = {}) {
|
|
|
107
130
|
if (fs.existsSync(dir)) {
|
|
108
131
|
const backupParent = path.join(root, "..", "backups");
|
|
109
132
|
fs.mkdirSync(backupParent, { recursive: true });
|
|
110
|
-
|
|
133
|
+
let backupDir = path.join(backupParent, `${skillName}.backup.${timestamp()}`);
|
|
134
|
+
let suffix = 1;
|
|
135
|
+
while (fs.existsSync(backupDir)) {
|
|
136
|
+
backupDir = path.join(backupParent, `${skillName}.backup.${timestamp()}.${suffix++}`);
|
|
137
|
+
}
|
|
111
138
|
fs.renameSync(dir, backupDir);
|
|
112
139
|
if (!quiet) console.log(`Existing skill backed up to: ${backupDir}`);
|
|
113
140
|
}
|
|
@@ -122,6 +149,77 @@ function copySkills({ quiet = false, target = "both" } = {}) {
|
|
|
122
149
|
if (!quiet) console.log("Restart Codex, then invoke: Use $genesis-harness");
|
|
123
150
|
}
|
|
124
151
|
|
|
152
|
+
function copySkillsToProjectRoot(rootPath, { quiet = false } = {}) {
|
|
153
|
+
ensureSource();
|
|
154
|
+
const projectSkillsRoot = path.join(rootPath, ".codex", "skills");
|
|
155
|
+
fs.mkdirSync(projectSkillsRoot, { recursive: true });
|
|
156
|
+
|
|
157
|
+
for (const skillName of skillNames) {
|
|
158
|
+
const sourceDir = path.join(sourceRoot, skillName);
|
|
159
|
+
const dir = path.join(projectSkillsRoot, skillName);
|
|
160
|
+
|
|
161
|
+
if (fs.existsSync(dir)) {
|
|
162
|
+
const backupParent = path.join(rootPath, ".codex", "backups");
|
|
163
|
+
fs.mkdirSync(backupParent, { recursive: true });
|
|
164
|
+
let backupDir = path.join(backupParent, `${skillName}.backup.${timestamp()}`);
|
|
165
|
+
let suffix = 1;
|
|
166
|
+
while (fs.existsSync(backupDir)) {
|
|
167
|
+
backupDir = path.join(backupParent, `${skillName}.backup.${timestamp()}.${suffix++}`);
|
|
168
|
+
}
|
|
169
|
+
fs.renameSync(dir, backupDir);
|
|
170
|
+
if (!quiet) console.log(`Existing project skill backed up to: ${backupDir}`);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
fs.cpSync(sourceDir, dir, { recursive: true });
|
|
174
|
+
chmodScripts(path.join(dir, "scripts"));
|
|
175
|
+
|
|
176
|
+
if (!quiet) console.log(`Installed ${skillName} to: ${dir}`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function shouldSeedProjectRoot(rootPath) {
|
|
181
|
+
if (!rootPath) return false;
|
|
182
|
+
const resolvedRoot = path.resolve(rootPath);
|
|
183
|
+
if (resolvedRoot === packageRoot) return false;
|
|
184
|
+
const markers = [
|
|
185
|
+
"package.json",
|
|
186
|
+
"AGENTS.md",
|
|
187
|
+
"pyproject.toml",
|
|
188
|
+
"Cargo.toml",
|
|
189
|
+
"go.mod",
|
|
190
|
+
".git",
|
|
191
|
+
".codebase"
|
|
192
|
+
];
|
|
193
|
+
return markers.some(marker => fs.existsSync(path.join(resolvedRoot, marker)));
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function seedLeanCtxPolicy(rootPath = process.cwd(), { quiet = false } = {}) {
|
|
197
|
+
if (!shouldSeedProjectRoot(rootPath)) return false;
|
|
198
|
+
|
|
199
|
+
const sourcePolicy = path.join(packageRoot, ".codebase", "context-policy.json");
|
|
200
|
+
if (!fs.existsSync(sourcePolicy)) return false;
|
|
201
|
+
|
|
202
|
+
const codebaseDir = path.join(rootPath, ".codebase");
|
|
203
|
+
const targetPolicy = path.join(codebaseDir, "context-policy.json");
|
|
204
|
+
fs.mkdirSync(codebaseDir, { recursive: true });
|
|
205
|
+
|
|
206
|
+
if (fs.existsSync(targetPolicy)) {
|
|
207
|
+
if (!quiet) console.log(`[genesis-harness] LeanCTX policy already exists: ${targetPolicy}`);
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
fs.copyFileSync(sourcePolicy, targetPolicy);
|
|
212
|
+
if (!quiet) console.log(`[genesis-harness] LeanCTX policy installed: ${targetPolicy}`);
|
|
213
|
+
return true;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function resolvePostinstallProjectRoot() {
|
|
217
|
+
const initCwd = process.env.INIT_CWD;
|
|
218
|
+
if (shouldSeedProjectRoot(initCwd)) return initCwd;
|
|
219
|
+
if (shouldSeedProjectRoot(process.cwd())) return process.cwd();
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
|
|
125
223
|
function uninstallSkills(target = "both") {
|
|
126
224
|
for (const root of targetRoots(target)) {
|
|
127
225
|
for (const skillName of [...skillNames, ...legacySkillNames]) {
|
|
@@ -165,6 +263,119 @@ function resolveBash() {
|
|
|
165
263
|
return "bash";
|
|
166
264
|
}
|
|
167
265
|
|
|
266
|
+
function commandExists(commandName) {
|
|
267
|
+
if (!/^[A-Za-z0-9._-]+$/.test(commandName)) return false;
|
|
268
|
+
const result = process.platform === "win32"
|
|
269
|
+
? spawnSync("where", [commandName], { stdio: "ignore" })
|
|
270
|
+
: spawnSync("sh", ["-c", `command -v ${commandName}`], { stdio: "ignore" });
|
|
271
|
+
return result.status === 0;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function detectCommandWrapper() {
|
|
275
|
+
if (process.env.GENESIS_HARNESS_COMMAND_WRAPPER) {
|
|
276
|
+
return {
|
|
277
|
+
command: process.env.GENESIS_HARNESS_COMMAND_WRAPPER,
|
|
278
|
+
source: "GENESIS_HARNESS_COMMAND_WRAPPER"
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (process.env.GENESIS_HARNESS_DISABLE_RTK === "1") {
|
|
283
|
+
return { command: null, source: "disabled" };
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (commandExists("rtk")) {
|
|
287
|
+
return { command: "rtk", source: "auto-detected" };
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return { command: null, source: "not detected" };
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function readJsonIfExists(filePath) {
|
|
294
|
+
if (!fs.existsSync(filePath)) return null;
|
|
295
|
+
try {
|
|
296
|
+
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
297
|
+
} catch (error) {
|
|
298
|
+
return null;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function defaultContextPolicy() {
|
|
303
|
+
return {
|
|
304
|
+
name: "leanctx-default",
|
|
305
|
+
token_budget: 12000,
|
|
306
|
+
warn_at: 0.6,
|
|
307
|
+
compact_at: 0.7,
|
|
308
|
+
hard_stop_at: 0.85,
|
|
309
|
+
portable_commands: [
|
|
310
|
+
"genesis-harness leanctx",
|
|
311
|
+
"genesis-harness sync",
|
|
312
|
+
"genesis-harness docs-gate",
|
|
313
|
+
"npm run verify",
|
|
314
|
+
"npm run eval"
|
|
315
|
+
],
|
|
316
|
+
wrapper_policy: "rtk optional when installed locally; public docs and CI must use portable commands.",
|
|
317
|
+
layers: []
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function loadContextPolicy(rootPath = process.cwd()) {
|
|
322
|
+
const projectPolicy = readJsonIfExists(path.join(rootPath, ".codebase", "context-policy.json"));
|
|
323
|
+
const packagedPolicy = readJsonIfExists(path.join(packageRoot, ".codebase", "context-policy.json"));
|
|
324
|
+
return {
|
|
325
|
+
...defaultContextPolicy(),
|
|
326
|
+
...(packagedPolicy || {}),
|
|
327
|
+
...(projectPolicy || {})
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function formatCommand(command, wrapper) {
|
|
332
|
+
if (!wrapper.command) return command;
|
|
333
|
+
return `${wrapper.command} ${command}`;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function buildLeanCtxReport(rootPath = process.cwd()) {
|
|
337
|
+
const policy = loadContextPolicy(rootPath);
|
|
338
|
+
const wrapper = detectCommandWrapper();
|
|
339
|
+
const tokenBudget = Number(policy.token_budget || 12000);
|
|
340
|
+
const compactAt = Number(policy.compact_at || 0.7);
|
|
341
|
+
const hardStopAt = Number(policy.hard_stop_at || 0.85);
|
|
342
|
+
const lines = [];
|
|
343
|
+
|
|
344
|
+
lines.push("# LeanCTX Policy");
|
|
345
|
+
lines.push("");
|
|
346
|
+
lines.push(`- Policy: ${policy.name || "leanctx-default"}`);
|
|
347
|
+
lines.push(`- Token budget: ${tokenBudget}`);
|
|
348
|
+
lines.push(`- Compact at: ${Math.round(tokenBudget * compactAt)} tokens (${compactAt})`);
|
|
349
|
+
lines.push(`- Hard stop at: ${Math.round(tokenBudget * hardStopAt)} tokens (${hardStopAt})`);
|
|
350
|
+
lines.push(`- Command wrapper: ${wrapper.command ? `${wrapper.command} (${wrapper.source})` : `none (${wrapper.source})`} - rtk optional`);
|
|
351
|
+
lines.push(`- Wrapper policy: ${policy.wrapper_policy || "rtk optional; keep public commands portable."}`);
|
|
352
|
+
lines.push("");
|
|
353
|
+
lines.push("## Portable Commands");
|
|
354
|
+
for (const command of policy.portable_commands || []) {
|
|
355
|
+
lines.push(`- \`${command}\``);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
if (wrapper.command) {
|
|
359
|
+
lines.push("");
|
|
360
|
+
lines.push("## Local Wrapper Commands");
|
|
361
|
+
for (const command of policy.portable_commands || []) {
|
|
362
|
+
lines.push(`- \`${formatCommand(command, wrapper)}\``);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
if (Array.isArray(policy.layers) && policy.layers.length > 0) {
|
|
367
|
+
lines.push("");
|
|
368
|
+
lines.push("## Context Layers");
|
|
369
|
+
for (const layer of policy.layers) {
|
|
370
|
+
lines.push(`- ${layer.name}: ${layer.max_tokens || "unbounded"} tokens`);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
lines.push("");
|
|
375
|
+
lines.push("Use LeanCTX by loading core state first, then active context, then deferred references only when needed.");
|
|
376
|
+
return lines.join("\n");
|
|
377
|
+
}
|
|
378
|
+
|
|
168
379
|
function chmodScripts(dir) {
|
|
169
380
|
if (!fs.existsSync(dir)) return;
|
|
170
381
|
for (const entry of fs.readdirSync(dir)) {
|
|
@@ -236,11 +447,11 @@ function showStatus() {
|
|
|
236
447
|
}
|
|
237
448
|
}
|
|
238
449
|
} else {
|
|
239
|
-
console.log("\n\x1b[1m\x1b[33m[-] FSM Active Planning:\x1b[0m No active .planning/ session found.
|
|
450
|
+
console.log("\n\x1b[1m\x1b[33m[-] FSM Active Planning:\x1b[0m No active .planning/ session found. Start with a user idea or run `genesis-harness init --yes --platform codex --idea \"<brief>\"`.");
|
|
240
451
|
}
|
|
241
452
|
|
|
242
453
|
// 3. Skills Inventory
|
|
243
|
-
console.log("\n\x1b[1m\x1b[32m[+] Skills Inventory Check (Exactly
|
|
454
|
+
console.log("\n\x1b[1m\x1b[32m[+] Skills Inventory Check (Exactly 25 core skills):\x1b[0m");
|
|
244
455
|
let found = 0;
|
|
245
456
|
let mismatched = 0;
|
|
246
457
|
for (const skillName of skillNames) {
|
|
@@ -304,7 +515,7 @@ function showDocsStatus() {
|
|
|
304
515
|
}
|
|
305
516
|
}
|
|
306
517
|
console.log(` Files: \x1b[33m${found.join(", ")}\x1b[0m`);
|
|
307
|
-
|
|
518
|
+
|
|
308
519
|
// Print request structure preview if request.json exists
|
|
309
520
|
const reqPath = path.join(apiDir, endpoint, "request.json");
|
|
310
521
|
if (fs.existsSync(reqPath)) {
|
|
@@ -596,12 +807,19 @@ function primeContext() {
|
|
|
596
807
|
out.push("3. **Single source**: Avoid duplicating plans across multi-line markdown logs; use `genesis-harness remember` to store critical project coordinates.");
|
|
597
808
|
out.push("4. **TDD Pattern**: Create or update failing tests in `tests/` before making changes to public behaviors.");
|
|
598
809
|
out.push("");
|
|
810
|
+
out.push("## 🪶 7. LeanCTX Policy");
|
|
811
|
+
out.push(buildLeanCtxReport(process.cwd()));
|
|
812
|
+
out.push("");
|
|
599
813
|
out.push("---");
|
|
600
814
|
out.push("");
|
|
601
815
|
|
|
602
816
|
console.log(out.join("\n"));
|
|
603
817
|
}
|
|
604
818
|
|
|
819
|
+
function showLeanCtx() {
|
|
820
|
+
console.log(buildLeanCtxReport(process.cwd()));
|
|
821
|
+
}
|
|
822
|
+
|
|
605
823
|
function openFileNatively(filePath) {
|
|
606
824
|
if (process.platform === "win32") {
|
|
607
825
|
const cp = spawnSync("cmd.exe", ["/c", "start", "", filePath], { shell: true });
|
|
@@ -611,179 +829,2719 @@ function openFileNatively(filePath) {
|
|
|
611
829
|
if (process.platform === "linux") {
|
|
612
830
|
cmd = "xdg-open";
|
|
613
831
|
}
|
|
614
|
-
|
|
832
|
+
|
|
615
833
|
const cp = spawnSync(cmd, [filePath]);
|
|
616
834
|
return cp.status === 0;
|
|
617
835
|
}
|
|
618
836
|
|
|
619
|
-
function
|
|
620
|
-
const
|
|
621
|
-
|
|
622
|
-
|
|
837
|
+
function ensureProjectScaffold(rootPath) {
|
|
838
|
+
const dirs = [
|
|
839
|
+
".codebase/context",
|
|
840
|
+
".codebase/failures",
|
|
841
|
+
".codebase/memories",
|
|
842
|
+
"contracts/api",
|
|
843
|
+
"contracts/ui",
|
|
844
|
+
"tests/integration",
|
|
845
|
+
"tests/unit",
|
|
846
|
+
"fixtures",
|
|
847
|
+
"observability/agent-runs"
|
|
848
|
+
];
|
|
849
|
+
|
|
850
|
+
for (const dir of dirs) {
|
|
851
|
+
fs.mkdirSync(path.join(rootPath, dir), { recursive: true });
|
|
852
|
+
}
|
|
853
|
+
}
|
|
623
854
|
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
});
|
|
641
|
-
}
|
|
642
|
-
}
|
|
643
|
-
}
|
|
855
|
+
function runInitPlanning(rootPath, idea = "") {
|
|
856
|
+
const initScript = path.join(packageRoot, ".codex", "skills", "genesis-harness", "scripts", "init-planning.sh");
|
|
857
|
+
if (!fs.existsSync(initScript)) {
|
|
858
|
+
fail(`missing init planning script at ${initScript}`);
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
const bash = resolveBash();
|
|
862
|
+
const commandArgs = [initScript, "--confirmed", "--root", rootPath];
|
|
863
|
+
if (idea) {
|
|
864
|
+
commandArgs.push("--idea", idea);
|
|
865
|
+
}
|
|
866
|
+
const result = spawnSync(bash, commandArgs, {
|
|
867
|
+
encoding: "utf8",
|
|
868
|
+
env: {
|
|
869
|
+
...process.env,
|
|
870
|
+
PROJECT_BRIEF_CONFIRMED: "1"
|
|
644
871
|
}
|
|
645
|
-
};
|
|
872
|
+
});
|
|
646
873
|
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
874
|
+
if (result.status !== 0) {
|
|
875
|
+
const details = [result.stdout, result.stderr].filter(Boolean).join("\n").trim();
|
|
876
|
+
fail(`init planning failed${details ? `\n${details}` : ""}`);
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
return result.stdout || "";
|
|
650
880
|
}
|
|
651
881
|
|
|
652
|
-
function
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
return;
|
|
658
|
-
}
|
|
882
|
+
function initializeProject({ rootPath = process.cwd(), platform = "antigravity", idea = "" } = {}) {
|
|
883
|
+
const normalized = String(platform || "").toLowerCase();
|
|
884
|
+
if (!["antigravity", "codex"].includes(normalized)) {
|
|
885
|
+
fail(`unsupported init platform "${platform}". Use "antigravity" or "codex".`);
|
|
886
|
+
}
|
|
659
887
|
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
if (found) {
|
|
663
|
-
console.log(`\n\x1b[1m\x1b[32m[+] Opening mockup for ${found.title}:\x1b[0m ${found.fileName}`);
|
|
664
|
-
openFileNatively(found.fullPath);
|
|
665
|
-
return;
|
|
666
|
-
}
|
|
888
|
+
const platformLabel = normalized === "codex" ? "Codex / Claude (VS Code)" : "Antigravity IDE (Gemini)";
|
|
889
|
+
const isAntigravity = normalized === "antigravity";
|
|
667
890
|
|
|
668
|
-
|
|
669
|
-
|
|
891
|
+
console.log(`\n\x1b[1m\x1b[32m[+] Initializing Genesis Harness for ${platformLabel}...\x1b[0m\n`);
|
|
892
|
+
|
|
893
|
+
ensureProjectScaffold(rootPath);
|
|
894
|
+
|
|
895
|
+
if (!isAntigravity) {
|
|
896
|
+
console.log(" Copying local skills to .codex/skills/...");
|
|
897
|
+
copySkillsToProjectRoot(rootPath);
|
|
898
|
+
} else {
|
|
899
|
+
console.log(" Skipping local skills copy (Antigravity uses global plugin).");
|
|
670
900
|
}
|
|
671
901
|
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
902
|
+
seedLeanCtxPolicy(rootPath);
|
|
903
|
+
setupHooks(rootPath);
|
|
904
|
+
runInitPlanning(rootPath, idea);
|
|
905
|
+
|
|
906
|
+
console.log("\n\x1b[1m\x1b[32m✓ Initialization Complete.\x1b[0m");
|
|
907
|
+
console.log("Next steps:");
|
|
908
|
+
console.log(" 1. Answer `.planning/INIT_QA.md`.");
|
|
909
|
+
console.log(" 2. Confirm product approach, tech stack, and QA sign-off owner.");
|
|
910
|
+
console.log(" 3. Update `.planning/PROJECT.md`, `.planning/REQUIREMENTS.md`, and `.planning/STACK.md` before feature planning.\n");
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
function parseInitArgs(args) {
|
|
914
|
+
const options = {
|
|
915
|
+
autoConfirm: false,
|
|
916
|
+
platform: null,
|
|
917
|
+
idea: ""
|
|
918
|
+
};
|
|
919
|
+
|
|
920
|
+
for (let i = 0; i < args.length; i++) {
|
|
921
|
+
const arg = args[i];
|
|
922
|
+
if (arg === "--yes" || arg === "--confirmed") {
|
|
923
|
+
options.autoConfirm = true;
|
|
924
|
+
continue;
|
|
925
|
+
}
|
|
926
|
+
if (arg === "--platform") {
|
|
927
|
+
options.platform = args[i + 1] || null;
|
|
928
|
+
i++;
|
|
929
|
+
continue;
|
|
930
|
+
}
|
|
931
|
+
if (arg === "--idea") {
|
|
932
|
+
options.idea = args[i + 1] || "";
|
|
933
|
+
i++;
|
|
934
|
+
continue;
|
|
935
|
+
}
|
|
936
|
+
usage(2);
|
|
676
937
|
}
|
|
677
938
|
|
|
678
|
-
|
|
679
|
-
|
|
939
|
+
return options;
|
|
940
|
+
}
|
|
680
941
|
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
942
|
+
function parseRunArgs(args) {
|
|
943
|
+
const options = {
|
|
944
|
+
autoConfirm: false,
|
|
945
|
+
platform: null,
|
|
946
|
+
idea: "",
|
|
947
|
+
productApproach: "",
|
|
948
|
+
primaryUser: "",
|
|
949
|
+
v1Outcome: "",
|
|
950
|
+
qaOwner: "",
|
|
951
|
+
backend: "",
|
|
952
|
+
frontend: "",
|
|
953
|
+
database: "",
|
|
954
|
+
deployment: "",
|
|
955
|
+
testStrategy: "",
|
|
956
|
+
stackOwner: ""
|
|
957
|
+
};
|
|
686
958
|
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
959
|
+
const valueFlags = new Map([
|
|
960
|
+
["--platform", "platform"],
|
|
961
|
+
["--idea", "idea"],
|
|
962
|
+
["--product-approach", "productApproach"],
|
|
963
|
+
["--primary-user", "primaryUser"],
|
|
964
|
+
["--v1-outcome", "v1Outcome"],
|
|
965
|
+
["--qa-owner", "qaOwner"],
|
|
966
|
+
["--backend", "backend"],
|
|
967
|
+
["--frontend", "frontend"],
|
|
968
|
+
["--database", "database"],
|
|
969
|
+
["--deployment", "deployment"],
|
|
970
|
+
["--test-strategy", "testStrategy"],
|
|
971
|
+
["--stack-owner", "stackOwner"]
|
|
972
|
+
]);
|
|
692
973
|
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
}
|
|
699
|
-
});
|
|
700
|
-
console.log(" ------------------------------------------------------------------\n");
|
|
701
|
-
} else if (currentView === "DETAIL") {
|
|
702
|
-
const selected = mockups[selectedIndex];
|
|
703
|
-
console.log(" \x1b[1m\x1b[32m[+] LAUNCHED SYSTEM VIEW FOR:\x1b[0m \x1b[1m" + selected.title + "\x1b[0m\n");
|
|
704
|
-
console.log(` - \x1b[1mMockup File:\x1b[0m ${selected.fileName}`);
|
|
705
|
-
console.log(` - \x1b[1mFolder Path:\x1b[0m ${path.dirname(selected.fullPath)}`);
|
|
706
|
-
|
|
707
|
-
let sizeText = "Unknown";
|
|
708
|
-
try {
|
|
709
|
-
const stats = fs.statSync(selected.fullPath);
|
|
710
|
-
sizeText = `${(stats.size / 1024).toFixed(1)} KB`;
|
|
711
|
-
} catch (e) {}
|
|
712
|
-
console.log(` - \x1b[1mFile Size:\x1b[0m ${sizeText}`);
|
|
713
|
-
console.log("");
|
|
714
|
-
console.log(" ==================================================================");
|
|
715
|
-
console.log(" \x1b[36m[OS SYSTEM PREVIEW LAUNCHED]\x1b[0m");
|
|
716
|
-
console.log(" The mockup has been opened in your system's native image viewer.");
|
|
717
|
-
console.log(" ==================================================================\n");
|
|
718
|
-
console.log(" \x1b[1m\x1b[33m← Press Left Arrow to go BACK to list.\x1b[0m");
|
|
719
|
-
console.log(" \x1b[90mPress Esc or Ctrl+C to exit.\x1b[0m\n");
|
|
974
|
+
for (let i = 0; i < args.length; i++) {
|
|
975
|
+
const arg = args[i];
|
|
976
|
+
if (arg === "--yes" || arg === "--confirmed") {
|
|
977
|
+
options.autoConfirm = true;
|
|
978
|
+
continue;
|
|
720
979
|
}
|
|
721
|
-
|
|
980
|
+
if (valueFlags.has(arg)) {
|
|
981
|
+
const key = valueFlags.get(arg);
|
|
982
|
+
options[key] = args[i + 1] || "";
|
|
983
|
+
i++;
|
|
984
|
+
continue;
|
|
985
|
+
}
|
|
986
|
+
usage(2);
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
if (!options.idea) {
|
|
990
|
+
fail('run requires --idea "<user brief>".');
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
const requiredDiscoveryFields = [
|
|
994
|
+
["--product-approach", options.productApproach],
|
|
995
|
+
["--primary-user", options.primaryUser],
|
|
996
|
+
["--v1-outcome", options.v1Outcome],
|
|
997
|
+
["--qa-owner", options.qaOwner],
|
|
998
|
+
["--backend", options.backend],
|
|
999
|
+
["--frontend", options.frontend],
|
|
1000
|
+
["--database", options.database],
|
|
1001
|
+
["--deployment", options.deployment],
|
|
1002
|
+
["--test-strategy", options.testStrategy]
|
|
1003
|
+
].filter(([, value]) => !value);
|
|
1004
|
+
|
|
1005
|
+
if (requiredDiscoveryFields.length > 0) {
|
|
1006
|
+
fail(`run requires discovery answers for ${requiredDiscoveryFields.map(([flag]) => flag).join(", ")}.`);
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
return options;
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
function parseCompleteFeatureArgs(args) {
|
|
1013
|
+
const options = {
|
|
1014
|
+
verifyCmd: "",
|
|
1015
|
+
evidence: ""
|
|
722
1016
|
};
|
|
1017
|
+
const valueFlags = new Map([
|
|
1018
|
+
["--verify-cmd", "verifyCmd"],
|
|
1019
|
+
["--evidence", "evidence"]
|
|
1020
|
+
]);
|
|
723
1021
|
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
1022
|
+
for (let i = 0; i < args.length; i++) {
|
|
1023
|
+
const arg = args[i];
|
|
1024
|
+
if (!valueFlags.has(arg)) usage(2);
|
|
1025
|
+
options[valueFlags.get(arg)] = args[i + 1] || "";
|
|
1026
|
+
i++;
|
|
1027
|
+
}
|
|
727
1028
|
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
1029
|
+
if (!options.verifyCmd) fail('complete-feature requires --verify-cmd "<command>".');
|
|
1030
|
+
if (!options.evidence) fail('complete-feature requires --evidence "<summary>".');
|
|
1031
|
+
return options;
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
function parseAddFeatureArgs(args) {
|
|
1035
|
+
const options = {
|
|
1036
|
+
title: "",
|
|
1037
|
+
slug: "",
|
|
1038
|
+
verifyCmd: ""
|
|
734
1039
|
};
|
|
1040
|
+
const valueFlags = new Map([
|
|
1041
|
+
["--title", "title"],
|
|
1042
|
+
["--slug", "slug"],
|
|
1043
|
+
["--verify-cmd", "verifyCmd"]
|
|
1044
|
+
]);
|
|
735
1045
|
|
|
736
|
-
|
|
1046
|
+
for (let i = 0; i < args.length; i++) {
|
|
1047
|
+
const arg = args[i];
|
|
1048
|
+
if (!valueFlags.has(arg)) usage(2);
|
|
1049
|
+
options[valueFlags.get(arg)] = args[i + 1] || "";
|
|
1050
|
+
i++;
|
|
1051
|
+
}
|
|
737
1052
|
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
1053
|
+
if (!options.title) fail('add-feature requires --title "<title>".');
|
|
1054
|
+
if (!options.slug) fail('add-feature requires --slug "<slug>".');
|
|
1055
|
+
if (!options.verifyCmd) fail('add-feature requires --verify-cmd "<command>".');
|
|
1056
|
+
return options;
|
|
1057
|
+
}
|
|
742
1058
|
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
selectedIndex = (selectedIndex - 1 + mockups.length) % mockups.length;
|
|
746
|
-
renderMenu();
|
|
747
|
-
}
|
|
748
|
-
else if (key === "\u001b[B") {
|
|
749
|
-
selectedIndex = (selectedIndex + 1) % mockups.length;
|
|
750
|
-
renderMenu();
|
|
751
|
-
}
|
|
752
|
-
else if (key === "\u001b[C" || key === "\r") {
|
|
753
|
-
currentView = "DETAIL";
|
|
754
|
-
const selected = mockups[selectedIndex];
|
|
755
|
-
openFileNatively(selected.fullPath);
|
|
756
|
-
renderMenu();
|
|
757
|
-
}
|
|
758
|
-
} else if (currentView === "DETAIL") {
|
|
759
|
-
if (key === "\u001b[D") {
|
|
760
|
-
currentView = "LIST";
|
|
761
|
-
renderMenu();
|
|
762
|
-
}
|
|
763
|
-
}
|
|
764
|
-
});
|
|
1059
|
+
function parseProjectVerificationArgs(args) {
|
|
1060
|
+
return parseCompleteFeatureArgs(args);
|
|
765
1061
|
}
|
|
766
1062
|
|
|
767
|
-
|
|
768
|
-
const
|
|
1063
|
+
function parseProjectCompletionArgs(args) {
|
|
1064
|
+
const options = { evidence: "" };
|
|
1065
|
+
for (let i = 0; i < args.length; i++) {
|
|
1066
|
+
if (args[i] !== "--evidence") usage(2);
|
|
1067
|
+
options.evidence = args[i + 1] || "";
|
|
1068
|
+
i++;
|
|
1069
|
+
}
|
|
1070
|
+
if (!options.evidence) fail('complete-project requires --evidence "<summary>".');
|
|
1071
|
+
return options;
|
|
1072
|
+
}
|
|
769
1073
|
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
1074
|
+
function escapeRegExp(value) {
|
|
1075
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
function replaceSection(content, heading, replacement) {
|
|
1079
|
+
const pattern = new RegExp(`(## ${escapeRegExp(heading)}\\n\\n)([\\s\\S]*?)(?=\\n## |$)`);
|
|
1080
|
+
if (!pattern.test(content)) return content;
|
|
1081
|
+
return content.replace(pattern, `$1${replacement.trim()}\n`);
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
function replaceLineValue(content, label, value) {
|
|
1085
|
+
const pattern = new RegExp(`^${escapeRegExp(label)}: .*?$`, "m");
|
|
1086
|
+
if (!pattern.test(content)) return content;
|
|
1087
|
+
return content.replace(pattern, `${label}: ${value}`);
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
function writeFileIfChanged(filePath, content) {
|
|
1091
|
+
fs.writeFileSync(filePath, content, "utf8");
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
function writeJsonFile(filePath, value) {
|
|
1095
|
+
writeFileIfChanged(filePath, `${JSON.stringify(value, null, 2)}\n`);
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
function updateMarkdownFile(filePath, updater) {
|
|
1099
|
+
const current = fs.readFileSync(filePath, "utf8");
|
|
1100
|
+
const next = updater(current);
|
|
1101
|
+
writeFileIfChanged(filePath, next);
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
function normalizeAnswer(value, fallback = "TBD") {
|
|
1105
|
+
const normalized = String(value || "").trim();
|
|
1106
|
+
return normalized || fallback;
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
function sessionRunDir(rootPath, sessionId) {
|
|
1110
|
+
return path.join(rootPath, ".runs", sessionId);
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
function appendLifecycleEvent(rootPath, state, event) {
|
|
1114
|
+
const sessionId = state.session_id || "lifecycle";
|
|
1115
|
+
const runDir = sessionRunDir(rootPath, sessionId);
|
|
1116
|
+
fs.mkdirSync(runDir, { recursive: true });
|
|
1117
|
+
fs.appendFileSync(
|
|
1118
|
+
path.join(runDir, "EVENTS.jsonl"),
|
|
1119
|
+
`${JSON.stringify({
|
|
1120
|
+
timestamp: new Date().toISOString(),
|
|
1121
|
+
session_id: sessionId,
|
|
1122
|
+
...event
|
|
1123
|
+
})}\n`,
|
|
1124
|
+
"utf8"
|
|
1125
|
+
);
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
function runProofCommand(rootPath, command, label) {
|
|
1129
|
+
const result = spawnSync(command, {
|
|
1130
|
+
cwd: rootPath,
|
|
1131
|
+
env: process.env,
|
|
1132
|
+
shell: true,
|
|
1133
|
+
stdio: "inherit"
|
|
1134
|
+
});
|
|
1135
|
+
if (result.error) {
|
|
1136
|
+
return { ok: false, message: `${label} could not start: ${result.error.message}` };
|
|
1137
|
+
}
|
|
1138
|
+
if (result.status !== 0) {
|
|
1139
|
+
return { ok: false, message: `${label} failed with exit ${result.status}.` };
|
|
1140
|
+
}
|
|
1141
|
+
return { ok: true, message: `${label} passed.` };
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
function writeLifecycleCurrentState(rootPath, state, details) {
|
|
1145
|
+
const now = state.last_updated_at || new Date().toISOString();
|
|
1146
|
+
writeFileIfChanged(
|
|
1147
|
+
path.join(rootPath, ".codebase", "CURRENT_STATE.md"),
|
|
1148
|
+
[
|
|
1149
|
+
"# Current System State",
|
|
1150
|
+
"",
|
|
1151
|
+
`**Time**: ${now.slice(0, 10)}`,
|
|
1152
|
+
`**Status**: \`${state.current_state}\``,
|
|
1153
|
+
`**Latest Session**: \`${state.session_id || "lifecycle"}\``,
|
|
1154
|
+
"",
|
|
1155
|
+
"## Lifecycle",
|
|
1156
|
+
"",
|
|
1157
|
+
...details.map(detail => `- ${detail}`),
|
|
1158
|
+
""
|
|
1159
|
+
].join("\n")
|
|
1160
|
+
);
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
function writeLifecycleRunRecord(rootPath, state, record, aliases = []) {
|
|
1164
|
+
const observabilityDir = path.join(rootPath, "observability", "agent-runs");
|
|
1165
|
+
fs.mkdirSync(observabilityDir, { recursive: true });
|
|
1166
|
+
const sessionId = state.session_id || record.phase || "lifecycle";
|
|
1167
|
+
const payload = {
|
|
1168
|
+
session_id: sessionId,
|
|
1169
|
+
timestamp: record.timestamp || new Date().toISOString(),
|
|
1170
|
+
skill: "genesis-pipeline-orchestration",
|
|
1171
|
+
recovery_needed: false,
|
|
1172
|
+
...record
|
|
1173
|
+
};
|
|
1174
|
+
writeJsonFile(path.join(observabilityDir, `${sessionId}-${record.id}.json`), payload);
|
|
1175
|
+
for (const alias of aliases) {
|
|
1176
|
+
writeJsonFile(path.join(observabilityDir, `${sessionId}-${alias}.json`), payload);
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
function buildResumeMarkdown({ sessionId, state, answers, artifactDir }) {
|
|
1181
|
+
const nextTasks = (state.pending_tasks || []).map(task => `- ${task}`);
|
|
1182
|
+
const answerLines = [
|
|
1183
|
+
`- Product approach: ${answers.product_approach || "TBD"}`,
|
|
1184
|
+
`- Primary user: ${answers.primary_user || "TBD"}`,
|
|
1185
|
+
`- V1 outcome: ${answers.v1_outcome || "TBD"}`,
|
|
1186
|
+
`- QA owner: ${answers.qa_owner || "TBD"}`,
|
|
1187
|
+
`- Backend/runtime: ${answers.backend || "TBD"}`,
|
|
1188
|
+
`- Frontend/client: ${answers.frontend || "TBD"}`,
|
|
1189
|
+
`- Database: ${answers.database || "TBD"}`,
|
|
1190
|
+
`- Deployment: ${answers.deployment || "TBD"}`,
|
|
1191
|
+
`- Test strategy: ${answers.test_strategy || "TBD"}`,
|
|
1192
|
+
`- Stack owner: ${answers.stack_owner || "TBD"}`
|
|
1193
|
+
];
|
|
1194
|
+
|
|
1195
|
+
return [
|
|
1196
|
+
"# Resume Brief",
|
|
1197
|
+
"",
|
|
1198
|
+
`- Session: \`${sessionId}\``,
|
|
1199
|
+
`- Current state: \`${state.current_state || "INIT"}\``,
|
|
1200
|
+
`- Active work: ${state.active_work || "TBD"}`,
|
|
1201
|
+
`- Active feature: ${state.active_feature || "None"}`,
|
|
1202
|
+
`- Artifact dir: \`${artifactDir}\``,
|
|
1203
|
+
"",
|
|
1204
|
+
"## Discovery Snapshot",
|
|
1205
|
+
"",
|
|
1206
|
+
...answerLines,
|
|
1207
|
+
"",
|
|
1208
|
+
"## Next Tasks",
|
|
1209
|
+
"",
|
|
1210
|
+
...(nextTasks.length > 0 ? nextTasks : ["- No pending tasks recorded."]),
|
|
1211
|
+
""
|
|
1212
|
+
].join("\n");
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
function persistRunArtifacts(rootPath, { sessionId, state, answers, idea, recordedAt }) {
|
|
1216
|
+
if (!sessionId) {
|
|
1217
|
+
fail("cannot persist run artifacts without session_id");
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
const runDir = sessionRunDir(rootPath, sessionId);
|
|
1221
|
+
fs.mkdirSync(runDir, { recursive: true });
|
|
1222
|
+
|
|
1223
|
+
const discovery = {
|
|
1224
|
+
session_id: sessionId,
|
|
1225
|
+
recorded_at: recordedAt || new Date().toISOString(),
|
|
1226
|
+
idea: idea || "",
|
|
1227
|
+
...answers
|
|
1228
|
+
};
|
|
1229
|
+
|
|
1230
|
+
const artifactState = {
|
|
1231
|
+
session_id: sessionId,
|
|
1232
|
+
current_state: state.current_state || "INIT",
|
|
1233
|
+
active_work: state.active_work || "",
|
|
1234
|
+
active_feature: state.active_feature || "",
|
|
1235
|
+
pending_tasks: state.pending_tasks || [],
|
|
1236
|
+
required_verification: state.required_verification || [],
|
|
1237
|
+
latest_recovery_point: state.latest_recovery_point || "",
|
|
1238
|
+
session_started_at: state.session_started_at || discovery.recorded_at,
|
|
1239
|
+
completed_at: state.completed_at || "",
|
|
1240
|
+
metrics: state.metrics || {},
|
|
1241
|
+
recorded_at: discovery.recorded_at
|
|
1242
|
+
};
|
|
1243
|
+
|
|
1244
|
+
writeFileIfChanged(
|
|
1245
|
+
path.join(runDir, "INPUT.md"),
|
|
1246
|
+
[
|
|
1247
|
+
"# Run Input",
|
|
1248
|
+
"",
|
|
1249
|
+
`- Session: \`${sessionId}\``,
|
|
1250
|
+
`- Recorded at: ${discovery.recorded_at}`,
|
|
1251
|
+
"",
|
|
1252
|
+
"## User Brief",
|
|
1253
|
+
"",
|
|
1254
|
+
idea || "No explicit user brief captured.",
|
|
1255
|
+
""
|
|
1256
|
+
].join("\n")
|
|
1257
|
+
);
|
|
1258
|
+
writeJsonFile(path.join(runDir, "DISCOVERY.json"), discovery);
|
|
1259
|
+
writeJsonFile(path.join(runDir, "STATE.json"), artifactState);
|
|
1260
|
+
writeFileIfChanged(
|
|
1261
|
+
path.join(runDir, "RESUME.md"),
|
|
1262
|
+
buildResumeMarkdown({
|
|
1263
|
+
sessionId,
|
|
1264
|
+
state: artifactState,
|
|
1265
|
+
answers: discovery,
|
|
1266
|
+
artifactDir: runDir
|
|
1267
|
+
})
|
|
1268
|
+
);
|
|
1269
|
+
|
|
1270
|
+
return runDir;
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
function backfillRunArtifacts(rootPath, state) {
|
|
1274
|
+
const sessionId = state.session_id;
|
|
1275
|
+
if (!sessionId) {
|
|
1276
|
+
fail("cannot resume because .codebase/state.json is missing session_id");
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
const discoveryAnswers = state.discovery_answers || {};
|
|
1280
|
+
persistRunArtifacts(rootPath, {
|
|
1281
|
+
sessionId,
|
|
1282
|
+
state,
|
|
1283
|
+
answers: discoveryAnswers,
|
|
1284
|
+
idea: discoveryAnswers.idea || "",
|
|
1285
|
+
recordedAt: discoveryAnswers.captured_at || state.session_started_at || new Date().toISOString()
|
|
1286
|
+
});
|
|
1287
|
+
|
|
1288
|
+
return sessionRunDir(rootPath, sessionId);
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
function resumeProject(rootPath = process.cwd()) {
|
|
1292
|
+
const statePath = path.join(rootPath, ".codebase", "state.json");
|
|
1293
|
+
if (!fs.existsSync(statePath)) {
|
|
1294
|
+
fail(`missing state file at ${statePath}; run init or run first.`);
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
const state = JSON.parse(fs.readFileSync(statePath, "utf8"));
|
|
1298
|
+
const sessionId = state.session_id;
|
|
1299
|
+
if (!sessionId) {
|
|
1300
|
+
fail("cannot resume because .codebase/state.json does not record session_id");
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
const runDir = fs.existsSync(sessionRunDir(rootPath, sessionId))
|
|
1304
|
+
? sessionRunDir(rootPath, sessionId)
|
|
1305
|
+
: backfillRunArtifacts(rootPath, state);
|
|
1306
|
+
const artifactStatePath = path.join(runDir, "STATE.json");
|
|
1307
|
+
const artifactDiscoveryPath = path.join(runDir, "DISCOVERY.json");
|
|
1308
|
+
const artifactState = fs.existsSync(artifactStatePath)
|
|
1309
|
+
? JSON.parse(fs.readFileSync(artifactStatePath, "utf8"))
|
|
1310
|
+
: state;
|
|
1311
|
+
const artifactDiscovery = fs.existsSync(artifactDiscoveryPath)
|
|
1312
|
+
? JSON.parse(fs.readFileSync(artifactDiscoveryPath, "utf8"))
|
|
1313
|
+
: (state.discovery_answers || {});
|
|
1314
|
+
|
|
1315
|
+
const nextTasks = artifactState.pending_tasks || [];
|
|
1316
|
+
console.log("\nGENESIS HARNESS - RESUME REPORT\n");
|
|
1317
|
+
console.log(`Resume session: ${sessionId}`);
|
|
1318
|
+
console.log(`Current state: ${artifactState.current_state || state.current_state || "INIT"}`);
|
|
1319
|
+
console.log(`Active work: ${artifactState.active_work || state.active_work || "TBD"}`);
|
|
1320
|
+
console.log(`Active feature: ${artifactState.active_feature || state.active_feature || "None"}`);
|
|
1321
|
+
console.log(`Artifact dir: ${runDir}`);
|
|
1322
|
+
console.log(`Primary user: ${artifactDiscovery.primary_user || "TBD"}`);
|
|
1323
|
+
console.log(`Product approach: ${artifactDiscovery.product_approach || "TBD"}`);
|
|
1324
|
+
console.log("Next tasks:");
|
|
1325
|
+
if (nextTasks.length === 0) {
|
|
1326
|
+
console.log(" - No pending tasks recorded.");
|
|
1327
|
+
} else {
|
|
1328
|
+
for (const task of nextTasks) {
|
|
1329
|
+
console.log(` - ${task}`);
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
console.log("");
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
function readProjectLifecycle(rootPath) {
|
|
1336
|
+
const statePath = path.join(rootPath, ".codebase", "state.json");
|
|
1337
|
+
const registryPath = path.join(rootPath, ".planning", "FEATURE_REGISTRY.json");
|
|
1338
|
+
if (!fs.existsSync(statePath)) {
|
|
1339
|
+
fail(`missing state file at ${statePath}; run genesis-harness run first.`);
|
|
1340
|
+
}
|
|
1341
|
+
if (!fs.existsSync(registryPath)) {
|
|
1342
|
+
fail(`missing feature registry at ${registryPath}; run genesis-harness run first.`);
|
|
1343
|
+
}
|
|
1344
|
+
const state = JSON.parse(fs.readFileSync(statePath, "utf8"));
|
|
1345
|
+
const registry = JSON.parse(fs.readFileSync(registryPath, "utf8"));
|
|
1346
|
+
registry.project_status = registry.project_status
|
|
1347
|
+
|| (state.current_state === "COMPLETED" ? "completed" : "implementation");
|
|
1348
|
+
registry.project_verification = registry.project_verification || {
|
|
1349
|
+
status: "pending",
|
|
1350
|
+
verify_cmd: "",
|
|
1351
|
+
evidence: "",
|
|
1352
|
+
verified_at: ""
|
|
1353
|
+
};
|
|
1354
|
+
registry.features = (registry.features || []).map(feature => ({
|
|
1355
|
+
attempts: 0,
|
|
1356
|
+
last_error: "",
|
|
1357
|
+
...feature
|
|
1358
|
+
}));
|
|
1359
|
+
return {
|
|
1360
|
+
statePath,
|
|
1361
|
+
registryPath,
|
|
1362
|
+
state,
|
|
1363
|
+
registry
|
|
1364
|
+
};
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
function showNextAction(rootPath = process.cwd()) {
|
|
1368
|
+
const { state, registry } = readProjectLifecycle(rootPath);
|
|
1369
|
+
const active = registry.features.find(feature => feature.status === "in-progress")
|
|
1370
|
+
|| registry.features.find(feature => feature.status === "planned");
|
|
1371
|
+
const nextTask = (state.pending_tasks || [])[0];
|
|
1372
|
+
|
|
1373
|
+
console.log("\nGENESIS HARNESS - NEXT ACTION\n");
|
|
1374
|
+
if (!active) {
|
|
1375
|
+
if (state.current_state === "VERIFICATION") {
|
|
1376
|
+
console.log("Next action: Run genesis-harness verify-project.");
|
|
1377
|
+
} else if (state.current_state === "RELEASE_READY") {
|
|
1378
|
+
console.log("Next action: Run genesis-harness complete-project.");
|
|
1379
|
+
} else {
|
|
1380
|
+
console.log("No planned or in-progress feature remains.");
|
|
1381
|
+
}
|
|
1382
|
+
return;
|
|
1383
|
+
}
|
|
1384
|
+
console.log(`Feature: ${active.title}`);
|
|
1385
|
+
console.log(`Path: ${active.path}`);
|
|
1386
|
+
console.log(`Status: ${active.status}`);
|
|
1387
|
+
console.log(`Next action: ${nextTask || "Run feature verification and complete the feature."}`);
|
|
1388
|
+
console.log("");
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
function addFeature(rootPath, options) {
|
|
1392
|
+
const { statePath, registryPath, state, registry } = readProjectLifecycle(rootPath);
|
|
1393
|
+
if (["RELEASE_READY", "COMPLETED"].includes(state.current_state)) {
|
|
1394
|
+
fail(`cannot add a feature while project state is ${state.current_state}.`);
|
|
1395
|
+
}
|
|
1396
|
+
if (registry.features.some(feature => feature.title === options.title)) {
|
|
1397
|
+
console.log(`Feature already queued: ${options.title}`);
|
|
1398
|
+
return;
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
const featureRelativePath = createFeatureScaffold(rootPath, {
|
|
1402
|
+
slug: slugifyFeature(options.slug),
|
|
1403
|
+
summary: options.title
|
|
1404
|
+
});
|
|
1405
|
+
const now = new Date().toISOString();
|
|
1406
|
+
const nextId = `F${String(registry.features.length + 1).padStart(3, "0")}`;
|
|
1407
|
+
registry.features.push({
|
|
1408
|
+
id: nextId,
|
|
1409
|
+
status: "planned",
|
|
1410
|
+
title: options.title,
|
|
1411
|
+
path: featureRelativePath,
|
|
1412
|
+
verify_cmd: options.verifyCmd,
|
|
1413
|
+
evidence: "",
|
|
1414
|
+
started_at: "",
|
|
1415
|
+
verified_at: "",
|
|
1416
|
+
attempts: 0,
|
|
1417
|
+
last_error: ""
|
|
1418
|
+
});
|
|
1419
|
+
registry.project_status = "implementation";
|
|
1420
|
+
registry.updated_at = now;
|
|
1421
|
+
writeJsonFile(registryPath, registry);
|
|
1422
|
+
|
|
1423
|
+
const featureIndexPath = path.join(rootPath, ".planning", "FEATURE_INDEX.md");
|
|
1424
|
+
if (fs.existsSync(featureIndexPath)) {
|
|
1425
|
+
updateMarkdownFile(featureIndexPath, content => {
|
|
1426
|
+
const row = `| ${options.title} | [ ] | Queue | ${featureRelativePath.replace(".planning/", "")} | Planned feature |`;
|
|
1427
|
+
return content.includes(`| ${options.title} |`) ? content : `${content.trim()}\n${row}\n`;
|
|
1428
|
+
});
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
state.last_updated_at = now;
|
|
1432
|
+
state.pending_tasks = state.pending_tasks || [];
|
|
1433
|
+
writeJsonFile(statePath, state);
|
|
1434
|
+
appendLifecycleEvent(rootPath, state, {
|
|
1435
|
+
type: "feature.queued",
|
|
1436
|
+
feature_id: nextId,
|
|
1437
|
+
feature_path: featureRelativePath
|
|
1438
|
+
});
|
|
1439
|
+
persistRunArtifacts(rootPath, {
|
|
1440
|
+
sessionId: state.session_id || "lifecycle",
|
|
1441
|
+
state,
|
|
1442
|
+
answers: state.discovery_answers || {},
|
|
1443
|
+
idea: (state.discovery_answers && state.discovery_answers.idea) || "",
|
|
1444
|
+
recordedAt: now
|
|
1445
|
+
});
|
|
1446
|
+
console.log(`Feature queued: ${options.title}`);
|
|
1447
|
+
console.log(`Path: ${featureRelativePath}`);
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
function completeFeature(rootPath, options) {
|
|
1451
|
+
const { statePath, registryPath, state, registry } = readProjectLifecycle(rootPath);
|
|
1452
|
+
const previousState = state.current_state || "IMPLEMENTATION";
|
|
1453
|
+
const active = registry.features.find(feature => feature.path === state.active_feature)
|
|
1454
|
+
|| registry.features.find(feature => feature.status === "in-progress");
|
|
1455
|
+
if (!active) fail("no in-progress feature is available to complete.");
|
|
1456
|
+
|
|
1457
|
+
const verificationStartedAt = Date.now();
|
|
1458
|
+
active.attempts = (active.attempts || 0) + 1;
|
|
1459
|
+
const verification = runProofCommand(rootPath, options.verifyCmd, `feature ${active.id} verification`);
|
|
1460
|
+
if (!verification.ok) {
|
|
1461
|
+
active.last_error = verification.message;
|
|
1462
|
+
registry.updated_at = new Date().toISOString();
|
|
1463
|
+
writeJsonFile(registryPath, registry);
|
|
1464
|
+
state.metrics = state.metrics || {};
|
|
1465
|
+
state.metrics.failed_gate_count = (state.metrics.failed_gate_count || 0) + 1;
|
|
1466
|
+
state.last_updated_at = new Date().toISOString();
|
|
1467
|
+
writeJsonFile(statePath, state);
|
|
1468
|
+
appendLifecycleEvent(rootPath, state, {
|
|
1469
|
+
type: "feature.verification_failed",
|
|
1470
|
+
feature_id: active.id,
|
|
1471
|
+
error: verification.message
|
|
1472
|
+
});
|
|
1473
|
+
fail(verification.message);
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
const now = new Date().toISOString();
|
|
1477
|
+
const startedAt = Date.parse(active.started_at || state.session_started_at || now);
|
|
1478
|
+
const leadTimeSeconds = Math.max(0, Math.round((Date.now() - startedAt) / 1000));
|
|
1479
|
+
active.status = "verified";
|
|
1480
|
+
active.verify_cmd = options.verifyCmd;
|
|
1481
|
+
active.evidence = options.evidence;
|
|
1482
|
+
active.verified_at = now;
|
|
1483
|
+
active.last_error = "";
|
|
1484
|
+
const nextFeature = registry.features.find(feature => feature.status === "planned");
|
|
1485
|
+
if (nextFeature) {
|
|
1486
|
+
nextFeature.status = "in-progress";
|
|
1487
|
+
nextFeature.started_at = now;
|
|
1488
|
+
registry.project_status = "implementation";
|
|
1489
|
+
state.current_state = "IMPLEMENTATION";
|
|
1490
|
+
state.active_work = `Implement ${nextFeature.title}`;
|
|
1491
|
+
state.active_feature = nextFeature.path;
|
|
1492
|
+
state.pending_tasks = [
|
|
1493
|
+
`Add the first failing test for ${nextFeature.title}`,
|
|
1494
|
+
`Implement ${nextFeature.title}`,
|
|
1495
|
+
"Run feature verification and record evidence"
|
|
1496
|
+
];
|
|
1497
|
+
} else {
|
|
1498
|
+
registry.project_status = "verification";
|
|
1499
|
+
state.current_state = "VERIFICATION";
|
|
1500
|
+
state.active_work = "Project verification";
|
|
1501
|
+
state.active_feature = "";
|
|
1502
|
+
state.pending_tasks = ["Run project verification", "Prepare final implementation handoff"];
|
|
1503
|
+
}
|
|
1504
|
+
registry.updated_at = now;
|
|
1505
|
+
writeJsonFile(registryPath, registry);
|
|
1506
|
+
|
|
1507
|
+
const featureIndexPath = path.join(rootPath, ".planning", "FEATURE_INDEX.md");
|
|
1508
|
+
if (fs.existsSync(featureIndexPath)) {
|
|
1509
|
+
updateMarkdownFile(featureIndexPath, content =>
|
|
1510
|
+
content.replace(
|
|
1511
|
+
new RegExp(`\\| ${escapeRegExp(active.title)} \\| \\[~\\] \\|`),
|
|
1512
|
+
`| ${active.title} | [x] |`
|
|
1513
|
+
)
|
|
1514
|
+
);
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
const verificationPath = path.join(rootPath, active.path, "VERIFICATION.md");
|
|
1518
|
+
if (fs.existsSync(verificationPath)) {
|
|
1519
|
+
updateMarkdownFile(verificationPath, content => [
|
|
1520
|
+
content.trim(),
|
|
1521
|
+
"",
|
|
1522
|
+
"## Completion Evidence",
|
|
1523
|
+
"",
|
|
1524
|
+
`- Verified at: ${now}`,
|
|
1525
|
+
`- Command: \`${options.verifyCmd}\``,
|
|
1526
|
+
`- Evidence: ${options.evidence}`,
|
|
1527
|
+
""
|
|
1528
|
+
].join("\n"));
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1531
|
+
state.history = state.history || [];
|
|
1532
|
+
state.history.push({
|
|
1533
|
+
from: previousState,
|
|
1534
|
+
to: nextFeature ? "IMPLEMENTATION" : "VERIFICATION",
|
|
1535
|
+
reason: `Verified feature: ${active.title}`,
|
|
1536
|
+
timestamp: now,
|
|
1537
|
+
session_id: state.session_id || "feature-completion"
|
|
1538
|
+
});
|
|
1539
|
+
state.last_updated_at = now;
|
|
1540
|
+
state.latest_recovery_point = `Feature verified: ${active.title}`;
|
|
1541
|
+
state.metrics = {
|
|
1542
|
+
...(state.metrics || {}),
|
|
1543
|
+
time_to_verified_feature_seconds: leadTimeSeconds,
|
|
1544
|
+
last_verification_duration_ms: Date.now() - verificationStartedAt,
|
|
1545
|
+
failed_gate_count: (state.metrics && state.metrics.failed_gate_count) || 0
|
|
1546
|
+
};
|
|
1547
|
+
writeJsonFile(statePath, state);
|
|
1548
|
+
|
|
1549
|
+
writeLifecycleCurrentState(rootPath, state, [
|
|
1550
|
+
`Verified feature: ${active.title}`,
|
|
1551
|
+
`Evidence: ${options.evidence}`,
|
|
1552
|
+
nextFeature
|
|
1553
|
+
? `Promoted next feature: ${nextFeature.title}`
|
|
1554
|
+
: "All queued features are verified; project verification is next."
|
|
1555
|
+
]);
|
|
1556
|
+
|
|
1557
|
+
writeLifecycleRunRecord(
|
|
1558
|
+
rootPath,
|
|
1559
|
+
state,
|
|
1560
|
+
{
|
|
1561
|
+
id: `${active.id}-complete`,
|
|
1562
|
+
timestamp: now,
|
|
1563
|
+
phase: "verify",
|
|
1564
|
+
outcome: "success",
|
|
1565
|
+
evidence: options.evidence,
|
|
1566
|
+
task_id: active.id,
|
|
1567
|
+
duration_ms: Date.now() - verificationStartedAt,
|
|
1568
|
+
metrics: state.metrics
|
|
1569
|
+
},
|
|
1570
|
+
["feature-complete"]
|
|
1571
|
+
);
|
|
1572
|
+
|
|
1573
|
+
appendLifecycleEvent(rootPath, state, {
|
|
1574
|
+
type: "feature.verified",
|
|
1575
|
+
feature_id: active.id,
|
|
1576
|
+
evidence: options.evidence,
|
|
1577
|
+
next_feature_id: nextFeature ? nextFeature.id : ""
|
|
1578
|
+
});
|
|
1579
|
+
persistRunArtifacts(rootPath, {
|
|
1580
|
+
sessionId: state.session_id || "feature-completion",
|
|
1581
|
+
state,
|
|
1582
|
+
answers: state.discovery_answers || {},
|
|
1583
|
+
idea: (state.discovery_answers && state.discovery_answers.idea) || "",
|
|
1584
|
+
recordedAt: now
|
|
1585
|
+
});
|
|
1586
|
+
|
|
1587
|
+
console.log(`Feature completed: ${active.title}`);
|
|
1588
|
+
console.log(`Evidence: ${options.evidence}`);
|
|
1589
|
+
console.log(nextFeature ? `Next feature: ${nextFeature.title}` : "Next stage: project verification");
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
function verifyProject(rootPath, options) {
|
|
1593
|
+
const { statePath, registryPath, state, registry } = readProjectLifecycle(rootPath);
|
|
1594
|
+
if (state.current_state === "RELEASE_READY" && registry.project_verification.status === "passed") {
|
|
1595
|
+
console.log("Project already verified and release-ready.");
|
|
1596
|
+
return;
|
|
1597
|
+
}
|
|
1598
|
+
if (state.current_state !== "VERIFICATION") {
|
|
1599
|
+
fail(`verify-project requires project state VERIFICATION, found ${state.current_state}.`);
|
|
1600
|
+
}
|
|
1601
|
+
const incomplete = registry.features.filter(feature => feature.status !== "verified");
|
|
1602
|
+
if (incomplete.length > 0) {
|
|
1603
|
+
fail(`verify-project blocked by unverified features: ${incomplete.map(feature => feature.id).join(", ")}.`);
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1606
|
+
const startedAt = Date.now();
|
|
1607
|
+
for (const feature of registry.features) {
|
|
1608
|
+
const result = runProofCommand(rootPath, feature.verify_cmd, `feature ${feature.id} proof`);
|
|
1609
|
+
if (!result.ok) {
|
|
1610
|
+
state.metrics = state.metrics || {};
|
|
1611
|
+
state.metrics.failed_gate_count = (state.metrics.failed_gate_count || 0) + 1;
|
|
1612
|
+
state.last_updated_at = new Date().toISOString();
|
|
1613
|
+
writeJsonFile(statePath, state);
|
|
1614
|
+
appendLifecycleEvent(rootPath, state, {
|
|
1615
|
+
type: "project.verification_failed",
|
|
1616
|
+
feature_id: feature.id,
|
|
1617
|
+
error: result.message
|
|
1618
|
+
});
|
|
1619
|
+
fail(result.message);
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
const projectProof = runProofCommand(rootPath, options.verifyCmd, "project verification");
|
|
1623
|
+
if (!projectProof.ok) {
|
|
1624
|
+
state.metrics = state.metrics || {};
|
|
1625
|
+
state.metrics.failed_gate_count = (state.metrics.failed_gate_count || 0) + 1;
|
|
1626
|
+
state.last_updated_at = new Date().toISOString();
|
|
1627
|
+
writeJsonFile(statePath, state);
|
|
1628
|
+
appendLifecycleEvent(rootPath, state, {
|
|
1629
|
+
type: "project.verification_failed",
|
|
1630
|
+
error: projectProof.message
|
|
1631
|
+
});
|
|
1632
|
+
fail(projectProof.message);
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
const now = new Date().toISOString();
|
|
1636
|
+
registry.project_status = "release-ready";
|
|
1637
|
+
registry.project_verification = {
|
|
1638
|
+
status: "passed",
|
|
1639
|
+
verify_cmd: options.verifyCmd,
|
|
1640
|
+
evidence: options.evidence,
|
|
1641
|
+
verified_at: now
|
|
1642
|
+
};
|
|
1643
|
+
registry.updated_at = now;
|
|
1644
|
+
writeJsonFile(registryPath, registry);
|
|
1645
|
+
|
|
1646
|
+
writeJsonFile(path.join(rootPath, ".planning", "PROJECT_VERIFICATION.json"), {
|
|
1647
|
+
status: "passed",
|
|
1648
|
+
verified_at: now,
|
|
1649
|
+
feature_count: registry.features.length,
|
|
1650
|
+
feature_proofs: registry.features.map(feature => ({
|
|
1651
|
+
id: feature.id,
|
|
1652
|
+
verify_cmd: feature.verify_cmd,
|
|
1653
|
+
evidence: feature.evidence,
|
|
1654
|
+
verified_at: feature.verified_at
|
|
1655
|
+
})),
|
|
1656
|
+
project_verify_cmd: options.verifyCmd,
|
|
1657
|
+
evidence: options.evidence
|
|
1658
|
+
});
|
|
1659
|
+
writeFileIfChanged(
|
|
1660
|
+
path.join(rootPath, ".planning", "IMPLEMENTATION_HANDOFF.md"),
|
|
1661
|
+
[
|
|
1662
|
+
"# Implementation Handoff",
|
|
1663
|
+
"",
|
|
1664
|
+
`- Status: Release ready`,
|
|
1665
|
+
`- Verified at: ${now}`,
|
|
1666
|
+
`- Features verified: ${registry.features.length}`,
|
|
1667
|
+
`- Project evidence: ${options.evidence}`,
|
|
1668
|
+
`- Project proof command: \`${options.verifyCmd}\``,
|
|
1669
|
+
"",
|
|
1670
|
+
"## Verified Features",
|
|
1671
|
+
"",
|
|
1672
|
+
...registry.features.map(feature => `- [x] ${feature.id}: ${feature.title} - ${feature.evidence}`),
|
|
1673
|
+
"",
|
|
1674
|
+
"## Next Action",
|
|
1675
|
+
"",
|
|
1676
|
+
"- Run `genesis-harness complete-project --evidence \"<release or acceptance evidence>\"`.",
|
|
1677
|
+
""
|
|
1678
|
+
].join("\n")
|
|
1679
|
+
);
|
|
1680
|
+
|
|
1681
|
+
state.history = state.history || [];
|
|
1682
|
+
state.history.push({
|
|
1683
|
+
from: "VERIFICATION",
|
|
1684
|
+
to: "RELEASE_READY",
|
|
1685
|
+
reason: "Project verification passed",
|
|
1686
|
+
timestamp: now,
|
|
1687
|
+
session_id: state.session_id || "project-verification"
|
|
1688
|
+
});
|
|
1689
|
+
state.current_state = "RELEASE_READY";
|
|
1690
|
+
state.active_work = "Release readiness";
|
|
1691
|
+
state.pending_tasks = ["Complete project with release or acceptance evidence"];
|
|
1692
|
+
state.last_updated_at = now;
|
|
1693
|
+
state.latest_handoff = ".planning/IMPLEMENTATION_HANDOFF.md";
|
|
1694
|
+
state.metrics = {
|
|
1695
|
+
...(state.metrics || {}),
|
|
1696
|
+
project_verification_duration_ms: Date.now() - startedAt
|
|
1697
|
+
};
|
|
1698
|
+
writeJsonFile(statePath, state);
|
|
1699
|
+
writeLifecycleCurrentState(rootPath, state, [
|
|
1700
|
+
"All feature proof commands passed.",
|
|
1701
|
+
`Project evidence: ${options.evidence}`,
|
|
1702
|
+
"Final completion is awaiting release or acceptance evidence."
|
|
1703
|
+
]);
|
|
1704
|
+
appendLifecycleEvent(rootPath, state, {
|
|
1705
|
+
type: "project.verified",
|
|
1706
|
+
evidence: options.evidence
|
|
1707
|
+
});
|
|
1708
|
+
writeLifecycleRunRecord(rootPath, state, {
|
|
1709
|
+
id: "project-verified",
|
|
1710
|
+
timestamp: now,
|
|
1711
|
+
phase: "verify",
|
|
1712
|
+
outcome: "success",
|
|
1713
|
+
evidence: options.evidence,
|
|
1714
|
+
task_id: "PROJECT",
|
|
1715
|
+
duration_ms: Date.now() - startedAt,
|
|
1716
|
+
metrics: state.metrics
|
|
1717
|
+
});
|
|
1718
|
+
persistRunArtifacts(rootPath, {
|
|
1719
|
+
sessionId: state.session_id || "project-verification",
|
|
1720
|
+
state,
|
|
1721
|
+
answers: state.discovery_answers || {},
|
|
1722
|
+
idea: (state.discovery_answers && state.discovery_answers.idea) || "",
|
|
1723
|
+
recordedAt: now
|
|
1724
|
+
});
|
|
1725
|
+
console.log("Project verified: all feature and project proof commands passed.");
|
|
1726
|
+
console.log("State: RELEASE_READY");
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
function completeProject(rootPath, options) {
|
|
1730
|
+
const { statePath, registryPath, state, registry } = readProjectLifecycle(rootPath);
|
|
1731
|
+
if (state.current_state === "COMPLETED" && registry.project_status === "completed") {
|
|
1732
|
+
console.log("Project already completed.");
|
|
1733
|
+
return;
|
|
1734
|
+
}
|
|
1735
|
+
if (state.current_state !== "RELEASE_READY") {
|
|
1736
|
+
fail(`complete-project requires project state RELEASE_READY, found ${state.current_state}.`);
|
|
1737
|
+
}
|
|
1738
|
+
if (registry.project_verification.status !== "passed") {
|
|
1739
|
+
fail("complete-project requires passed project verification.");
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
const now = new Date().toISOString();
|
|
1743
|
+
registry.project_status = "completed";
|
|
1744
|
+
registry.completed_at = now;
|
|
1745
|
+
registry.completion_evidence = options.evidence;
|
|
1746
|
+
registry.updated_at = now;
|
|
1747
|
+
writeJsonFile(registryPath, registry);
|
|
1748
|
+
|
|
1749
|
+
state.history = state.history || [];
|
|
1750
|
+
state.history.push({
|
|
1751
|
+
from: "RELEASE_READY",
|
|
1752
|
+
to: "COMPLETED",
|
|
1753
|
+
reason: options.evidence,
|
|
1754
|
+
timestamp: now,
|
|
1755
|
+
session_id: state.session_id || "project-completion"
|
|
1756
|
+
});
|
|
1757
|
+
state.current_state = "COMPLETED";
|
|
1758
|
+
state.completed_at = now;
|
|
1759
|
+
state.active_work = "";
|
|
1760
|
+
state.active_feature = "";
|
|
1761
|
+
state.pending_tasks = [];
|
|
1762
|
+
state.last_updated_at = now;
|
|
1763
|
+
state.latest_recovery_point = "Project completed from release-ready state";
|
|
1764
|
+
writeJsonFile(statePath, state);
|
|
1765
|
+
writeLifecycleCurrentState(rootPath, state, [
|
|
1766
|
+
"All queued features are verified.",
|
|
1767
|
+
`Project verification: ${registry.project_verification.evidence}`,
|
|
1768
|
+
`Completion evidence: ${options.evidence}`
|
|
1769
|
+
]);
|
|
1770
|
+
appendLifecycleEvent(rootPath, state, {
|
|
1771
|
+
type: "project.completed",
|
|
1772
|
+
evidence: options.evidence
|
|
1773
|
+
});
|
|
1774
|
+
writeLifecycleRunRecord(rootPath, state, {
|
|
1775
|
+
id: "project-completed",
|
|
1776
|
+
timestamp: now,
|
|
1777
|
+
phase: "release",
|
|
1778
|
+
outcome: "success",
|
|
1779
|
+
evidence: options.evidence,
|
|
1780
|
+
task_id: "PROJECT",
|
|
1781
|
+
duration_ms: 0,
|
|
1782
|
+
metrics: state.metrics || {}
|
|
1783
|
+
});
|
|
1784
|
+
persistRunArtifacts(rootPath, {
|
|
1785
|
+
sessionId: state.session_id || "project-completion",
|
|
1786
|
+
state,
|
|
1787
|
+
answers: state.discovery_answers || {},
|
|
1788
|
+
idea: (state.discovery_answers && state.discovery_answers.idea) || "",
|
|
1789
|
+
recordedAt: now
|
|
1790
|
+
});
|
|
1791
|
+
console.log("Project completed.");
|
|
1792
|
+
console.log(`Evidence: ${options.evidence}`);
|
|
1793
|
+
}
|
|
1794
|
+
|
|
1795
|
+
function auditPipeline(rootPath) {
|
|
1796
|
+
const { state, registry } = readProjectLifecycle(rootPath);
|
|
1797
|
+
const errors = [];
|
|
1798
|
+
const activeFeatures = registry.features.filter(feature => feature.status === "in-progress");
|
|
1799
|
+
const unverified = registry.features.filter(feature => feature.status !== "verified");
|
|
1800
|
+
const verificationPath = path.join(rootPath, ".planning", "PROJECT_VERIFICATION.json");
|
|
1801
|
+
const handoffPath = path.join(rootPath, ".planning", "IMPLEMENTATION_HANDOFF.md");
|
|
1802
|
+
const eventsPath = path.join(sessionRunDir(rootPath, state.session_id || "lifecycle"), "EVENTS.jsonl");
|
|
1803
|
+
|
|
1804
|
+
if (state.current_state === "IMPLEMENTATION" && activeFeatures.length !== 1) {
|
|
1805
|
+
errors.push(`IMPLEMENTATION requires exactly one active feature; found ${activeFeatures.length}.`);
|
|
1806
|
+
}
|
|
1807
|
+
if (["VERIFICATION", "RELEASE_READY", "COMPLETED"].includes(state.current_state) && unverified.length > 0) {
|
|
1808
|
+
errors.push(`${state.current_state} contains unverified features: ${unverified.map(feature => feature.id).join(", ")}.`);
|
|
1809
|
+
}
|
|
1810
|
+
if (state.active_feature && !registry.features.some(feature => feature.path === state.active_feature && feature.status === "in-progress")) {
|
|
1811
|
+
errors.push("state.active_feature does not match an in-progress registry feature.");
|
|
1812
|
+
}
|
|
1813
|
+
if (["RELEASE_READY", "COMPLETED"].includes(state.current_state)) {
|
|
1814
|
+
if (registry.project_verification.status !== "passed") errors.push("project verification is not passed.");
|
|
1815
|
+
if (!fs.existsSync(verificationPath)) errors.push("PROJECT_VERIFICATION.json is missing.");
|
|
1816
|
+
if (!fs.existsSync(handoffPath)) errors.push("IMPLEMENTATION_HANDOFF.md is missing.");
|
|
1817
|
+
}
|
|
1818
|
+
if (state.current_state === "COMPLETED" && registry.project_status !== "completed") {
|
|
1819
|
+
errors.push("completed state does not match registry project_status.");
|
|
1820
|
+
}
|
|
1821
|
+
if (!fs.existsSync(eventsPath)) errors.push("lifecycle event history is missing.");
|
|
1822
|
+
|
|
1823
|
+
if (errors.length > 0) {
|
|
1824
|
+
console.error("Pipeline audit failed:");
|
|
1825
|
+
for (const error of errors) console.error(`- ${error}`);
|
|
1826
|
+
process.exit(1);
|
|
1827
|
+
}
|
|
1828
|
+
console.log("Pipeline audit passed.");
|
|
1829
|
+
console.log(`State: ${state.current_state}`);
|
|
1830
|
+
console.log(`Features: ${registry.features.length}`);
|
|
1831
|
+
}
|
|
1832
|
+
|
|
1833
|
+
function completeDiscoveryPhase(rootPath, answers) {
|
|
1834
|
+
const planningRoot = path.join(rootPath, ".planning");
|
|
1835
|
+
if (!fs.existsSync(planningRoot)) {
|
|
1836
|
+
fail(`missing planning directory at ${planningRoot}; run init first.`);
|
|
1837
|
+
}
|
|
1838
|
+
|
|
1839
|
+
const idea = normalizeAnswer(answers.idea, "No explicit user brief captured.");
|
|
1840
|
+
const productApproach = normalizeAnswer(
|
|
1841
|
+
answers.productApproach,
|
|
1842
|
+
`Bootstrap around this brief: ${idea}`
|
|
1843
|
+
);
|
|
1844
|
+
const primaryUser = normalizeAnswer(answers.primaryUser, "TBD");
|
|
1845
|
+
const v1Outcome = normalizeAnswer(answers.v1Outcome, "TBD");
|
|
1846
|
+
const qaOwner = normalizeAnswer(answers.qaOwner, "TBD");
|
|
1847
|
+
const backend = normalizeAnswer(answers.backend, "TBD");
|
|
1848
|
+
const frontend = normalizeAnswer(answers.frontend, "TBD");
|
|
1849
|
+
const database = normalizeAnswer(answers.database, "TBD");
|
|
1850
|
+
const deployment = normalizeAnswer(answers.deployment, "TBD");
|
|
1851
|
+
const testStrategy = normalizeAnswer(answers.testStrategy, "TBD");
|
|
1852
|
+
const stackOwner = normalizeAnswer(answers.stackOwner || answers.qaOwner, "TBD");
|
|
1853
|
+
const nowIso = new Date().toISOString();
|
|
1854
|
+
const today = nowIso.slice(0, 10);
|
|
1855
|
+
|
|
1856
|
+
updateMarkdownFile(path.join(planningRoot, "PROJECT.md"), (content) => {
|
|
1857
|
+
let next = content;
|
|
1858
|
+
next = replaceSection(next, "What This Project Is", `${idea}\n\nPreferred approach: ${productApproach}`);
|
|
1859
|
+
next = replaceSection(next, "Target Users", primaryUser);
|
|
1860
|
+
next = replaceSection(next, "Core Value", v1Outcome);
|
|
1861
|
+
next = replaceSection(
|
|
1862
|
+
next,
|
|
1863
|
+
"Product Scope",
|
|
1864
|
+
`- [x] Build around this brief: ${idea}\n- [x] Preferred product approach: ${productApproach}`
|
|
1865
|
+
);
|
|
1866
|
+
next = replaceSection(next, "Current Milestone", "First feature planning is ready.");
|
|
1867
|
+
next = replaceSection(
|
|
1868
|
+
next,
|
|
1869
|
+
"Success Criteria",
|
|
1870
|
+
`- [x] Discovery closed with explicit product approach.\n- [x] Primary user confirmed: ${primaryUser}\n- [x] Smallest acceptable v1 outcome confirmed: ${v1Outcome}`
|
|
1871
|
+
);
|
|
1872
|
+
return next;
|
|
1873
|
+
});
|
|
1874
|
+
|
|
1875
|
+
updateMarkdownFile(path.join(planningRoot, "REQUIREMENTS.md"), (content) => {
|
|
1876
|
+
let next = content;
|
|
1877
|
+
next = replaceSection(
|
|
1878
|
+
next,
|
|
1879
|
+
"Functional Requirements",
|
|
1880
|
+
`- [x] Support the approved product approach: ${productApproach}\n- [x] Deliver the smallest acceptable v1 outcome: ${v1Outcome}`
|
|
1881
|
+
);
|
|
1882
|
+
next = replaceSection(
|
|
1883
|
+
next,
|
|
1884
|
+
"User Stories",
|
|
1885
|
+
`- [x] As ${primaryUser}, I want ${v1Outcome.toLowerCase()} so that the core workflow can be completed without context loss.`
|
|
1886
|
+
);
|
|
1887
|
+
next = replaceSection(
|
|
1888
|
+
next,
|
|
1889
|
+
"Acceptance Criteria",
|
|
1890
|
+
`- [x] Discovery answers are recorded in INIT_QA.md.\n- [x] Product approach is explicit: ${productApproach}\n- [x] QA sign-off owner is explicit: ${qaOwner}`
|
|
1891
|
+
);
|
|
1892
|
+
next = replaceSection(
|
|
1893
|
+
next,
|
|
1894
|
+
"Known Unknowns",
|
|
1895
|
+
"- [ ] Decompose the approved scope into the first implementation features."
|
|
1896
|
+
);
|
|
1897
|
+
return next;
|
|
1898
|
+
});
|
|
1899
|
+
|
|
1900
|
+
updateMarkdownFile(path.join(planningRoot, "STACK.md"), (content) => {
|
|
1901
|
+
let next = content;
|
|
1902
|
+
next = replaceLineValue(next, "Language", backend);
|
|
1903
|
+
next = replaceLineValue(next, "Framework", frontend);
|
|
1904
|
+
next = replaceLineValue(next, "Runtime", backend);
|
|
1905
|
+
next = replaceLineValue(next, "Database", database);
|
|
1906
|
+
next = replaceLineValue(next, "Test framework", testStrategy);
|
|
1907
|
+
next = replaceLineValue(next, "Deployment target", deployment);
|
|
1908
|
+
return next;
|
|
1909
|
+
});
|
|
1910
|
+
|
|
1911
|
+
updateMarkdownFile(path.join(planningRoot, "INIT_QA.md"), (content) => {
|
|
1912
|
+
const answersBlock = [
|
|
1913
|
+
"## Recorded Answers",
|
|
1914
|
+
"",
|
|
1915
|
+
`- [x] Product approach: ${productApproach}`,
|
|
1916
|
+
`- [x] Primary user: ${primaryUser}`,
|
|
1917
|
+
`- [x] Smallest acceptable v1 outcome: ${v1Outcome}`,
|
|
1918
|
+
`- [x] QA sign-off owner: ${qaOwner}`,
|
|
1919
|
+
`- [x] Backend/runtime choice: ${backend}`,
|
|
1920
|
+
`- [x] Frontend/client choice: ${frontend}`,
|
|
1921
|
+
`- [x] Storage/database choice: ${database}`,
|
|
1922
|
+
`- [x] Test strategy: ${testStrategy}`,
|
|
1923
|
+
`- [x] Deployment target: ${deployment}`,
|
|
1924
|
+
`- [x] Final tech stack owner: ${stackOwner}`
|
|
1925
|
+
].join("\n");
|
|
1926
|
+
|
|
1927
|
+
if (content.includes("## Recorded Answers")) {
|
|
1928
|
+
return replaceSection(content, "Recorded Answers", answersBlock.replace("## Recorded Answers\n\n", ""));
|
|
1929
|
+
}
|
|
1930
|
+
return `${content.trim()}\n\n${answersBlock}\n`;
|
|
1931
|
+
});
|
|
1932
|
+
|
|
1933
|
+
updateMarkdownFile(path.join(planningRoot, "decisions", "ADR-001-tech-stack.md"), (content) => {
|
|
1934
|
+
let next = content;
|
|
1935
|
+
next = next.replace("Status: Proposed", "Status: Accepted");
|
|
1936
|
+
next = replaceSection(next, "Context", `Brief: ${idea}`);
|
|
1937
|
+
next = replaceSection(
|
|
1938
|
+
next,
|
|
1939
|
+
"Decision",
|
|
1940
|
+
`Use ${backend} for backend/runtime, ${frontend} for the client, ${database} for storage, deploy to ${deployment}, and verify through ${testStrategy}.`
|
|
1941
|
+
);
|
|
1942
|
+
next = replaceSection(next, "Alternatives Considered", "- [x] Alternatives will be revisited only if the first feature plan exposes blocking constraints.");
|
|
1943
|
+
next = replaceSection(next, "Consequences", `- [x] Discovery is closed and feature planning can assume this stack.\n- [x] Stack owner: ${stackOwner}`);
|
|
1944
|
+
next = replaceSection(next, "Risks", "- [ ] Feature-level implementation risks will be captured in the first feature plan.");
|
|
1945
|
+
next = replaceSection(next, "Mitigation", "- [x] Revisit this ADR if the first implementation feature invalidates the chosen stack.");
|
|
1946
|
+
next = replaceSection(next, "Verification Evidence", `- [x] Discovery answers captured via genesis-harness run on ${today}.`);
|
|
1947
|
+
return next;
|
|
1948
|
+
});
|
|
1949
|
+
|
|
1950
|
+
updateMarkdownFile(path.join(planningRoot, "ROADMAP.md"), (content) =>
|
|
1951
|
+
content
|
|
1952
|
+
.replace(
|
|
1953
|
+
"| 01 Discovery & QA | Validation | [ ] | 00 Foundation | Product approach confirmed, QA checklist answered, tech stack signed off |",
|
|
1954
|
+
"| 01 Discovery & QA | Validation | [x] | 00 Foundation | Product approach confirmed, QA checklist answered, tech stack signed off |"
|
|
1955
|
+
)
|
|
1956
|
+
.replace(
|
|
1957
|
+
"| TBD | Feature | [ ] | 01 Discovery & QA | To be planned after requirements finalized |",
|
|
1958
|
+
"| TBD | Feature | [~] | 01 Discovery & QA | Ready for first feature plan |"
|
|
1959
|
+
)
|
|
1960
|
+
);
|
|
1961
|
+
|
|
1962
|
+
updateMarkdownFile(path.join(planningRoot, "STATE.md"), (content) => {
|
|
1963
|
+
let next = content;
|
|
1964
|
+
next = next.replace(
|
|
1965
|
+
/Current project state: .*$/m,
|
|
1966
|
+
"Current project state: [~] Discovery closed, ready for feature planning."
|
|
1967
|
+
);
|
|
1968
|
+
next = next.replace(
|
|
1969
|
+
/Current phase: .*$/m,
|
|
1970
|
+
"Current phase: 02 First Feature Planning"
|
|
1971
|
+
);
|
|
1972
|
+
next = next.replace(
|
|
1973
|
+
/Last completed task: .*$/m,
|
|
1974
|
+
"Last completed task: Closed discovery Q&A, QA sign-off path, and tech stack."
|
|
1975
|
+
);
|
|
1976
|
+
next = next.replace(
|
|
1977
|
+
/Next task: .*$/m,
|
|
1978
|
+
"Next task: Create the first feature plan from the approved scope."
|
|
1979
|
+
);
|
|
1980
|
+
next = next.replace(
|
|
1981
|
+
/Latest verification result: .*$/m,
|
|
1982
|
+
"Latest verification result: Discovery bootstrap completed."
|
|
1983
|
+
);
|
|
1984
|
+
return next;
|
|
1985
|
+
});
|
|
1986
|
+
|
|
1987
|
+
updateMarkdownFile(path.join(planningRoot, "SUMMARY.md"), (content) => {
|
|
1988
|
+
let next = content;
|
|
1989
|
+
next = replaceSection(next, "Current Focus", "- [x] Discovery closed and project moved into first feature planning.");
|
|
1990
|
+
next = replaceSection(next, "Recent Changes", `- [x] Discovery answers captured for ${primaryUser}.\n- [x] Stack accepted: ${backend} + ${frontend} + ${database}.`);
|
|
1991
|
+
next = replaceSection(next, "Next Recommended Task", "- [ ] Create the first feature plan and its verification contract.");
|
|
1992
|
+
return next;
|
|
1993
|
+
});
|
|
1994
|
+
|
|
1995
|
+
const currentStatePath = path.join(rootPath, ".codebase", "CURRENT_STATE.md");
|
|
1996
|
+
if (fs.existsSync(currentStatePath)) {
|
|
1997
|
+
writeFileIfChanged(
|
|
1998
|
+
currentStatePath,
|
|
1999
|
+
[
|
|
2000
|
+
"# Current System State",
|
|
2001
|
+
"",
|
|
2002
|
+
`**Time**: ${today} `,
|
|
2003
|
+
"**Status**: `IN_PROGRESS` ",
|
|
2004
|
+
`**Latest Session**: \`${today}-run-pipeline\` `,
|
|
2005
|
+
"",
|
|
2006
|
+
"## Active Bootstrap",
|
|
2007
|
+
"",
|
|
2008
|
+
`- Planning harness initialized from the user brief: ${idea}`,
|
|
2009
|
+
"- Discovery answers are now recorded and the project is ready for feature planning.",
|
|
2010
|
+
"- Current planner phase: `PLANNING`",
|
|
2011
|
+
"- Next task: Create the first feature plan from the approved scope."
|
|
2012
|
+
].join("\n")
|
|
2013
|
+
);
|
|
2014
|
+
}
|
|
2015
|
+
|
|
2016
|
+
const statePath = path.join(rootPath, ".codebase", "state.json");
|
|
2017
|
+
if (fs.existsSync(statePath)) {
|
|
2018
|
+
const state = JSON.parse(fs.readFileSync(statePath, "utf8"));
|
|
2019
|
+
const sessionId = `${today}-run-pipeline`;
|
|
2020
|
+
state.current_state = "PLANNING";
|
|
2021
|
+
state.active_work = "First feature planning";
|
|
2022
|
+
state.active_feature = "";
|
|
2023
|
+
state.session_id = sessionId;
|
|
2024
|
+
state.session_started_at = state.session_started_at || nowIso;
|
|
2025
|
+
state.last_updated_at = nowIso;
|
|
2026
|
+
state.latest_recovery_point = "Discovery Q&A completed";
|
|
2027
|
+
state.required_verification = [
|
|
2028
|
+
"genesis-harness run --idea \"<user brief>\" ...",
|
|
2029
|
+
"genesis-harness resume",
|
|
2030
|
+
"Review .planning/PROJECT.md, REQUIREMENTS.md, STACK.md",
|
|
2031
|
+
"Create the first feature plan"
|
|
2032
|
+
];
|
|
2033
|
+
state.pending_tasks = ["Create the first feature plan", "Define the first verification contract"];
|
|
2034
|
+
state.discovery_answers = {
|
|
2035
|
+
idea,
|
|
2036
|
+
product_approach: productApproach,
|
|
2037
|
+
primary_user: primaryUser,
|
|
2038
|
+
v1_outcome: v1Outcome,
|
|
2039
|
+
qa_owner: qaOwner,
|
|
2040
|
+
backend,
|
|
2041
|
+
frontend,
|
|
2042
|
+
database,
|
|
2043
|
+
deployment,
|
|
2044
|
+
test_strategy: testStrategy,
|
|
2045
|
+
stack_owner: stackOwner,
|
|
2046
|
+
captured_at: nowIso
|
|
2047
|
+
};
|
|
2048
|
+
writeJsonFile(statePath, state);
|
|
2049
|
+
persistRunArtifacts(rootPath, {
|
|
2050
|
+
sessionId,
|
|
2051
|
+
state,
|
|
2052
|
+
answers: state.discovery_answers,
|
|
2053
|
+
idea,
|
|
2054
|
+
recordedAt: nowIso
|
|
2055
|
+
});
|
|
2056
|
+
}
|
|
2057
|
+
}
|
|
2058
|
+
|
|
2059
|
+
function slugifyFeature(value, fallback = "first-feature") {
|
|
2060
|
+
const slug = String(value || "")
|
|
2061
|
+
.toLowerCase()
|
|
2062
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
2063
|
+
.replace(/^-+|-+$/g, "")
|
|
2064
|
+
.slice(0, 60)
|
|
2065
|
+
.replace(/-+$/g, "");
|
|
2066
|
+
return slug || fallback;
|
|
2067
|
+
}
|
|
2068
|
+
|
|
2069
|
+
function toTitleCase(value) {
|
|
2070
|
+
return String(value || "")
|
|
2071
|
+
.split(/[\s-]+/)
|
|
2072
|
+
.filter(Boolean)
|
|
2073
|
+
.map(part => part.charAt(0).toUpperCase() + part.slice(1))
|
|
2074
|
+
.join(" ");
|
|
2075
|
+
}
|
|
2076
|
+
|
|
2077
|
+
function inferFeatureKind(answers) {
|
|
2078
|
+
const frontend = normalizeAnswer(answers.frontend, "TBD");
|
|
2079
|
+
const backend = normalizeAnswer(answers.backend, "TBD");
|
|
2080
|
+
const approach = `${answers.productApproach || ""} ${answers.v1Outcome || ""}`.toLowerCase();
|
|
2081
|
+
const uiHints = /(dashboard|screen|page|web|mobile|tablet|client|ui|form|portal)/.test(approach);
|
|
2082
|
+
const apiHints = /(api|endpoint|backend|service|queue|workflow|sync|request)/.test(approach);
|
|
2083
|
+
const hasFrontend = frontend !== "TBD";
|
|
2084
|
+
const hasBackend = backend !== "TBD";
|
|
2085
|
+
|
|
2086
|
+
if ((hasFrontend || uiHints) && (hasBackend || apiHints)) return "full-stack";
|
|
2087
|
+
if (hasFrontend || uiHints) return "ui";
|
|
2088
|
+
if (hasBackend || apiHints) return "api";
|
|
2089
|
+
return "generic";
|
|
2090
|
+
}
|
|
2091
|
+
|
|
2092
|
+
function deriveFeatureSurface(answers, featureRelativePath) {
|
|
2093
|
+
const summary = normalizeAnswer(
|
|
2094
|
+
answers.v1Outcome,
|
|
2095
|
+
normalizeAnswer(answers.productApproach, normalizeAnswer(answers.idea, "first feature slice"))
|
|
2096
|
+
);
|
|
2097
|
+
const combined = `${answers.productApproach || ""} ${summary}`.toLowerCase();
|
|
2098
|
+
let routeSegment = slugifyFeature(summary, path.basename(featureRelativePath));
|
|
2099
|
+
let collectionName = "items";
|
|
2100
|
+
let responseStatus = "ready";
|
|
2101
|
+
let selectionEvent = "onFeatureSelect";
|
|
2102
|
+
|
|
2103
|
+
if (/\bqueue\b/.test(combined)) {
|
|
2104
|
+
routeSegment = "staff-queue";
|
|
2105
|
+
collectionName = "items";
|
|
2106
|
+
responseStatus = "queued";
|
|
2107
|
+
selectionEvent = "onQueueItemSelect";
|
|
2108
|
+
} else if (/\blogin|sign[\s-]?in|auth\b/.test(combined)) {
|
|
2109
|
+
routeSegment = "login";
|
|
2110
|
+
collectionName = "sessions";
|
|
2111
|
+
responseStatus = "authenticated";
|
|
2112
|
+
selectionEvent = "onLoginSubmit";
|
|
2113
|
+
} else if (/\bdashboard\b/.test(combined)) {
|
|
2114
|
+
routeSegment = "dashboard";
|
|
2115
|
+
collectionName = "widgets";
|
|
2116
|
+
responseStatus = "loaded";
|
|
2117
|
+
selectionEvent = "onDashboardCardSelect";
|
|
2118
|
+
}
|
|
2119
|
+
|
|
2120
|
+
const route = `/${routeSegment}`;
|
|
2121
|
+
const apiPath = `/api/${routeSegment}/${collectionName}`;
|
|
2122
|
+
|
|
2123
|
+
return {
|
|
2124
|
+
kind: inferFeatureKind(answers),
|
|
2125
|
+
route,
|
|
2126
|
+
apiPath,
|
|
2127
|
+
responseStatus,
|
|
2128
|
+
selectionEvent,
|
|
2129
|
+
entityName: toTitleCase(collectionName.replace(/-/g, " ")),
|
|
2130
|
+
contractSlug: path.basename(featureRelativePath)
|
|
2131
|
+
};
|
|
2132
|
+
}
|
|
2133
|
+
|
|
2134
|
+
function deriveFirstFeatureSeed(answers) {
|
|
2135
|
+
const summary = normalizeAnswer(
|
|
2136
|
+
answers.v1Outcome,
|
|
2137
|
+
normalizeAnswer(answers.productApproach, normalizeAnswer(answers.idea, "First feature slice"))
|
|
2138
|
+
);
|
|
2139
|
+
const slugSource = summary
|
|
2140
|
+
.replace(/^staff can\s+/i, "")
|
|
2141
|
+
.replace(/^users can\s+/i, "")
|
|
2142
|
+
.replace(/^allow\s+/i, "");
|
|
2143
|
+
return {
|
|
2144
|
+
summary,
|
|
2145
|
+
slug: slugifyFeature(slugSource, "first-feature-slice")
|
|
2146
|
+
};
|
|
2147
|
+
}
|
|
2148
|
+
|
|
2149
|
+
function createFeatureScaffold(rootPath, { slug, summary }) {
|
|
2150
|
+
const scriptPath = path.join(packageRoot, ".codex", "skills", "genesis-harness", "scripts", "create-feature.sh");
|
|
2151
|
+
const result = spawnSync("bash", [scriptPath, slug, summary, rootPath], {
|
|
2152
|
+
cwd: rootPath,
|
|
2153
|
+
encoding: "utf8"
|
|
2154
|
+
});
|
|
2155
|
+
|
|
2156
|
+
if (result.status !== 0) {
|
|
2157
|
+
const details = (result.stderr || result.stdout || "unknown create-feature.sh failure").trim();
|
|
2158
|
+
fail(`failed to scaffold first feature: ${details}`);
|
|
2159
|
+
}
|
|
2160
|
+
|
|
2161
|
+
const relativePath = (result.stdout || "").trim();
|
|
2162
|
+
if (!relativePath) {
|
|
2163
|
+
fail("failed to scaffold first feature: create-feature.sh did not return the feature path");
|
|
2164
|
+
}
|
|
2165
|
+
|
|
2166
|
+
return relativePath;
|
|
2167
|
+
}
|
|
2168
|
+
|
|
2169
|
+
function seedUiContractsAndFixtures(rootPath, featureRelativePath, featureSurface, answers, featureSeed) {
|
|
2170
|
+
const uiContractDir = path.join(rootPath, "contracts", "ui", featureSurface.contractSlug);
|
|
2171
|
+
const uiFixturePath = path.join(rootPath, "playwright", "fixtures", `${featureSurface.contractSlug}-ui-fixture.md`);
|
|
2172
|
+
fs.mkdirSync(uiContractDir, { recursive: true });
|
|
2173
|
+
fs.mkdirSync(path.dirname(uiFixturePath), { recursive: true });
|
|
2174
|
+
|
|
2175
|
+
writeJsonFile(path.join(uiContractDir, "screen-contract.json"), {
|
|
2176
|
+
contract_id: `UI-${featureSurface.contractSlug.toUpperCase().replace(/[^A-Z0-9]+/g, "-")}`,
|
|
2177
|
+
version: "1.0.0",
|
|
2178
|
+
description: `Bootstrap UI contract for the first feature slice: ${featureSeed.summary}.`,
|
|
2179
|
+
inputs: {
|
|
2180
|
+
route: featureSurface.route,
|
|
2181
|
+
data: [
|
|
2182
|
+
{
|
|
2183
|
+
name: "actor",
|
|
2184
|
+
type: "string",
|
|
2185
|
+
validation: normalizeAnswer(answers.primaryUser, "Primary user"),
|
|
2186
|
+
required: true
|
|
2187
|
+
}
|
|
2188
|
+
]
|
|
2189
|
+
},
|
|
2190
|
+
states: {
|
|
2191
|
+
initial: `Route ${featureSurface.route} is visible with an empty state ready for ${featureSeed.summary.toLowerCase()}.`,
|
|
2192
|
+
valid: `The primary action is enabled and the ${featureSurface.entityName.toLowerCase()} list is interactive.`,
|
|
2193
|
+
loading: "The main action is pending and the primary controls are disabled.",
|
|
2194
|
+
error: "An inline error is shown with enough context for QA triage."
|
|
2195
|
+
},
|
|
2196
|
+
outputs: {
|
|
2197
|
+
events: [
|
|
2198
|
+
{
|
|
2199
|
+
name: featureSurface.selectionEvent,
|
|
2200
|
+
payload: {
|
|
2201
|
+
id: "string",
|
|
2202
|
+
status: featureSurface.responseStatus
|
|
2203
|
+
}
|
|
2204
|
+
}
|
|
2205
|
+
]
|
|
2206
|
+
},
|
|
2207
|
+
mockup_reference: `${featureRelativePath}/mockup.png`
|
|
2208
|
+
});
|
|
2209
|
+
|
|
2210
|
+
writeFileIfChanged(
|
|
2211
|
+
uiFixturePath,
|
|
2212
|
+
[
|
|
2213
|
+
"# UI Fixture",
|
|
2214
|
+
"",
|
|
2215
|
+
`- Route: \`${featureSurface.route}\``,
|
|
2216
|
+
`- User role: ${normalizeAnswer(answers.primaryUser, "TBD")}`,
|
|
2217
|
+
"- Viewport: tablet landscape",
|
|
2218
|
+
`- Mocked API: \`${featureSurface.apiPath}\` returns ${featureSurface.responseStatus}`,
|
|
2219
|
+
`- Expected text: ${featureSeed.summary}`,
|
|
2220
|
+
`- Expected state: ${featureSurface.responseStatus} items render in the primary queue or list`,
|
|
2221
|
+
""
|
|
2222
|
+
].join("\n")
|
|
2223
|
+
);
|
|
2224
|
+
|
|
2225
|
+
return {
|
|
2226
|
+
contractPath: path.relative(rootPath, path.join(uiContractDir, "screen-contract.json")),
|
|
2227
|
+
fixturePath: path.relative(rootPath, uiFixturePath)
|
|
2228
|
+
};
|
|
2229
|
+
}
|
|
2230
|
+
|
|
2231
|
+
function seedApiContractsAndFixtures(rootPath, featureSurface, answers, featureSeed) {
|
|
2232
|
+
const apiContractDir = path.join(rootPath, "contracts", "api", featureSurface.contractSlug);
|
|
2233
|
+
const apiFixturePath = path.join(rootPath, "fixtures", "api", `${featureSurface.contractSlug}-api-fixture.md`);
|
|
2234
|
+
fs.mkdirSync(apiContractDir, { recursive: true });
|
|
2235
|
+
fs.mkdirSync(path.dirname(apiFixturePath), { recursive: true });
|
|
2236
|
+
|
|
2237
|
+
writeJsonFile(path.join(apiContractDir, "request.json"), {
|
|
2238
|
+
method: "POST",
|
|
2239
|
+
path: featureSurface.apiPath,
|
|
2240
|
+
body: {
|
|
2241
|
+
actor: normalizeAnswer(answers.primaryUser, "Primary user"),
|
|
2242
|
+
intent: featureSeed.summary,
|
|
2243
|
+
status: featureSurface.responseStatus
|
|
2244
|
+
}
|
|
2245
|
+
});
|
|
2246
|
+
writeJsonFile(path.join(apiContractDir, "response.json"), {
|
|
2247
|
+
status: 200,
|
|
2248
|
+
body: {
|
|
2249
|
+
ok: true,
|
|
2250
|
+
status: featureSurface.responseStatus,
|
|
2251
|
+
item: {
|
|
2252
|
+
id: "generated-id",
|
|
2253
|
+
summary: featureSeed.summary
|
|
2254
|
+
}
|
|
2255
|
+
}
|
|
2256
|
+
});
|
|
2257
|
+
writeJsonFile(path.join(apiContractDir, "schema.json"), {
|
|
2258
|
+
type: "object",
|
|
2259
|
+
required: ["ok", "status", "item"],
|
|
2260
|
+
properties: {
|
|
2261
|
+
ok: { type: "boolean" },
|
|
2262
|
+
status: { type: "string" },
|
|
2263
|
+
item: {
|
|
2264
|
+
type: "object",
|
|
2265
|
+
required: ["id", "summary"],
|
|
2266
|
+
properties: {
|
|
2267
|
+
id: { type: "string" },
|
|
2268
|
+
summary: { type: "string" }
|
|
2269
|
+
}
|
|
2270
|
+
}
|
|
2271
|
+
}
|
|
2272
|
+
});
|
|
2273
|
+
writeJsonFile(path.join(apiContractDir, "example.json"), {
|
|
2274
|
+
request: {
|
|
2275
|
+
method: "POST",
|
|
2276
|
+
path: featureSurface.apiPath
|
|
2277
|
+
},
|
|
2278
|
+
response: {
|
|
2279
|
+
status: 200,
|
|
2280
|
+
body: {
|
|
2281
|
+
ok: true,
|
|
2282
|
+
status: featureSurface.responseStatus
|
|
2283
|
+
}
|
|
2284
|
+
}
|
|
2285
|
+
});
|
|
2286
|
+
writeJsonFile(path.join(apiContractDir, "error.json"), {
|
|
2287
|
+
error: "invalid_feature_request",
|
|
2288
|
+
message: `Request failed validation for ${featureSeed.summary}.`
|
|
2289
|
+
});
|
|
2290
|
+
|
|
2291
|
+
writeFileIfChanged(
|
|
2292
|
+
apiFixturePath,
|
|
2293
|
+
[
|
|
2294
|
+
"# API Fixture",
|
|
2295
|
+
"",
|
|
2296
|
+
"## Input",
|
|
2297
|
+
"",
|
|
2298
|
+
"- Method: `POST`",
|
|
2299
|
+
`- Path: \`${featureSurface.apiPath}\``,
|
|
2300
|
+
`- Auth: ${normalizeAnswer(answers.primaryUser, "TBD")}`,
|
|
2301
|
+
`- Body intent: ${featureSeed.summary}`,
|
|
2302
|
+
"",
|
|
2303
|
+
"## Expected Output",
|
|
2304
|
+
"",
|
|
2305
|
+
"- Status: `200`",
|
|
2306
|
+
`- Body status: \`${featureSurface.responseStatus}\``,
|
|
2307
|
+
`- Persistence: a ${featureSurface.entityName.toLowerCase()} record is created or updated`,
|
|
2308
|
+
"",
|
|
2309
|
+
"## Validation Notes",
|
|
2310
|
+
"",
|
|
2311
|
+
"- Request schema must reject missing actor or intent fields.",
|
|
2312
|
+
"- Response schema must include ok/status/item.",
|
|
2313
|
+
""
|
|
2314
|
+
].join("\n")
|
|
2315
|
+
);
|
|
2316
|
+
|
|
2317
|
+
return {
|
|
2318
|
+
contractDir: path.relative(rootPath, apiContractDir),
|
|
2319
|
+
fixturePath: path.relative(rootPath, apiFixturePath)
|
|
2320
|
+
};
|
|
2321
|
+
}
|
|
2322
|
+
|
|
2323
|
+
function seedFirstFeatureExecution(rootPath, answers) {
|
|
2324
|
+
const planningRoot = path.join(rootPath, ".planning");
|
|
2325
|
+
if (!fs.existsSync(planningRoot)) {
|
|
2326
|
+
fail(`missing planning directory at ${planningRoot}; run init first.`);
|
|
2327
|
+
}
|
|
2328
|
+
|
|
2329
|
+
const idea = normalizeAnswer(answers.idea, "No explicit user brief captured.");
|
|
2330
|
+
const productApproach = normalizeAnswer(answers.productApproach, `Bootstrap around this brief: ${idea}`);
|
|
2331
|
+
const primaryUser = normalizeAnswer(answers.primaryUser, "the primary user");
|
|
2332
|
+
const v1Outcome = normalizeAnswer(answers.v1Outcome, "deliver the first feature slice");
|
|
2333
|
+
const qaOwner = normalizeAnswer(answers.qaOwner, "TBD");
|
|
2334
|
+
const backend = normalizeAnswer(answers.backend, "TBD");
|
|
2335
|
+
const frontend = normalizeAnswer(answers.frontend, "TBD");
|
|
2336
|
+
const database = normalizeAnswer(answers.database, "TBD");
|
|
2337
|
+
const deployment = normalizeAnswer(answers.deployment, "TBD");
|
|
2338
|
+
const testStrategy = normalizeAnswer(answers.testStrategy, "TBD");
|
|
2339
|
+
const stackOwner = normalizeAnswer(answers.stackOwner || answers.qaOwner, "TBD");
|
|
2340
|
+
const nowIso = new Date().toISOString();
|
|
2341
|
+
const today = nowIso.slice(0, 10);
|
|
2342
|
+
const featureSeed = deriveFirstFeatureSeed(answers);
|
|
2343
|
+
const featureRelativePath = createFeatureScaffold(rootPath, featureSeed);
|
|
2344
|
+
const featureDir = path.join(rootPath, featureRelativePath);
|
|
2345
|
+
const featureName = path.basename(featureRelativePath);
|
|
2346
|
+
const featureSurface = deriveFeatureSurface(answers, featureRelativePath);
|
|
2347
|
+
const generatedArtifacts = {};
|
|
2348
|
+
if (featureSurface.kind === "ui" || featureSurface.kind === "full-stack") {
|
|
2349
|
+
generatedArtifacts.ui = seedUiContractsAndFixtures(rootPath, featureRelativePath, featureSurface, answers, featureSeed);
|
|
2350
|
+
}
|
|
2351
|
+
if (featureSurface.kind === "api" || featureSurface.kind === "full-stack") {
|
|
2352
|
+
generatedArtifacts.api = seedApiContractsAndFixtures(rootPath, featureSurface, answers, featureSeed);
|
|
2353
|
+
}
|
|
2354
|
+
|
|
2355
|
+
writeFileIfChanged(
|
|
2356
|
+
path.join(featureDir, "SPEC.md"),
|
|
2357
|
+
[
|
|
2358
|
+
`# Feature: ${featureSeed.summary}`,
|
|
2359
|
+
"",
|
|
2360
|
+
"## Summary",
|
|
2361
|
+
"",
|
|
2362
|
+
`${featureSeed.summary}`,
|
|
2363
|
+
"",
|
|
2364
|
+
"## User Story",
|
|
2365
|
+
"",
|
|
2366
|
+
`As ${primaryUser}, I want ${v1Outcome.toLowerCase()} so that the lobby team can complete the first core workflow without context switching.`,
|
|
2367
|
+
"",
|
|
2368
|
+
"## Expected Behavior",
|
|
2369
|
+
"",
|
|
2370
|
+
`- [x] Reflect the approved product approach: ${productApproach}`,
|
|
2371
|
+
`- [x] Deliver the v1 outcome: ${v1Outcome}`,
|
|
2372
|
+
`- [x] Align implementation choices with ${backend} + ${frontend} + ${database}`,
|
|
2373
|
+
"",
|
|
2374
|
+
"## Edge Cases",
|
|
2375
|
+
"",
|
|
2376
|
+
"- [ ] Empty-state flow has a visible fallback.",
|
|
2377
|
+
"- [ ] Failure path preserves enough detail for QA triage.",
|
|
2378
|
+
"- [ ] The first feature slice remains deployable without opening new scope.",
|
|
2379
|
+
"",
|
|
2380
|
+
"## Out Of Scope",
|
|
2381
|
+
"",
|
|
2382
|
+
"- [x] Additional features beyond the first implementation slice.",
|
|
2383
|
+
"- [x] Unapproved stack changes outside the accepted discovery answers.",
|
|
2384
|
+
"",
|
|
2385
|
+
"## Acceptance Criteria",
|
|
2386
|
+
"",
|
|
2387
|
+
`- [x] The first feature plan is scaffolded at \`${featureRelativePath}\`.`,
|
|
2388
|
+
"- [x] Tests, contracts, and verification steps are defined before implementation starts.",
|
|
2389
|
+
`- [x] QA sign-off path names ${qaOwner} as the owner for this slice.`,
|
|
2390
|
+
""
|
|
2391
|
+
].join("\n")
|
|
2392
|
+
);
|
|
2393
|
+
|
|
2394
|
+
writeFileIfChanged(
|
|
2395
|
+
path.join(featureDir, "IMPACT.md"),
|
|
2396
|
+
[
|
|
2397
|
+
"# Impact",
|
|
2398
|
+
"",
|
|
2399
|
+
"| Question | Answer | Notes |",
|
|
2400
|
+
"|---|---|---|",
|
|
2401
|
+
`| Does this affect API? | TBD | Define only the endpoints needed for: ${v1Outcome} |`,
|
|
2402
|
+
`| Does this affect database? | TBD | Keep schema changes bounded to ${database} decisions already accepted |`,
|
|
2403
|
+
"| Does this affect UI? | Yes | First feature execution starts from the approved primary flow |",
|
|
2404
|
+
"| Does this affect auth/security? | TBD | Capture login and permissions assumptions before implementation |",
|
|
2405
|
+
"| Does this affect integrations? | TBD | Defer unless the first slice cannot work without them |",
|
|
2406
|
+
"| Does this affect environment variables? | TBD | Document anything needed before deploy |",
|
|
2407
|
+
"| Does this affect architecture? | No | Stay within the accepted bootstrap architecture unless blocked |",
|
|
2408
|
+
"| Does this require docs update? | Yes | Update planning docs and runtime state as implementation progresses |",
|
|
2409
|
+
`| Does this require tests? | Yes | ${testStrategy} |`,
|
|
2410
|
+
"| Does this require migration? | TBD | Only if the first slice introduces persistent state changes |",
|
|
2411
|
+
"| Does this affect existing user journeys? | Yes | It defines the first explicit journey after discovery |",
|
|
2412
|
+
""
|
|
2413
|
+
].join("\n")
|
|
2414
|
+
);
|
|
2415
|
+
|
|
2416
|
+
writeFileIfChanged(
|
|
2417
|
+
path.join(featureDir, "PLAN.md"),
|
|
2418
|
+
[
|
|
2419
|
+
"# Plan",
|
|
2420
|
+
"",
|
|
2421
|
+
"## Files To Change",
|
|
2422
|
+
"",
|
|
2423
|
+
"### File: `.planning/features/...`",
|
|
2424
|
+
"",
|
|
2425
|
+
"Change: Replace TBD scaffolding with the approved first implementation slice.",
|
|
2426
|
+
`Why: Move the pipeline from discovery into real execution for ${v1Outcome}.`,
|
|
2427
|
+
"Risk: The slice becomes too broad and stops being executable.",
|
|
2428
|
+
`Test: ${testStrategy}`,
|
|
2429
|
+
"Docs impact: STATE.md, SUMMARY.md, FEATURE_INDEX.md, and SPEC_CHANGELOG.md",
|
|
2430
|
+
"",
|
|
2431
|
+
"## Implementation Steps",
|
|
2432
|
+
"",
|
|
2433
|
+
"- [x] Scaffold the first feature directory from the discovery-approved scope.",
|
|
2434
|
+
"- [x] Seed SPEC.md, TEST_CONTRACT.md, and VERIFICATION.md for the active slice.",
|
|
2435
|
+
"- [ ] Add the first failing tests or verification checks for this slice.",
|
|
2436
|
+
"- [ ] Implement the minimum code required to satisfy the first test contract.",
|
|
2437
|
+
"- [ ] Run verification and record evidence in VERIFICATION.md.",
|
|
2438
|
+
"",
|
|
2439
|
+
"## Test Strategy",
|
|
2440
|
+
"",
|
|
2441
|
+
`- [x] Start from ${testStrategy}.`,
|
|
2442
|
+
"- [ ] Add or update the narrowest failing test first.",
|
|
2443
|
+
"- [ ] Expand coverage only after the first slice is green.",
|
|
2444
|
+
"",
|
|
2445
|
+
"## Docs To Update",
|
|
2446
|
+
"",
|
|
2447
|
+
"- [x] `.planning/STATE.md`",
|
|
2448
|
+
"- [x] `.planning/SUMMARY.md`",
|
|
2449
|
+
"- [x] `.planning/FEATURE_INDEX.md`",
|
|
2450
|
+
...(generatedArtifacts.ui ? [`- [x] \`${generatedArtifacts.ui.contractPath}\``, `- [x] \`${generatedArtifacts.ui.fixturePath}\``] : []),
|
|
2451
|
+
...(generatedArtifacts.api ? [`- [x] \`${generatedArtifacts.api.contractDir}/request.json\``, `- [x] \`${generatedArtifacts.api.fixturePath}\``] : []),
|
|
2452
|
+
"- [ ] `.planning/SPEC_CHANGELOG.md`",
|
|
2453
|
+
"",
|
|
2454
|
+
"## Diagrams To Update",
|
|
2455
|
+
"",
|
|
2456
|
+
"- [x] `DIAGRAM.mmd`",
|
|
2457
|
+
"",
|
|
2458
|
+
"## Risks",
|
|
2459
|
+
"",
|
|
2460
|
+
`- [ ] Scope drift beyond ${featureSeed.summary}.`,
|
|
2461
|
+
`- [ ] Stack changes that conflict with ${stackOwner}'s sign-off.`,
|
|
2462
|
+
"",
|
|
2463
|
+
"## Rollback Plan",
|
|
2464
|
+
"",
|
|
2465
|
+
"- [ ] Revert the active slice to the last passing verification state and reopen planning if implementation scope changes.",
|
|
2466
|
+
"",
|
|
2467
|
+
"## Verification Commands",
|
|
2468
|
+
"",
|
|
2469
|
+
"```sh",
|
|
2470
|
+
"rtk bash scripts/verify.sh",
|
|
2471
|
+
"rtk bash scripts/run-evals.sh",
|
|
2472
|
+
"rtk node bin/genesis-harness.js verify-gate",
|
|
2473
|
+
"```",
|
|
2474
|
+
""
|
|
2475
|
+
].join("\n")
|
|
2476
|
+
);
|
|
2477
|
+
|
|
2478
|
+
writeFileIfChanged(
|
|
2479
|
+
path.join(featureDir, "TEST_CONTRACT.md"),
|
|
2480
|
+
[
|
|
2481
|
+
"# Test Contract",
|
|
2482
|
+
"",
|
|
2483
|
+
"## Normal Input / Output",
|
|
2484
|
+
"",
|
|
2485
|
+
`- [x] Primary actor: ${primaryUser}`,
|
|
2486
|
+
`- [x] Target behavior: ${v1Outcome}`,
|
|
2487
|
+
`- [x] Runtime direction: ${backend} + ${frontend}`,
|
|
2488
|
+
...(generatedArtifacts.ui ? [`- [x] UI contract: \`${generatedArtifacts.ui.contractPath}\``] : []),
|
|
2489
|
+
...(generatedArtifacts.api ? [`- [x] API contract: \`${generatedArtifacts.api.contractDir}/request.json\` and \`${generatedArtifacts.api.contractDir}/response.json\``] : []),
|
|
2490
|
+
"",
|
|
2491
|
+
"## Edge Cases",
|
|
2492
|
+
"",
|
|
2493
|
+
"- [ ] Empty input or no records available.",
|
|
2494
|
+
"- [ ] Verification catches a missing required dependency or contract drift.",
|
|
2495
|
+
"",
|
|
2496
|
+
"## Invalid Inputs",
|
|
2497
|
+
"",
|
|
2498
|
+
"- [ ] Unsupported assumptions that were not approved during discovery.",
|
|
2499
|
+
"- [ ] Scope expansion that needs a new feature plan instead of implementation work.",
|
|
2500
|
+
"",
|
|
2501
|
+
"## Expected Errors",
|
|
2502
|
+
"",
|
|
2503
|
+
"- [ ] Failing tests should clearly identify the missing first-slice behavior.",
|
|
2504
|
+
"",
|
|
2505
|
+
"## Acceptance Tests",
|
|
2506
|
+
"",
|
|
2507
|
+
`- [x] The feature remains traceable to the approved product approach: ${productApproach}`,
|
|
2508
|
+
`- [x] The execution plan stays bounded to: ${featureSeed.summary}`,
|
|
2509
|
+
"",
|
|
2510
|
+
"## Manual Verification",
|
|
2511
|
+
"",
|
|
2512
|
+
`- [x] QA owner: ${qaOwner}`,
|
|
2513
|
+
`- [x] Deployment target: ${deployment}`,
|
|
2514
|
+
...(generatedArtifacts.ui ? [`- [x] UI fixture: \`${generatedArtifacts.ui.fixturePath}\``] : []),
|
|
2515
|
+
...(generatedArtifacts.api ? [`- [x] API fixture: \`${generatedArtifacts.api.fixturePath}\``] : []),
|
|
2516
|
+
""
|
|
2517
|
+
].join("\n")
|
|
2518
|
+
);
|
|
2519
|
+
|
|
2520
|
+
writeFileIfChanged(
|
|
2521
|
+
path.join(featureDir, "TASKS.md"),
|
|
2522
|
+
[
|
|
2523
|
+
"# Tasks",
|
|
2524
|
+
"",
|
|
2525
|
+
"- [x] Read required planning docs",
|
|
2526
|
+
"- [x] Read PITFALLS.md",
|
|
2527
|
+
"- [x] Read LESSONS_LEARNED.md",
|
|
2528
|
+
"- [x] Research existing codebase patterns",
|
|
2529
|
+
"- [x] Research best practices",
|
|
2530
|
+
"- [x] Create or update Mermaid diagram",
|
|
2531
|
+
"- [x] Write SPEC.md",
|
|
2532
|
+
"- [x] Write IMPACT.md",
|
|
2533
|
+
"- [x] Write PLAN.md",
|
|
2534
|
+
"- [x] Write TEST_CONTRACT.md",
|
|
2535
|
+
"- [ ] Add failing tests or verification",
|
|
2536
|
+
"- [ ] Implement feature",
|
|
2537
|
+
"- [ ] Run verification",
|
|
2538
|
+
"- [ ] Update docs",
|
|
2539
|
+
"- [ ] Review changed files",
|
|
2540
|
+
"- [ ] Remove unnecessary files/code",
|
|
2541
|
+
"- [x] Update STATE.md",
|
|
2542
|
+
"- [x] Update FEATURE_INDEX.md",
|
|
2543
|
+
"- [ ] Update SPEC_CHANGELOG.md",
|
|
2544
|
+
"- [ ] Mark completed tasks",
|
|
2545
|
+
""
|
|
2546
|
+
].join("\n")
|
|
2547
|
+
);
|
|
2548
|
+
|
|
2549
|
+
writeFileIfChanged(
|
|
2550
|
+
path.join(featureDir, "VERIFICATION.md"),
|
|
2551
|
+
[
|
|
2552
|
+
"# Verification",
|
|
2553
|
+
"",
|
|
2554
|
+
"- [x] Define commands",
|
|
2555
|
+
"- [ ] Run commands",
|
|
2556
|
+
"- [ ] Record results",
|
|
2557
|
+
"",
|
|
2558
|
+
"| Command | Result | Evidence |",
|
|
2559
|
+
"|---|---|---|",
|
|
2560
|
+
"| `rtk bash scripts/verify.sh` | Pending | Run after the first code change for this slice |",
|
|
2561
|
+
"| `rtk bash scripts/run-evals.sh` | Pending | Run after the first code change for this slice |",
|
|
2562
|
+
"| `rtk node bin/genesis-harness.js verify-gate` | Pending | Final blocker before claiming completion |",
|
|
2563
|
+
""
|
|
2564
|
+
].join("\n")
|
|
2565
|
+
);
|
|
2566
|
+
|
|
2567
|
+
writeFileIfChanged(
|
|
2568
|
+
path.join(featureDir, "REVIEW.md"),
|
|
2569
|
+
[
|
|
2570
|
+
"# Review",
|
|
2571
|
+
"",
|
|
2572
|
+
"- [ ] Changed files reviewed",
|
|
2573
|
+
"- [ ] Missing docs checked",
|
|
2574
|
+
"- [ ] Debug logs removed",
|
|
2575
|
+
"- [ ] Unnecessary changes removed",
|
|
2576
|
+
"",
|
|
2577
|
+
"## Findings",
|
|
2578
|
+
"",
|
|
2579
|
+
"| Severity | File | Issue | Follow-Up |",
|
|
2580
|
+
"|---|---|---|---|",
|
|
2581
|
+
"| TBD | TBD | TBD | TBD |",
|
|
2582
|
+
""
|
|
2583
|
+
].join("\n")
|
|
2584
|
+
);
|
|
2585
|
+
|
|
2586
|
+
writeFileIfChanged(
|
|
2587
|
+
path.join(featureDir, "DIAGRAM.mmd"),
|
|
2588
|
+
[
|
|
2589
|
+
"flowchart LR",
|
|
2590
|
+
` User["${primaryUser}"] --> Feature["${featureSeed.summary}"]`,
|
|
2591
|
+
` Feature --> Product["${productApproach}"]`,
|
|
2592
|
+
` Product --> Verify["${testStrategy}"]`,
|
|
2593
|
+
""
|
|
2594
|
+
].join("\n")
|
|
2595
|
+
);
|
|
2596
|
+
|
|
2597
|
+
updateMarkdownFile(path.join(planningRoot, "FEATURE_INDEX.md"), (content) => {
|
|
2598
|
+
const row = `| ${featureSeed.summary} | [~] | 02 | ${featureRelativePath.replace(".planning/", "")} | Active first implementation slice |`;
|
|
2599
|
+
if (content.includes(`| ${featureSeed.summary} |`)) return content;
|
|
2600
|
+
return `${content.trim()}\n${row}\n`;
|
|
2601
|
+
});
|
|
2602
|
+
|
|
2603
|
+
writeJsonFile(path.join(planningRoot, "FEATURE_REGISTRY.json"), {
|
|
2604
|
+
version: "1.0.0",
|
|
2605
|
+
updated_at: nowIso,
|
|
2606
|
+
project_status: "implementation",
|
|
2607
|
+
project_verification: {
|
|
2608
|
+
status: "pending",
|
|
2609
|
+
verify_cmd: "",
|
|
2610
|
+
evidence: "",
|
|
2611
|
+
verified_at: ""
|
|
2612
|
+
},
|
|
2613
|
+
features: [
|
|
2614
|
+
{
|
|
2615
|
+
id: "F001",
|
|
2616
|
+
status: "in-progress",
|
|
2617
|
+
title: featureSeed.summary,
|
|
2618
|
+
path: featureRelativePath,
|
|
2619
|
+
verify_cmd: "node bin/genesis-harness.js verify-gate",
|
|
2620
|
+
evidence: "",
|
|
2621
|
+
started_at: nowIso,
|
|
2622
|
+
verified_at: "",
|
|
2623
|
+
attempts: 0,
|
|
2624
|
+
last_error: ""
|
|
2625
|
+
}
|
|
2626
|
+
]
|
|
2627
|
+
});
|
|
2628
|
+
|
|
2629
|
+
updateMarkdownFile(path.join(planningRoot, "ROADMAP.md"), (content) =>
|
|
2630
|
+
content.replace(
|
|
2631
|
+
"| TBD | Feature | [~] | 01 Discovery & QA | Ready for first feature plan |",
|
|
2632
|
+
`| 02 ${featureSeed.summary} | Feature | [~] | 01 Discovery & QA | Active first implementation slice is scaffolded and ready for tests |`
|
|
2633
|
+
)
|
|
2634
|
+
);
|
|
2635
|
+
|
|
2636
|
+
updateMarkdownFile(path.join(planningRoot, "STATE.md"), (content) => {
|
|
2637
|
+
let next = content;
|
|
2638
|
+
next = next.replace(
|
|
2639
|
+
/Current project state: .*$/m,
|
|
2640
|
+
"Current project state: [~] Active first feature execution."
|
|
2641
|
+
);
|
|
2642
|
+
next = next.replace(
|
|
2643
|
+
/Current phase: .*$/m,
|
|
2644
|
+
"Current phase: 02 First Feature Execution"
|
|
2645
|
+
);
|
|
2646
|
+
next = next.replace(
|
|
2647
|
+
/Current feature or bug: .*$/m,
|
|
2648
|
+
`Current feature or bug: ${featureRelativePath.replace(".planning/", "")}`
|
|
2649
|
+
);
|
|
2650
|
+
next = next.replace(
|
|
2651
|
+
/Last completed task: .*$/m,
|
|
2652
|
+
`Last completed task: Seeded the first execution-ready feature scaffold for ${featureSeed.summary}.`
|
|
2653
|
+
);
|
|
2654
|
+
next = next.replace(
|
|
2655
|
+
/Next task: .*$/m,
|
|
2656
|
+
`Next task: Add the first failing tests for ${featureSeed.summary}.`
|
|
2657
|
+
);
|
|
2658
|
+
next = next.replace(
|
|
2659
|
+
/Latest verification result: .*$/m,
|
|
2660
|
+
"Latest verification result: Discovery complete and first feature execution scaffolded."
|
|
2661
|
+
);
|
|
2662
|
+
return next;
|
|
2663
|
+
});
|
|
2664
|
+
|
|
2665
|
+
updateMarkdownFile(path.join(planningRoot, "SUMMARY.md"), (content) => {
|
|
2666
|
+
let next = content;
|
|
2667
|
+
next = replaceSection(next, "Current Focus", `- [x] Active feature execution started for ${featureSeed.summary}.`);
|
|
2668
|
+
next = replaceSection(
|
|
2669
|
+
next,
|
|
2670
|
+
"Recent Changes",
|
|
2671
|
+
`- [x] Discovery answers closed into an execution-ready feature scaffold.\n- [x] Active feature path: ${featureRelativePath.replace(".planning/", "")}.`
|
|
2672
|
+
);
|
|
2673
|
+
next = replaceSection(next, "Next Recommended Task", `- [ ] Add the first failing test and implement ${featureSeed.summary}.`);
|
|
2674
|
+
return next;
|
|
2675
|
+
});
|
|
2676
|
+
|
|
2677
|
+
updateMarkdownFile(path.join(planningRoot, "SPEC_CHANGELOG.md"), (content) => {
|
|
2678
|
+
const entry = `| ${nowIso} | Scaffolded first execution-ready feature: ${featureSeed.summary} | Close the bootstrap gap between discovery and implementation | .planning/features/, STATE.md, SUMMARY.md, FEATURE_INDEX.md | tests/integration/cli-smoke.test.js | None |`;
|
|
2679
|
+
if (content.includes(`Scaffolded first execution-ready feature: ${featureSeed.summary}`)) return content;
|
|
2680
|
+
return `${content.trim()}\n${entry}\n`;
|
|
2681
|
+
});
|
|
2682
|
+
|
|
2683
|
+
const currentStatePath = path.join(rootPath, ".codebase", "CURRENT_STATE.md");
|
|
2684
|
+
if (fs.existsSync(currentStatePath)) {
|
|
2685
|
+
writeFileIfChanged(
|
|
2686
|
+
currentStatePath,
|
|
2687
|
+
[
|
|
2688
|
+
"# Current System State",
|
|
2689
|
+
"",
|
|
2690
|
+
`**Time**: ${today} `,
|
|
2691
|
+
"**Status**: `IN_PROGRESS` ",
|
|
2692
|
+
`**Latest Session**: \`${today}-run-pipeline\` `,
|
|
2693
|
+
"",
|
|
2694
|
+
"## Active Bootstrap",
|
|
2695
|
+
"",
|
|
2696
|
+
`- Planning harness initialized from the user brief: ${idea}`,
|
|
2697
|
+
`- Discovery answers were promoted into the first active feature: ${featureRelativePath.replace(".planning/", "")}.`,
|
|
2698
|
+
"- Current planner phase: `IMPLEMENTATION`",
|
|
2699
|
+
`- Next task: Add the first failing tests for ${featureSeed.summary}.`
|
|
2700
|
+
].join("\n")
|
|
2701
|
+
);
|
|
2702
|
+
}
|
|
2703
|
+
|
|
2704
|
+
const statePath = path.join(rootPath, ".codebase", "state.json");
|
|
2705
|
+
if (fs.existsSync(statePath)) {
|
|
2706
|
+
const state = JSON.parse(fs.readFileSync(statePath, "utf8"));
|
|
2707
|
+
const sessionId = state.session_id || `${today}-run-pipeline`;
|
|
2708
|
+
state.current_state = "IMPLEMENTATION";
|
|
2709
|
+
state.active_work = `Implement ${featureName}`;
|
|
2710
|
+
state.active_feature = featureRelativePath;
|
|
2711
|
+
state.session_id = sessionId;
|
|
2712
|
+
state.session_started_at = state.session_started_at || nowIso;
|
|
2713
|
+
state.last_updated_at = nowIso;
|
|
2714
|
+
state.latest_recovery_point = "First feature execution scaffolded";
|
|
2715
|
+
state.required_verification = [
|
|
2716
|
+
`Review ${featureRelativePath}/SPEC.md`,
|
|
2717
|
+
`Review ${featureRelativePath}/TEST_CONTRACT.md`,
|
|
2718
|
+
"Add the first failing test for the active slice",
|
|
2719
|
+
"rtk bash scripts/verify.sh",
|
|
2720
|
+
"rtk bash scripts/run-evals.sh",
|
|
2721
|
+
"rtk node bin/genesis-harness.js verify-gate"
|
|
2722
|
+
];
|
|
2723
|
+
state.pending_tasks = [
|
|
2724
|
+
"Add the first failing test",
|
|
2725
|
+
"Implement the first feature slice",
|
|
2726
|
+
"Run verification and record evidence"
|
|
2727
|
+
];
|
|
2728
|
+
writeJsonFile(statePath, state);
|
|
2729
|
+
persistRunArtifacts(rootPath, {
|
|
2730
|
+
sessionId,
|
|
2731
|
+
state,
|
|
2732
|
+
answers: state.discovery_answers || {
|
|
2733
|
+
idea,
|
|
2734
|
+
product_approach: productApproach,
|
|
2735
|
+
primary_user: primaryUser,
|
|
2736
|
+
v1_outcome: v1Outcome,
|
|
2737
|
+
qa_owner: qaOwner,
|
|
2738
|
+
backend,
|
|
2739
|
+
frontend,
|
|
2740
|
+
database,
|
|
2741
|
+
deployment,
|
|
2742
|
+
test_strategy: testStrategy,
|
|
2743
|
+
stack_owner: stackOwner,
|
|
2744
|
+
captured_at: nowIso
|
|
2745
|
+
},
|
|
2746
|
+
idea,
|
|
2747
|
+
recordedAt: nowIso
|
|
2748
|
+
});
|
|
2749
|
+
}
|
|
2750
|
+
|
|
2751
|
+
return featureRelativePath;
|
|
2752
|
+
}
|
|
2753
|
+
|
|
2754
|
+
function runBootstrapPipeline({ rootPath = process.cwd(), options }) {
|
|
2755
|
+
initializeProject({
|
|
2756
|
+
rootPath,
|
|
2757
|
+
platform: options.platform || "codex",
|
|
2758
|
+
idea: options.idea
|
|
2759
|
+
});
|
|
2760
|
+
completeDiscoveryPhase(rootPath, options);
|
|
2761
|
+
const featurePath = seedFirstFeatureExecution(rootPath, options);
|
|
2762
|
+
console.log("\n\x1b[1m\x1b[32m✓ Run pipeline complete.\x1b[0m");
|
|
2763
|
+
console.log(`Discovery answers recorded and ${featurePath} is ready for execution.\n`);
|
|
2764
|
+
}
|
|
2765
|
+
|
|
2766
|
+
function discoverMockups(rootPath = packageRoot) {
|
|
2767
|
+
const mockups = [];
|
|
2768
|
+
const featuresDir = path.join(rootPath, ".planning", "features");
|
|
2769
|
+
const bugsDir = path.join(rootPath, ".planning", "bugs");
|
|
2770
|
+
|
|
2771
|
+
const scanDir = (dir, type) => {
|
|
2772
|
+
if (!fs.existsSync(dir)) return;
|
|
2773
|
+
const entries = fs.readdirSync(dir);
|
|
2774
|
+
for (const entry of entries) {
|
|
2775
|
+
const fullPath = path.join(dir, entry);
|
|
2776
|
+
if (fs.statSync(fullPath).isDirectory()) {
|
|
2777
|
+
const files = fs.readdirSync(fullPath);
|
|
2778
|
+
for (const file of files) {
|
|
2779
|
+
const ext = path.extname(file).toLowerCase();
|
|
2780
|
+
if ([".png", ".jpg", ".jpeg", ".webp"].includes(ext)) {
|
|
2781
|
+
mockups.push({
|
|
2782
|
+
id: `${type}-${entry}-${file}`,
|
|
2783
|
+
title: `${type === "feature" ? "[Feature]" : "[Bug]"} ${entry}`,
|
|
2784
|
+
folder: entry,
|
|
2785
|
+
fileName: file,
|
|
2786
|
+
fullPath: path.join(fullPath, file)
|
|
2787
|
+
});
|
|
2788
|
+
}
|
|
2789
|
+
}
|
|
2790
|
+
}
|
|
2791
|
+
}
|
|
2792
|
+
};
|
|
2793
|
+
|
|
2794
|
+
scanDir(featuresDir, "feature");
|
|
2795
|
+
scanDir(bugsDir, "bug");
|
|
2796
|
+
return mockups;
|
|
2797
|
+
}
|
|
2798
|
+
|
|
2799
|
+
function viewMockupsInteractive(arg) {
|
|
2800
|
+
if (arg) {
|
|
2801
|
+
if (fs.existsSync(arg) && fs.statSync(arg).isFile()) {
|
|
2802
|
+
console.log(`\n\x1b[1m\x1b[32m[+] Opening direct file:\x1b[0m ${arg}`);
|
|
2803
|
+
openFileNatively(arg);
|
|
2804
|
+
return;
|
|
2805
|
+
}
|
|
2806
|
+
|
|
2807
|
+
const mockups = discoverMockups();
|
|
2808
|
+
const found = mockups.find(m => m.folder === arg || m.id === arg);
|
|
2809
|
+
if (found) {
|
|
2810
|
+
console.log(`\n\x1b[1m\x1b[32m[+] Opening mockup for ${found.title}:\x1b[0m ${found.fileName}`);
|
|
2811
|
+
openFileNatively(found.fullPath);
|
|
2812
|
+
return;
|
|
2813
|
+
}
|
|
2814
|
+
|
|
2815
|
+
console.error(`\x1b[31mError: No mockup found matching direct path or slug "${arg}".\x1b[0m`);
|
|
2816
|
+
process.exit(1);
|
|
2817
|
+
}
|
|
2818
|
+
|
|
2819
|
+
const mockups = discoverMockups();
|
|
2820
|
+
if (mockups.length === 0) {
|
|
2821
|
+
console.log("\n\x1b[33m[-] No mockup images (.png, .jpg, .webp) found under .planning/features/ or .planning/bugs/.\x1b[0m\n");
|
|
2822
|
+
return;
|
|
2823
|
+
}
|
|
2824
|
+
|
|
2825
|
+
let selectedIndex = 0;
|
|
2826
|
+
let currentView = "LIST"; // "LIST" or "DETAIL"
|
|
2827
|
+
|
|
2828
|
+
const renderMenu = () => {
|
|
2829
|
+
console.clear();
|
|
2830
|
+
console.log("\x1b[1m\x1b[36m======================================================================\x1b[0m");
|
|
2831
|
+
console.log("\x1b[1m\x1b[36m GENESIS HARNESS - MOCKUP GALLERY VIEWER \x1b[0m");
|
|
2832
|
+
console.log("\x1b[1m\x1b[36m======================================================================\x1b[0m\n");
|
|
2833
|
+
|
|
2834
|
+
if (currentView === "LIST") {
|
|
2835
|
+
console.log(" \x1b[1mUse Up/Down Arrow to navigate, Right Arrow (or Enter) to view.\x1b[0m");
|
|
2836
|
+
console.log(" \x1b[90mPress Esc or Ctrl+C to exit.\x1b[0m\n");
|
|
2837
|
+
console.log(" \x1b[1mDISCOVERED SCREENS / MOCKUPS:\x1b[0m");
|
|
2838
|
+
console.log(" ------------------------------------------------------------------");
|
|
2839
|
+
|
|
2840
|
+
mockups.forEach((mockup, idx) => {
|
|
2841
|
+
if (idx === selectedIndex) {
|
|
2842
|
+
console.log(` \x1b[1m\x1b[36m➔ ${mockup.title} (${mockup.fileName})\x1b[0m`);
|
|
2843
|
+
} else {
|
|
2844
|
+
console.log(` \x1b[90m${mockup.title} (${mockup.fileName})\x1b[0m`);
|
|
2845
|
+
}
|
|
2846
|
+
});
|
|
2847
|
+
console.log(" ------------------------------------------------------------------\n");
|
|
2848
|
+
} else if (currentView === "DETAIL") {
|
|
2849
|
+
const selected = mockups[selectedIndex];
|
|
2850
|
+
console.log(" \x1b[1m\x1b[32m[+] LAUNCHED SYSTEM VIEW FOR:\x1b[0m \x1b[1m" + selected.title + "\x1b[0m\n");
|
|
2851
|
+
console.log(` - \x1b[1mMockup File:\x1b[0m ${selected.fileName}`);
|
|
2852
|
+
console.log(` - \x1b[1mFolder Path:\x1b[0m ${path.dirname(selected.fullPath)}`);
|
|
2853
|
+
|
|
2854
|
+
let sizeText = "Unknown";
|
|
2855
|
+
try {
|
|
2856
|
+
const stats = fs.statSync(selected.fullPath);
|
|
2857
|
+
sizeText = `${(stats.size / 1024).toFixed(1)} KB`;
|
|
2858
|
+
} catch (e) {}
|
|
2859
|
+
console.log(` - \x1b[1mFile Size:\x1b[0m ${sizeText}`);
|
|
2860
|
+
console.log("");
|
|
2861
|
+
console.log(" ==================================================================");
|
|
2862
|
+
console.log(" \x1b[36m[OS SYSTEM PREVIEW LAUNCHED]\x1b[0m");
|
|
2863
|
+
console.log(" The mockup has been opened in your system's native image viewer.");
|
|
2864
|
+
console.log(" ==================================================================\n");
|
|
2865
|
+
console.log(" \x1b[1m\x1b[33m← Press Left Arrow to go BACK to list.\x1b[0m");
|
|
2866
|
+
console.log(" \x1b[90mPress Esc or Ctrl+C to exit.\x1b[0m\n");
|
|
2867
|
+
}
|
|
2868
|
+
console.log("\x1b[1m\x1b[36m======================================================================\x1b[0m");
|
|
2869
|
+
};
|
|
2870
|
+
|
|
2871
|
+
process.stdin.setRawMode(true);
|
|
2872
|
+
process.stdin.resume();
|
|
2873
|
+
process.stdin.setEncoding("utf8");
|
|
2874
|
+
|
|
2875
|
+
const cleanExit = () => {
|
|
2876
|
+
process.stdin.setRawMode(false);
|
|
2877
|
+
process.stdin.pause();
|
|
2878
|
+
console.clear();
|
|
2879
|
+
console.log("\n\x1b[32m[+] Exited Mockup Gallery Viewer.\x1b[0m\n");
|
|
2880
|
+
process.exit(0);
|
|
2881
|
+
};
|
|
2882
|
+
|
|
2883
|
+
renderMenu();
|
|
2884
|
+
|
|
2885
|
+
process.stdin.on("data", (key) => {
|
|
2886
|
+
if (key === "\u0003" || key === "\u001b") {
|
|
2887
|
+
cleanExit();
|
|
2888
|
+
}
|
|
2889
|
+
|
|
2890
|
+
if (currentView === "LIST") {
|
|
2891
|
+
if (key === "\u001b[A") {
|
|
2892
|
+
selectedIndex = (selectedIndex - 1 + mockups.length) % mockups.length;
|
|
2893
|
+
renderMenu();
|
|
2894
|
+
}
|
|
2895
|
+
else if (key === "\u001b[B") {
|
|
2896
|
+
selectedIndex = (selectedIndex + 1) % mockups.length;
|
|
2897
|
+
renderMenu();
|
|
2898
|
+
}
|
|
2899
|
+
else if (key === "\u001b[C" || key === "\r") {
|
|
2900
|
+
currentView = "DETAIL";
|
|
2901
|
+
const selected = mockups[selectedIndex];
|
|
2902
|
+
openFileNatively(selected.fullPath);
|
|
2903
|
+
renderMenu();
|
|
2904
|
+
}
|
|
2905
|
+
} else if (currentView === "DETAIL") {
|
|
2906
|
+
if (key === "\u001b[D") {
|
|
2907
|
+
currentView = "LIST";
|
|
2908
|
+
renderMenu();
|
|
2909
|
+
}
|
|
2910
|
+
}
|
|
2911
|
+
});
|
|
2912
|
+
}
|
|
2913
|
+
|
|
2914
|
+
function syncContext() {
|
|
2915
|
+
const srcDirs = ['src', 'lib', 'tests', 'bin'];
|
|
2916
|
+
const codebaseDir = path.join(process.cwd(), '.codebase');
|
|
2917
|
+
const contextFile = path.join(codebaseDir, 'COMPRESSED_CONTEXT.md');
|
|
2918
|
+
const visualFile = path.join(codebaseDir, 'VISUAL_GRAPH.md');
|
|
2919
|
+
|
|
2920
|
+
if (!fs.existsSync(codebaseDir)) {
|
|
2921
|
+
fs.mkdirSync(codebaseDir, { recursive: true });
|
|
2922
|
+
}
|
|
2923
|
+
|
|
2924
|
+
let output = '# Compressed Context & Dependency Graph\n\n';
|
|
2925
|
+
let visualOutput = '# Visual Project Graph\n\n';
|
|
2926
|
+
const depEdges = [];
|
|
2927
|
+
|
|
2928
|
+
let parser, traverse;
|
|
2929
|
+
try {
|
|
2930
|
+
parser = require('@babel/parser');
|
|
2931
|
+
traverse = require('@babel/traverse').default;
|
|
2932
|
+
} catch (e) {
|
|
2933
|
+
console.error('[genesis-harness] AST parser dependencies missing. Run: npm install');
|
|
2934
|
+
process.exit(1);
|
|
2935
|
+
}
|
|
2936
|
+
|
|
2937
|
+
function walk(dir) {
|
|
2938
|
+
if (!fs.existsSync(dir)) return;
|
|
2939
|
+
const files = fs.readdirSync(dir);
|
|
2940
|
+
for (const file of files) {
|
|
2941
|
+
const fullPath = path.join(dir, file);
|
|
2942
|
+
const stat = fs.statSync(fullPath);
|
|
2943
|
+
if (stat.isDirectory() && file !== 'node_modules') {
|
|
2944
|
+
walk(fullPath);
|
|
2945
|
+
} else if (file.endsWith('.js') || file.endsWith('.ts')) {
|
|
2946
|
+
const content = fs.readFileSync(fullPath, 'utf8');
|
|
2947
|
+
const exportsList = [];
|
|
2948
|
+
const importsList = [];
|
|
2949
|
+
const relativePath = fullPath.replace(process.cwd() + '/', '');
|
|
2950
|
+
const featuresList = [];
|
|
2951
|
+
|
|
2952
|
+
try {
|
|
2953
|
+
const ast = parser.parse(content, {
|
|
2954
|
+
sourceType: 'module',
|
|
2955
|
+
plugins: ['typescript', 'jsx']
|
|
2956
|
+
});
|
|
2957
|
+
|
|
2958
|
+
if (ast.comments) {
|
|
2959
|
+
ast.comments.forEach(comment => {
|
|
2960
|
+
const match = comment.value.match(/@feature:\s*(.+)/i);
|
|
2961
|
+
if (match) {
|
|
2962
|
+
featuresList.push(match[1].trim());
|
|
2963
|
+
}
|
|
2964
|
+
});
|
|
2965
|
+
}
|
|
2966
|
+
|
|
2967
|
+
traverse(ast, {
|
|
2968
|
+
ExportNamedDeclaration(path) {
|
|
2969
|
+
const decl = path.node.declaration;
|
|
2970
|
+
if (decl) {
|
|
2971
|
+
if (decl.type === 'ClassDeclaration' && decl.id) {
|
|
2972
|
+
exportsList.push('class ' + decl.id.name);
|
|
2973
|
+
} else if (decl.type === 'FunctionDeclaration' && decl.id) {
|
|
2974
|
+
exportsList.push('function ' + decl.id.name);
|
|
2975
|
+
} else if (decl.type === 'VariableDeclaration') {
|
|
2976
|
+
decl.declarations.forEach(d => {
|
|
2977
|
+
if (d.id) exportsList.push('const ' + d.id.name);
|
|
2978
|
+
});
|
|
2979
|
+
}
|
|
2980
|
+
}
|
|
2981
|
+
},
|
|
2982
|
+
ImportDeclaration(path) {
|
|
2983
|
+
importsList.push(path.node.source.value);
|
|
2984
|
+
depEdges.push(` "${relativePath}" --> "${path.node.source.value}"`);
|
|
2985
|
+
},
|
|
2986
|
+
CallExpression(path) {
|
|
2987
|
+
if (path.node.callee.name === 'require' && path.node.arguments.length > 0) {
|
|
2988
|
+
if (path.node.arguments[0].type === 'StringLiteral') {
|
|
2989
|
+
importsList.push(path.node.arguments[0].value);
|
|
2990
|
+
depEdges.push(` "${relativePath}" --> "${path.node.arguments[0].value}"`);
|
|
2991
|
+
}
|
|
2992
|
+
}
|
|
2993
|
+
}
|
|
2994
|
+
});
|
|
2995
|
+
} catch (err) {
|
|
2996
|
+
exportsList.push('// AST Parse Error: ' + err.message);
|
|
2997
|
+
}
|
|
2998
|
+
|
|
2999
|
+
if (exportsList.length > 0 || importsList.length > 0 || featuresList.length > 0) {
|
|
3000
|
+
output += '## ' + relativePath + '\n';
|
|
3001
|
+
if (featuresList.length > 0) {
|
|
3002
|
+
output += '### Implements Features\n';
|
|
3003
|
+
featuresList.forEach(f => output += '- `' + f + '`\n');
|
|
3004
|
+
}
|
|
3005
|
+
if (exportsList.length > 0) {
|
|
3006
|
+
output += '### Exports\n';
|
|
3007
|
+
exportsList.forEach(sig => output += '- `' + sig + '`\n');
|
|
3008
|
+
}
|
|
3009
|
+
if (importsList.length > 0) {
|
|
3010
|
+
output += '### Dependencies\n';
|
|
3011
|
+
importsList.forEach(imp => output += '- `' + imp + '`\n');
|
|
3012
|
+
}
|
|
3013
|
+
output += '\n';
|
|
3014
|
+
}
|
|
3015
|
+
}
|
|
3016
|
+
}
|
|
3017
|
+
}
|
|
3018
|
+
|
|
3019
|
+
srcDirs.forEach(dir => walk(path.join(process.cwd(), dir)));
|
|
3020
|
+
// Generate Visual Graph
|
|
3021
|
+
visualOutput += '## Harness Relationship Map\n\n```mermaid\nflowchart LR\n';
|
|
3022
|
+
visualOutput += ' manifest[".codex-plugin/plugin.json"] --> skills[".codex/skills/*"]\n';
|
|
3023
|
+
visualOutput += ' package["package.json"] --> cli["bin/genesis-harness.js"]\n';
|
|
3024
|
+
visualOutput += ' package --> verify["scripts/verify.sh"]\n';
|
|
3025
|
+
visualOutput += ' package --> evals["scripts/run-evals.sh"]\n';
|
|
3026
|
+
visualOutput += ' cli --> install["install / postinstall"]\n';
|
|
3027
|
+
visualOutput += ' cli --> hooks["setup-hooks"]\n';
|
|
3028
|
+
visualOutput += ' hooks --> docsgate["genesis-harness docs-gate"]\n';
|
|
3029
|
+
visualOutput += ' docsgate --> docsync["check-docs-sync.sh"]\n';
|
|
3030
|
+
visualOutput += ' docsgate --> specsync["check-spec-changelog.sh"]\n';
|
|
3031
|
+
visualOutput += ' skills --> contracts["contracts/"]\n';
|
|
3032
|
+
visualOutput += ' skills --> fixtures["fixtures/"]\n';
|
|
3033
|
+
visualOutput += ' skills --> tests["tests/ + playwright/"]\n';
|
|
3034
|
+
visualOutput += ' skills --> memory[".codebase/"]\n';
|
|
3035
|
+
visualOutput += ' verify --> skills\n';
|
|
3036
|
+
visualOutput += ' verify --> contracts\n';
|
|
3037
|
+
visualOutput += ' verify --> fixtures\n';
|
|
3038
|
+
visualOutput += ' verify --> memory\n';
|
|
3039
|
+
visualOutput += ' evals --> install\n';
|
|
3040
|
+
visualOutput += ' evals --> cli\n';
|
|
3041
|
+
visualOutput += ' evals --> unit["tests/unit/*.test.js"]\n';
|
|
3042
|
+
visualOutput += ' evals --> integration["tests/integration/*.test.js"]\n';
|
|
3043
|
+
visualOutput += ' evals --> pack["npm pack smoke"]\n';
|
|
3044
|
+
visualOutput += '```\n\n';
|
|
3045
|
+
|
|
3046
|
+
visualOutput += '## Skill Workflow Relationships\n\n```mermaid\nflowchart TD\n';
|
|
3047
|
+
visualOutput += ' harness["genesis-harness"] --> planning["genesis-planning"]\n';
|
|
3048
|
+
visualOutput += ' harness --> research["genesis-research-first"]\n';
|
|
3049
|
+
visualOutput += ' planning --> architecture["genesis-architecture"]\n';
|
|
3050
|
+
visualOutput += ' planning --> api["genesis-api-contract"]\n';
|
|
3051
|
+
visualOutput += ' planning --> design["genesis-design-spec"]\n';
|
|
3052
|
+
visualOutput += ' api --> apisync["genesis-api-sync"]\n';
|
|
3053
|
+
visualOutput += ' design --> ui["genesis-ui-ux-test"]\n';
|
|
3054
|
+
visualOutput += ' api --> specimpact["spec-impact-engine"]\n';
|
|
3055
|
+
visualOutput += ' specimpact --> specprop["genesis-spec-propagation"]\n';
|
|
3056
|
+
visualOutput += ' specprop --> docs["genesis-docs-automation"]\n';
|
|
3057
|
+
visualOutput += ' ui --> verifybefore["genesis-verification-before-completion"]\n';
|
|
3058
|
+
visualOutput += ' apisync --> verifybefore\n';
|
|
3059
|
+
visualOutput += ' docs --> verifybefore\n';
|
|
3060
|
+
visualOutput += ' verifybefore --> release["genesis-release"]\n';
|
|
3061
|
+
visualOutput += ' harness --> memorymap["genesis-codebase-map"]\n';
|
|
3062
|
+
visualOutput += ' harness --> observability["genesis-observability-automation"]\n';
|
|
3063
|
+
visualOutput += '```\n\n';
|
|
3064
|
+
|
|
3065
|
+
visualOutput += '## Code Dependency Hints\n\n```mermaid\nflowchart TD\n';
|
|
3066
|
+
if (depEdges.length > 0) {
|
|
3067
|
+
visualOutput += depEdges.join('\n') + '\n';
|
|
3068
|
+
} else {
|
|
3069
|
+
visualOutput += ' Root["No dependencies found"]\n';
|
|
3070
|
+
}
|
|
3071
|
+
visualOutput += '```\n\n';
|
|
3072
|
+
|
|
3073
|
+
// Parse Roadmap for features and roles
|
|
3074
|
+
const roadmapFile = path.join(process.cwd(), '.planning', 'ROADMAP.md');
|
|
3075
|
+
if (fs.existsSync(roadmapFile)) {
|
|
3076
|
+
visualOutput += '## .planning/ROADMAP.md Derived Feature Status\n\n```mermaid\ngraph TD\n';
|
|
3077
|
+
visualOutput += ' classDef completed fill:#d4edda,stroke:#28a745,stroke-width:2px;\n';
|
|
3078
|
+
visualOutput += ' classDef inprogress fill:#fff3cd,stroke:#ffc107,stroke-width:2px;\n';
|
|
3079
|
+
visualOutput += ' classDef pending fill:#e2e3e5,stroke:#6c757d,stroke-width:2px;\n';
|
|
3080
|
+
|
|
3081
|
+
const rmContent = fs.readFileSync(roadmapFile, 'utf8').split('\n');
|
|
3082
|
+
const roles = [];
|
|
3083
|
+
let currentRoleObj = { title: 'General', tasks: [] };
|
|
3084
|
+
|
|
3085
|
+
let taskIdCounter = 0;
|
|
3086
|
+
const allTasksMap = new Map();
|
|
3087
|
+
|
|
3088
|
+
rmContent.forEach(line => {
|
|
3089
|
+
if (line.match(/^#+\s+(.+)/)) {
|
|
3090
|
+
const title = line.match(/^#+\s+(.+)/)[1].trim();
|
|
3091
|
+
// If switching roles, push the current one if it has tasks
|
|
3092
|
+
if (currentRoleObj.tasks.length > 0) {
|
|
3093
|
+
roles.push(currentRoleObj);
|
|
3094
|
+
}
|
|
3095
|
+
currentRoleObj = { title: title, tasks: [] };
|
|
3096
|
+
} else if (line.match(/^-\s*\[([ xX~!\/])\]\s+(.+)/)) {
|
|
3097
|
+
const match = line.match(/^-\s*\[([ xX~!\/])\]\s+(.+)/);
|
|
3098
|
+
const statusChar = match[1].toLowerCase();
|
|
3099
|
+
let rawName = match[2].trim();
|
|
3100
|
+
let dependsOn = [];
|
|
3101
|
+
let mappedFiles = [];
|
|
3102
|
+
|
|
3103
|
+
const depMatch = rawName.match(/\(depends_on:\s*(.+?)\)/i);
|
|
3104
|
+
if (depMatch) {
|
|
3105
|
+
dependsOn = depMatch[1].split(',').map(s => s.trim());
|
|
3106
|
+
rawName = rawName.replace(/\(depends_on:\s*.+?\)/i, '').trim();
|
|
3107
|
+
}
|
|
3108
|
+
|
|
3109
|
+
const filesMatch = rawName.match(/\(files:\s*(.+?)\)/i);
|
|
3110
|
+
if (filesMatch) {
|
|
3111
|
+
mappedFiles = filesMatch[1].split(',').map(s => s.trim());
|
|
3112
|
+
rawName = rawName.replace(/\(files:\s*.+?\)/i, '').trim();
|
|
3113
|
+
}
|
|
3114
|
+
|
|
3115
|
+
const taskId = `Task${taskIdCounter++}`;
|
|
3116
|
+
allTasksMap.set(rawName.toLowerCase(), taskId);
|
|
3117
|
+
|
|
3118
|
+
currentRoleObj.tasks.push({
|
|
3119
|
+
id: taskId,
|
|
3120
|
+
statusChar: statusChar,
|
|
3121
|
+
name: rawName,
|
|
3122
|
+
dependsOn: dependsOn,
|
|
3123
|
+
mappedFiles: mappedFiles
|
|
3124
|
+
});
|
|
3125
|
+
}
|
|
3126
|
+
});
|
|
3127
|
+
if (currentRoleObj.tasks.length > 0) {
|
|
3128
|
+
roles.push(currentRoleObj);
|
|
3129
|
+
}
|
|
3130
|
+
|
|
3131
|
+
if (roles.length === 0) {
|
|
3132
|
+
visualOutput += ' Project["Project Roadmap"] --> NoTasks["No tasks found"]\n';
|
|
3133
|
+
} else {
|
|
3134
|
+
roles.forEach((r, idx) => {
|
|
3135
|
+
visualOutput += ` subgraph Role_${idx} ["${r.title}"]\n`;
|
|
3136
|
+
r.tasks.forEach(t => {
|
|
3137
|
+
let label = `Roadmap task ${t.id.replace('Task', '')}`;
|
|
3138
|
+
visualOutput += ` ${t.id}["${label}"]\n`;
|
|
3139
|
+
if (t.statusChar === 'x') {
|
|
3140
|
+
visualOutput += ` class ${t.id} completed;\n`;
|
|
3141
|
+
} else if (t.statusChar === '/' || t.statusChar === '~' || t.statusChar === '!') {
|
|
3142
|
+
visualOutput += ` class ${t.id} inprogress;\n`;
|
|
3143
|
+
} else {
|
|
3144
|
+
visualOutput += ` class ${t.id} pending;\n`;
|
|
3145
|
+
}
|
|
3146
|
+
});
|
|
3147
|
+
visualOutput += ` end\n`;
|
|
3148
|
+
});
|
|
3149
|
+
|
|
3150
|
+
// Draw dependencies
|
|
3151
|
+
roles.forEach(r => {
|
|
3152
|
+
r.tasks.forEach(t => {
|
|
3153
|
+
if (t.dependsOn.length > 0) {
|
|
3154
|
+
t.dependsOn.forEach(depName => {
|
|
3155
|
+
const depId = allTasksMap.get(depName.toLowerCase());
|
|
3156
|
+
if (depId) {
|
|
3157
|
+
visualOutput += ` ${depId} --> ${t.id}\n`;
|
|
3158
|
+
}
|
|
3159
|
+
});
|
|
3160
|
+
}
|
|
3161
|
+
});
|
|
3162
|
+
});
|
|
3163
|
+
}
|
|
3164
|
+
visualOutput += '```\n\n';
|
|
3165
|
+
|
|
3166
|
+
// Đưa Roadmap vào COMPRESSED_CONTEXT.md cho AI đọc (Dạng text thuần)
|
|
3167
|
+
output += '\n## Project Planning & Roadmap\n';
|
|
3168
|
+
output += rmContent.join('\n') + '\n';
|
|
3169
|
+
}
|
|
3170
|
+
|
|
3171
|
+
fs.writeFileSync(contextFile, output);
|
|
3172
|
+
fs.writeFileSync(visualFile, visualOutput);
|
|
3173
|
+
console.log('[genesis-harness] Context compressed and saved to ' + contextFile);
|
|
3174
|
+
console.log('[genesis-harness] Visual Graph saved to ' + visualFile);
|
|
3175
|
+
}
|
|
3176
|
+
|
|
3177
|
+
function setupHooks(rootPath = process.cwd()) {
|
|
3178
|
+
if (!rootPath) {
|
|
3179
|
+
console.log('[genesis-harness] Project root not detected, skipping hooks setup.');
|
|
3180
|
+
return;
|
|
3181
|
+
}
|
|
3182
|
+
|
|
3183
|
+
const hooksDir = path.join(rootPath, '.git', 'hooks');
|
|
3184
|
+
const preCommitFile = path.join(hooksDir, 'pre-commit');
|
|
3185
|
+
|
|
3186
|
+
if (!fs.existsSync(hooksDir)) {
|
|
3187
|
+
console.log('[genesis-harness] Not a git repository, skipping hooks setup.');
|
|
3188
|
+
return;
|
|
3189
|
+
}
|
|
3190
|
+
|
|
3191
|
+
const hookContent = `#!/bin/sh
|
|
3192
|
+
# genesis-harness auto-sync
|
|
3193
|
+
echo "[genesis-harness] Syncing compressed context before commit..."
|
|
3194
|
+
npx genesis-harness sync
|
|
3195
|
+
echo "[genesis-harness] Running docs drift gate..."
|
|
3196
|
+
npx genesis-harness docs-gate
|
|
3197
|
+
git add .codebase/COMPRESSED_CONTEXT.md .codebase/VISUAL_GRAPH.md 2>/dev/null || true
|
|
3198
|
+
`;
|
|
3199
|
+
|
|
3200
|
+
if (fs.existsSync(preCommitFile)) {
|
|
3201
|
+
const existingContent = fs.readFileSync(preCommitFile, 'utf8');
|
|
3202
|
+
if (existingContent !== hookContent) {
|
|
3203
|
+
const backupPath = preCommitFile + '.backup.' + Date.now();
|
|
3204
|
+
fs.renameSync(preCommitFile, backupPath);
|
|
3205
|
+
console.log('[genesis-harness] Existing pre-commit hook backed up to ' + backupPath);
|
|
3206
|
+
} else {
|
|
3207
|
+
console.log('[genesis-harness] Git hooks already up to date.');
|
|
3208
|
+
return;
|
|
3209
|
+
}
|
|
3210
|
+
}
|
|
3211
|
+
|
|
3212
|
+
fs.writeFileSync(preCommitFile, hookContent);
|
|
3213
|
+
fs.chmodSync(preCommitFile, '755');
|
|
3214
|
+
console.log('[genesis-harness] Git pre-commit hook installed successfully.');
|
|
3215
|
+
}
|
|
3216
|
+
|
|
3217
|
+
function runDocsGate() {
|
|
3218
|
+
const docsSyncScript = path.join(packageRoot, ".codex", "skills", "genesis-harness", "scripts", "check-docs-sync.sh");
|
|
3219
|
+
const specChangelogScript = path.join(packageRoot, ".codex", "skills", "genesis-harness", "scripts", "check-spec-changelog.sh");
|
|
3220
|
+
const bash = resolveBash();
|
|
3221
|
+
|
|
3222
|
+
if (!fs.existsSync(docsSyncScript)) fail(`missing docs sync gate at ${docsSyncScript}`);
|
|
3223
|
+
|
|
3224
|
+
const docsResult = spawnSync(bash, [docsSyncScript, process.cwd()], {
|
|
3225
|
+
stdio: "inherit",
|
|
3226
|
+
env: process.env
|
|
3227
|
+
});
|
|
3228
|
+
if (docsResult.status) process.exit(docsResult.status);
|
|
3229
|
+
|
|
3230
|
+
if (fs.existsSync(path.join(process.cwd(), ".planning", "SPEC_CHANGELOG.md")) && fs.existsSync(specChangelogScript)) {
|
|
3231
|
+
const specResult = spawnSync(bash, [specChangelogScript, process.cwd()], {
|
|
3232
|
+
stdio: "inherit",
|
|
3233
|
+
env: process.env
|
|
3234
|
+
});
|
|
3235
|
+
if (specResult.status) process.exit(specResult.status);
|
|
3236
|
+
}
|
|
3237
|
+
}
|
|
3238
|
+
|
|
3239
|
+
/**
|
|
3240
|
+
* runVerifyGate() — L09 Victory Blocker
|
|
3241
|
+
*
|
|
3242
|
+
* Runs ALL required verification gates in sequence.
|
|
3243
|
+
* Agent MUST call this before claiming any task is done.
|
|
3244
|
+
* Exits with non-zero if any gate fails — prevents "under-finish" hallucination.
|
|
3245
|
+
*/
|
|
3246
|
+
function runVerifyGate() {
|
|
3247
|
+
const bash = resolveBash();
|
|
3248
|
+
const verifyScript = path.join(packageRoot, "scripts", "verify.sh");
|
|
3249
|
+
const evalsScript = path.join(packageRoot, "scripts", "run-evals.sh");
|
|
3250
|
+
const coldStartScript = path.join(packageRoot, "scripts", "cold-start-check.js");
|
|
3251
|
+
const cliPath = path.join(packageRoot, "bin", "genesis-harness.js");
|
|
3252
|
+
const npmCmd = process.platform === "win32" ? "npm.cmd" : "npm";
|
|
3253
|
+
|
|
3254
|
+
console.log("\x1b[1m\x1b[36m══════════════════════════════════════════════════════\x1b[0m");
|
|
3255
|
+
console.log("\x1b[1m\x1b[36m GENESIS HARNESS — VERIFY-GATE (L09 Victory Blocker) \x1b[0m");
|
|
3256
|
+
console.log("\x1b[1m\x1b[36m══════════════════════════════════════════════════════\x1b[0m");
|
|
3257
|
+
console.log("\x1b[33mRunning all verification gates. Task is NOT done until all pass.\x1b[0m\n");
|
|
3258
|
+
|
|
3259
|
+
const gates = [
|
|
3260
|
+
{
|
|
3261
|
+
name: "1. Structural verify (verify.sh)",
|
|
3262
|
+
run: () => spawnSync(bash, [verifyScript], { cwd: packageRoot, stdio: "inherit", env: process.env }).status
|
|
3263
|
+
},
|
|
3264
|
+
{
|
|
3265
|
+
name: "2. Eval regression suite (run-evals.sh)",
|
|
3266
|
+
run: () => spawnSync(bash, [evalsScript], { cwd: packageRoot, stdio: "inherit", env: process.env }).status
|
|
3267
|
+
},
|
|
3268
|
+
{
|
|
3269
|
+
name: "3. Documentation drift gate (docs-gate)",
|
|
3270
|
+
run: () => spawnSync(process.execPath, [cliPath, "docs-gate"], {
|
|
3271
|
+
cwd: packageRoot,
|
|
3272
|
+
stdio: "inherit",
|
|
3273
|
+
env: process.env
|
|
3274
|
+
}).status
|
|
3275
|
+
},
|
|
3276
|
+
{
|
|
3277
|
+
name: "4. Cold-start check (cold-start-check.js)",
|
|
3278
|
+
run: () => fs.existsSync(coldStartScript)
|
|
3279
|
+
? spawnSync(process.execPath, [coldStartScript], { cwd: packageRoot, stdio: "inherit", env: process.env }).status
|
|
3280
|
+
: 0
|
|
3281
|
+
},
|
|
3282
|
+
{
|
|
3283
|
+
name: "5. Package dry-run (npm run pack:check)",
|
|
3284
|
+
run: () => spawnSync(npmCmd, ["run", "pack:check"], {
|
|
3285
|
+
cwd: packageRoot,
|
|
3286
|
+
stdio: "inherit",
|
|
3287
|
+
env: process.env
|
|
3288
|
+
}).status
|
|
3289
|
+
},
|
|
3290
|
+
{
|
|
3291
|
+
name: "6. Lean context report (genesis-harness leanctx)",
|
|
3292
|
+
run: () => spawnSync(process.execPath, [cliPath, "leanctx"], {
|
|
3293
|
+
cwd: packageRoot,
|
|
3294
|
+
stdio: "inherit",
|
|
3295
|
+
env: process.env
|
|
3296
|
+
}).status
|
|
3297
|
+
}
|
|
3298
|
+
];
|
|
3299
|
+
|
|
3300
|
+
if (process.env.GENESIS_VERIFY_GATE_SELF_TEST === "1") {
|
|
3301
|
+
console.log(gates.map((gate) => gate.name).join("\n"));
|
|
3302
|
+
return;
|
|
3303
|
+
}
|
|
3304
|
+
|
|
3305
|
+
let allPassed = true;
|
|
3306
|
+
for (const gate of gates) {
|
|
3307
|
+
process.stdout.write(`\n\x1b[33m▶ ${gate.name}\x1b[0m\n`);
|
|
3308
|
+
const code = gate.run();
|
|
3309
|
+
if (code !== 0) {
|
|
3310
|
+
console.log(`\x1b[31m✗ FAILED (exit ${code})\x1b[0m`);
|
|
3311
|
+
allPassed = false;
|
|
3312
|
+
break; // Stop on first failure
|
|
3313
|
+
}
|
|
3314
|
+
console.log(`\x1b[32m✓ PASSED\x1b[0m`);
|
|
3315
|
+
}
|
|
3316
|
+
|
|
3317
|
+
console.log("\n\x1b[1m\x1b[36m══════════════════════════════════════════════════════\x1b[0m");
|
|
3318
|
+
if (allPassed) {
|
|
3319
|
+
console.log("\x1b[1m\x1b[32m✓ ALL GATES PASSED — Task may now be declared DONE.\x1b[0m");
|
|
3320
|
+
console.log("\x1b[32mUpdate .codebase/CURRENT_STATE.md and RECOVERY_POINTS.md.\x1b[0m");
|
|
3321
|
+
} else {
|
|
3322
|
+
console.log("\x1b[1m\x1b[31m✗ VERIFICATION FAILED — Do NOT declare this task done.\x1b[0m");
|
|
3323
|
+
console.log("\x1b[31mFix the failing gate, then re-run: genesis-harness verify-gate\x1b[0m");
|
|
3324
|
+
process.exit(1);
|
|
3325
|
+
}
|
|
3326
|
+
console.log("\x1b[1m\x1b[36m══════════════════════════════════════════════════════\x1b[0m\n");
|
|
3327
|
+
}
|
|
3328
|
+
|
|
3329
|
+
function runColdStart() {
|
|
3330
|
+
const coldStartScript = path.join(packageRoot, "scripts", "cold-start-check.js");
|
|
3331
|
+
if (fs.existsSync(coldStartScript)) {
|
|
3332
|
+
const result = spawnSync(process.execPath, [coldStartScript], { stdio: "inherit", env: process.env });
|
|
3333
|
+
process.exit(result.status);
|
|
3334
|
+
} else {
|
|
3335
|
+
console.error("Cold start script not found.");
|
|
3336
|
+
process.exit(1);
|
|
3337
|
+
}
|
|
3338
|
+
}
|
|
3339
|
+
|
|
3340
|
+
function healTest(testCommand) {
|
|
3341
|
+
if (!testCommand) {
|
|
3342
|
+
console.error('[genesis-harness] Missing test command. Usage: genesis-harness heal "npm test"');
|
|
3343
|
+
process.exit(1);
|
|
3344
|
+
}
|
|
3345
|
+
|
|
3346
|
+
const { execSync } = require('child_process');
|
|
3347
|
+
try {
|
|
3348
|
+
console.log(`[genesis-harness] Running test: ${testCommand}`);
|
|
3349
|
+
execSync(testCommand, { stdio: 'inherit' });
|
|
3350
|
+
console.log('[genesis-harness] Tests passed! No healing required.');
|
|
3351
|
+
} catch (error) {
|
|
3352
|
+
const errorLog = error.stdout ? error.stdout.toString() : error.message;
|
|
3353
|
+
const planningDir = path.join(process.cwd(), '.planning');
|
|
3354
|
+
if (!fs.existsSync(planningDir)) {
|
|
3355
|
+
fs.mkdirSync(planningDir, { recursive: true });
|
|
3356
|
+
}
|
|
3357
|
+
const logFile = path.join(planningDir, 'LAST_ERROR.log');
|
|
3358
|
+
fs.writeFileSync(logFile, errorLog);
|
|
3359
|
+
|
|
3360
|
+
console.log('\\n\\x1b[31m\\x1b[1m[AGENT_DIRECTIVE] TESTS FAILED. READ .planning/LAST_ERROR.log AND IMMEDIATELY EXECUTE genesis-debug-guide TO FIX THE CODE. DO NOT STOP UNTIL TESTS PASS.\\x1b[0m\\n');
|
|
3361
|
+
process.exit(1);
|
|
3362
|
+
}
|
|
3363
|
+
}
|
|
3364
|
+
|
|
3365
|
+
function mcpSetupInteractive() {
|
|
3366
|
+
const options = [
|
|
3367
|
+
{ name: "@modelcontextprotocol/server-puppeteer", desc: "Browser UI Testing", selected: true },
|
|
3368
|
+
{ name: "@modelcontextprotocol/server-fetch", desc: "URL Markdown Reader", selected: true },
|
|
3369
|
+
{ name: "@modelcontextprotocol/server-github", desc: "Repo & PR management", selected: false },
|
|
3370
|
+
{ name: "@modelcontextprotocol/server-memory", desc: "Knowledge Graph Memory", selected: true },
|
|
3371
|
+
{ name: "@modelcontextprotocol/server-sqlite", desc: "Vector Memory DB", selected: false }
|
|
3372
|
+
];
|
|
3373
|
+
|
|
3374
|
+
let selectedIndex = 0;
|
|
3375
|
+
|
|
3376
|
+
const renderMenu = () => {
|
|
3377
|
+
console.clear();
|
|
3378
|
+
console.log("\x1b[1m\x1b[36m======================================================================\x1b[0m");
|
|
3379
|
+
console.log("\x1b[1m\x1b[36m GENESIS HARNESS - MCP INSTALLER \x1b[0m");
|
|
3380
|
+
console.log("\x1b[1m\x1b[36m======================================================================\x1b[0m\n");
|
|
3381
|
+
console.log(" \x1b[1mSelect which MCP Servers you want to install globally.\x1b[0m");
|
|
3382
|
+
console.log(" Use \x1b[33mUp/Down Arrow\x1b[0m to navigate.");
|
|
3383
|
+
console.log(" Use \x1b[33mSpace\x1b[0m to toggle selection.");
|
|
3384
|
+
console.log(" Press \x1b[32mEnter\x1b[0m to confirm and install.");
|
|
3385
|
+
console.log(" Press \x1b[90mEsc or Ctrl+C\x1b[0m to cancel.\n");
|
|
3386
|
+
|
|
3387
|
+
options.forEach((opt, idx) => {
|
|
3388
|
+
const checkbox = opt.selected ? "\x1b[32m[x]\x1b[0m" : "[ ]";
|
|
3389
|
+
const cursor = idx === selectedIndex ? "\x1b[1m\x1b[36m➔\x1b[0m " : " ";
|
|
3390
|
+
const name = idx === selectedIndex ? `\x1b[1m${opt.name}\x1b[0m` : opt.name;
|
|
3391
|
+
console.log(` ${cursor} ${checkbox} ${name.padEnd(50)} \x1b[90m(${opt.desc})\x1b[0m`);
|
|
3392
|
+
});
|
|
3393
|
+
console.log("\n\x1b[1m\x1b[36m======================================================================\x1b[0m");
|
|
3394
|
+
};
|
|
3395
|
+
|
|
3396
|
+
process.stdin.setRawMode(true);
|
|
3397
|
+
process.stdin.resume();
|
|
3398
|
+
process.stdin.setEncoding("utf8");
|
|
3399
|
+
|
|
3400
|
+
const cleanExit = () => {
|
|
3401
|
+
process.stdin.setRawMode(false);
|
|
3402
|
+
process.stdin.pause();
|
|
3403
|
+
console.clear();
|
|
3404
|
+
console.log("\n\x1b[33m[-] MCP Setup Cancelled.\x1b[0m\n");
|
|
3405
|
+
process.exit(0);
|
|
3406
|
+
};
|
|
3407
|
+
|
|
3408
|
+
const executeInstall = () => {
|
|
3409
|
+
process.stdin.setRawMode(false);
|
|
3410
|
+
process.stdin.pause();
|
|
3411
|
+
console.clear();
|
|
3412
|
+
const toInstall = options.filter(o => o.selected).map(o => o.name);
|
|
3413
|
+
|
|
3414
|
+
if (toInstall.length === 0) {
|
|
3415
|
+
console.log("\n\x1b[33m[-] No MCP servers selected. Exiting.\x1b[0m\n");
|
|
3416
|
+
process.exit(0);
|
|
3417
|
+
}
|
|
3418
|
+
|
|
3419
|
+
console.log(`\n\x1b[1m\x1b[32m[+] Installing selected MCP servers globally...\x1b[0m\n`);
|
|
3420
|
+
const args = ["install", "-g", ...toInstall];
|
|
3421
|
+
const npmCmd = process.platform === "win32" ? "npm.cmd" : "npm";
|
|
3422
|
+
|
|
3423
|
+
const result = spawnSync(npmCmd, args, { stdio: "inherit", env: process.env });
|
|
3424
|
+
if (result.status === 0) {
|
|
3425
|
+
console.log(`\n\x1b[1m\x1b[32m✓ Successfully installed MCP servers.\x1b[0m`);
|
|
3426
|
+
console.log(`You can now configure your Agent Client to use them. See mcp.example.json.\n`);
|
|
3427
|
+
} else {
|
|
3428
|
+
console.error(`\n\x1b[1m\x1b[31m[-] Installation failed with status ${result.status}\x1b[0m\n`);
|
|
3429
|
+
}
|
|
3430
|
+
process.exit(result.status || 0);
|
|
3431
|
+
};
|
|
3432
|
+
|
|
3433
|
+
renderMenu();
|
|
3434
|
+
|
|
3435
|
+
process.stdin.on("data", (key) => {
|
|
3436
|
+
if (key === "\u0003" || key === "\u001b") { // Ctrl+C or Esc
|
|
3437
|
+
cleanExit();
|
|
3438
|
+
} else if (key === "\u001b[A") { // Up arrow
|
|
3439
|
+
selectedIndex = (selectedIndex - 1 + options.length) % options.length;
|
|
3440
|
+
renderMenu();
|
|
3441
|
+
} else if (key === "\u001b[B") { // Down arrow
|
|
3442
|
+
selectedIndex = (selectedIndex + 1) % options.length;
|
|
3443
|
+
renderMenu();
|
|
3444
|
+
} else if (key === " ") { // Space
|
|
3445
|
+
options[selectedIndex].selected = !options[selectedIndex].selected;
|
|
3446
|
+
renderMenu();
|
|
3447
|
+
} else if (key === "\r") { // Enter
|
|
3448
|
+
executeInstall();
|
|
3449
|
+
}
|
|
3450
|
+
});
|
|
3451
|
+
}
|
|
3452
|
+
|
|
3453
|
+
function initInteractive() {
|
|
3454
|
+
const options = [
|
|
3455
|
+
{ name: "Antigravity IDE (Gemini)", desc: "Uses global plugin", selected: true },
|
|
3456
|
+
{ name: "Codex / Claude (VS Code)", desc: "Uses local .codex/skills", selected: false }
|
|
3457
|
+
];
|
|
3458
|
+
|
|
3459
|
+
let selectedIndex = 0;
|
|
3460
|
+
|
|
3461
|
+
const renderMenu = () => {
|
|
3462
|
+
console.clear();
|
|
3463
|
+
console.log("\x1b[1m\x1b[36m======================================================================\x1b[0m");
|
|
3464
|
+
console.log("\x1b[1m\x1b[36m GENESIS HARNESS - INITIALIZATION \x1b[0m");
|
|
3465
|
+
console.log("\x1b[1m\x1b[36m======================================================================\x1b[0m\n");
|
|
3466
|
+
console.log(" \x1b[1mWhich AI Agent Platform are you using?\x1b[0m");
|
|
3467
|
+
console.log(" Use \x1b[33mUp/Down Arrow\x1b[0m to navigate.");
|
|
3468
|
+
console.log(" Press \x1b[32mEnter\x1b[0m to confirm and initialize.");
|
|
3469
|
+
console.log(" Press \x1b[90mEsc or Ctrl+C\x1b[0m to cancel.\n");
|
|
3470
|
+
|
|
3471
|
+
options.forEach((opt, idx) => {
|
|
3472
|
+
const cursor = idx === selectedIndex ? "\x1b[1m\x1b[36m➔\x1b[0m " : " ";
|
|
3473
|
+
const checkbox = idx === selectedIndex ? "\x1b[32m(◉)\x1b[0m" : "( )";
|
|
3474
|
+
const name = idx === selectedIndex ? `\x1b[1m${opt.name}\x1b[0m` : opt.name;
|
|
3475
|
+
console.log(` ${cursor} ${checkbox} ${name.padEnd(30)} \x1b[90m(${opt.desc})\x1b[0m`);
|
|
3476
|
+
});
|
|
3477
|
+
console.log("\n\x1b[1m\x1b[36m======================================================================\x1b[0m");
|
|
3478
|
+
};
|
|
3479
|
+
|
|
3480
|
+
process.stdin.setRawMode(true);
|
|
3481
|
+
process.stdin.resume();
|
|
3482
|
+
process.stdin.setEncoding("utf8");
|
|
3483
|
+
|
|
3484
|
+
const cleanExit = () => {
|
|
3485
|
+
process.stdin.setRawMode(false);
|
|
3486
|
+
process.stdin.pause();
|
|
3487
|
+
console.clear();
|
|
3488
|
+
console.log("\n\x1b[33m[-] Initialization Cancelled.\x1b[0m\n");
|
|
3489
|
+
process.exit(0);
|
|
3490
|
+
};
|
|
3491
|
+
|
|
3492
|
+
const executeInit = () => {
|
|
3493
|
+
process.stdin.setRawMode(false);
|
|
3494
|
+
process.stdin.pause();
|
|
3495
|
+
console.clear();
|
|
3496
|
+
initializeProject({
|
|
3497
|
+
rootPath: process.cwd(),
|
|
3498
|
+
platform: selectedIndex === 0 ? "antigravity" : "codex"
|
|
3499
|
+
});
|
|
3500
|
+
process.exit(0);
|
|
3501
|
+
};
|
|
3502
|
+
|
|
3503
|
+
renderMenu();
|
|
3504
|
+
|
|
3505
|
+
process.stdin.on("data", (key) => {
|
|
3506
|
+
if (key === "\u0003" || key === "\u001b") { // Ctrl+C or Esc
|
|
3507
|
+
cleanExit();
|
|
3508
|
+
} else if (key === "\u001b[A") { // Up arrow
|
|
3509
|
+
selectedIndex = (selectedIndex - 1 + options.length) % options.length;
|
|
3510
|
+
renderMenu();
|
|
3511
|
+
} else if (key === "\u001b[B") { // Down arrow
|
|
3512
|
+
selectedIndex = (selectedIndex + 1) % options.length;
|
|
3513
|
+
renderMenu();
|
|
3514
|
+
} else if (key === "\r") { // Enter
|
|
3515
|
+
executeInit();
|
|
3516
|
+
}
|
|
3517
|
+
});
|
|
3518
|
+
}
|
|
3519
|
+
|
|
3520
|
+
const command = process.argv[2] || "help";
|
|
3521
|
+
const args = process.argv.slice(3);
|
|
3522
|
+
|
|
3523
|
+
switch (command) {
|
|
3524
|
+
case "install":
|
|
3525
|
+
copySkills({ target: parseTarget(args, "both") });
|
|
3526
|
+
seedLeanCtxPolicy(process.cwd());
|
|
3527
|
+
setupHooks();
|
|
3528
|
+
break;
|
|
3529
|
+
case "postinstall":
|
|
3530
|
+
if (process.env.GENESIS_HARNESS_SKIP_POSTINSTALL === "1") {
|
|
3531
|
+
process.exit(0);
|
|
3532
|
+
}
|
|
3533
|
+
copySkills({ quiet: true, target: "both" });
|
|
3534
|
+
const postinstallRoot = resolvePostinstallProjectRoot();
|
|
3535
|
+
seedLeanCtxPolicy(postinstallRoot, { quiet: true });
|
|
3536
|
+
setupHooks(postinstallRoot);
|
|
3537
|
+
break;
|
|
3538
|
+
case "verify":
|
|
3539
|
+
verifySkill(parseTarget(args, "both"));
|
|
3540
|
+
break;
|
|
3541
|
+
case "uninstall":
|
|
3542
|
+
uninstallSkills(parseTarget(args, "both"));
|
|
3543
|
+
break;
|
|
3544
|
+
case "path":
|
|
787
3545
|
for (const root of [agentsSkillsRoot, legacySkillsRoot]) {
|
|
788
3546
|
for (const skillName of skillNames) {
|
|
789
3547
|
console.log(path.join(root, skillName));
|
|
@@ -796,6 +3554,9 @@ switch (command) {
|
|
|
796
3554
|
case "docs":
|
|
797
3555
|
showDocsStatus();
|
|
798
3556
|
break;
|
|
3557
|
+
case "docs-gate":
|
|
3558
|
+
runDocsGate();
|
|
3559
|
+
break;
|
|
799
3560
|
case "remember":
|
|
800
3561
|
rememberFact(args[0], args[1]);
|
|
801
3562
|
break;
|
|
@@ -808,9 +3569,78 @@ switch (command) {
|
|
|
808
3569
|
case "prime":
|
|
809
3570
|
primeContext();
|
|
810
3571
|
break;
|
|
3572
|
+
case "leanctx":
|
|
3573
|
+
showLeanCtx();
|
|
3574
|
+
break;
|
|
811
3575
|
case "view-mockup":
|
|
812
3576
|
viewMockupsInteractive(args[0]);
|
|
813
3577
|
break;
|
|
3578
|
+
case "mcp":
|
|
3579
|
+
mcpSetupInteractive();
|
|
3580
|
+
break;
|
|
3581
|
+
case "init": {
|
|
3582
|
+
const initOptions = parseInitArgs(args);
|
|
3583
|
+
if (initOptions.autoConfirm) {
|
|
3584
|
+
initializeProject({
|
|
3585
|
+
rootPath: process.cwd(),
|
|
3586
|
+
platform: initOptions.platform || "codex",
|
|
3587
|
+
idea: initOptions.idea
|
|
3588
|
+
});
|
|
3589
|
+
break;
|
|
3590
|
+
}
|
|
3591
|
+
if (!process.stdin.isTTY || typeof process.stdin.setRawMode !== "function") {
|
|
3592
|
+
fail("init requires a TTY unless you pass --yes --platform <codex|antigravity>.");
|
|
3593
|
+
}
|
|
3594
|
+
initInteractive();
|
|
3595
|
+
break;
|
|
3596
|
+
}
|
|
3597
|
+
case "run": {
|
|
3598
|
+
const runOptions = parseRunArgs(args);
|
|
3599
|
+
if (!runOptions.autoConfirm) {
|
|
3600
|
+
fail("run requires --yes so the bootstrap pipeline stays deterministic.");
|
|
3601
|
+
}
|
|
3602
|
+
runBootstrapPipeline({
|
|
3603
|
+
rootPath: process.cwd(),
|
|
3604
|
+
options: runOptions
|
|
3605
|
+
});
|
|
3606
|
+
break;
|
|
3607
|
+
}
|
|
3608
|
+
case "resume":
|
|
3609
|
+
resumeProject(process.cwd());
|
|
3610
|
+
break;
|
|
3611
|
+
case "next":
|
|
3612
|
+
showNextAction(process.cwd());
|
|
3613
|
+
break;
|
|
3614
|
+
case "add-feature":
|
|
3615
|
+
addFeature(process.cwd(), parseAddFeatureArgs(args));
|
|
3616
|
+
break;
|
|
3617
|
+
case "complete-feature":
|
|
3618
|
+
completeFeature(process.cwd(), parseCompleteFeatureArgs(args));
|
|
3619
|
+
break;
|
|
3620
|
+
case "verify-project":
|
|
3621
|
+
verifyProject(process.cwd(), parseProjectVerificationArgs(args));
|
|
3622
|
+
break;
|
|
3623
|
+
case "complete-project":
|
|
3624
|
+
completeProject(process.cwd(), parseProjectCompletionArgs(args));
|
|
3625
|
+
break;
|
|
3626
|
+
case "pipeline-audit":
|
|
3627
|
+
auditPipeline(process.cwd());
|
|
3628
|
+
break;
|
|
3629
|
+
case "sync":
|
|
3630
|
+
syncContext();
|
|
3631
|
+
break;
|
|
3632
|
+
case "setup-hooks":
|
|
3633
|
+
setupHooks();
|
|
3634
|
+
break;
|
|
3635
|
+
case "verify-gate":
|
|
3636
|
+
runVerifyGate();
|
|
3637
|
+
break;
|
|
3638
|
+
case "cold-start":
|
|
3639
|
+
runColdStart();
|
|
3640
|
+
break;
|
|
3641
|
+
case "heal":
|
|
3642
|
+
healTest(args.join(" "));
|
|
3643
|
+
break;
|
|
814
3644
|
case "help":
|
|
815
3645
|
case "--help":
|
|
816
3646
|
case "-h":
|