codex-genesis-harness 0.1.8 → 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/CURRENT_STATE.md +7 -33
- package/.codebase/KNOWN_PROBLEMS.md +20 -1
- package/.codebase/MODULE_INDEX.md +15 -2
- package/.codebase/PIPELINE_FLOW.md +10 -2
- package/.codebase/RECOVERY_POINTS.md +63 -0
- package/.codebase/TEST_MATRIX.md +5 -1
- package/.codebase/memories/lessons_learned.md +42 -0
- package/.codebase/state.json +130 -12
- package/.codex/skills/genesis-harness/SKILL.md +10 -1
- 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/init-planning.sh +245 -13
- package/.codex/skills/genesis-pipeline-orchestration/SKILL.md +15 -3
- package/.codex-plugin/plugin.json +4 -2
- package/CHANGELOG.md +21 -0
- package/README.EN.md +44 -2
- package/README.VI.md +44 -2
- package/README.md +80 -2
- package/VERSION +1 -2
- package/bin/genesis-harness.js +2121 -21
- package/contracts/features/project-registry-schema.json +37 -0
- package/contracts/observability/agent-run-schema.json +6 -1
- package/features/REGISTRY.md +9 -7
- 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/package.json +7 -2
- package/scripts/check-repository-hygiene.js +48 -0
- package/scripts/run-evals.sh +36 -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/transition_state.sh +32 -8
- package/scripts/validation_gates.sh +2 -80
- package/scripts/verify.sh +3 -1
- package/tests/fixtures/fixture-index.md +5 -0
- package/tests/integration/cli-smoke.test.js +403 -0
- package/tests/unit/repository_hygiene.test.js +17 -0
- package/tests/unit/state_metadata.test.js +76 -0
- 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/bin/genesis-harness.js
CHANGED
|
@@ -61,6 +61,15 @@ Usage:
|
|
|
61
61
|
genesis-harness leanctx Show token budget policy and portable command guidance
|
|
62
62
|
genesis-harness view-mockup [slug] Interactive console UI to search & view mockups
|
|
63
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
|
|
64
73
|
genesis-harness sync Compress and sync codebase context (AST/Regex)
|
|
65
74
|
genesis-harness setup-hooks Install auto-sync git pre-commit hook
|
|
66
75
|
genesis-harness heal <command> Run test & print agent directive on failure
|
|
@@ -121,7 +130,11 @@ function copySkills({ quiet = false, target = "both" } = {}) {
|
|
|
121
130
|
if (fs.existsSync(dir)) {
|
|
122
131
|
const backupParent = path.join(root, "..", "backups");
|
|
123
132
|
fs.mkdirSync(backupParent, { recursive: true });
|
|
124
|
-
|
|
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
|
+
}
|
|
125
138
|
fs.renameSync(dir, backupDir);
|
|
126
139
|
if (!quiet) console.log(`Existing skill backed up to: ${backupDir}`);
|
|
127
140
|
}
|
|
@@ -136,6 +149,34 @@ function copySkills({ quiet = false, target = "both" } = {}) {
|
|
|
136
149
|
if (!quiet) console.log("Restart Codex, then invoke: Use $genesis-harness");
|
|
137
150
|
}
|
|
138
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
|
+
|
|
139
180
|
function shouldSeedProjectRoot(rootPath) {
|
|
140
181
|
if (!rootPath) return false;
|
|
141
182
|
const resolvedRoot = path.resolve(rootPath);
|
|
@@ -406,7 +447,7 @@ function showStatus() {
|
|
|
406
447
|
}
|
|
407
448
|
}
|
|
408
449
|
} else {
|
|
409
|
-
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>\"`.");
|
|
410
451
|
}
|
|
411
452
|
|
|
412
453
|
// 3. Skills Inventory
|
|
@@ -793,6 +834,1935 @@ function openFileNatively(filePath) {
|
|
|
793
834
|
return cp.status === 0;
|
|
794
835
|
}
|
|
795
836
|
|
|
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
|
+
}
|
|
854
|
+
|
|
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"
|
|
871
|
+
}
|
|
872
|
+
});
|
|
873
|
+
|
|
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 || "";
|
|
880
|
+
}
|
|
881
|
+
|
|
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
|
+
}
|
|
887
|
+
|
|
888
|
+
const platformLabel = normalized === "codex" ? "Codex / Claude (VS Code)" : "Antigravity IDE (Gemini)";
|
|
889
|
+
const isAntigravity = normalized === "antigravity";
|
|
890
|
+
|
|
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).");
|
|
900
|
+
}
|
|
901
|
+
|
|
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);
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
return options;
|
|
940
|
+
}
|
|
941
|
+
|
|
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
|
+
};
|
|
958
|
+
|
|
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
|
+
]);
|
|
973
|
+
|
|
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;
|
|
979
|
+
}
|
|
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: ""
|
|
1016
|
+
};
|
|
1017
|
+
const valueFlags = new Map([
|
|
1018
|
+
["--verify-cmd", "verifyCmd"],
|
|
1019
|
+
["--evidence", "evidence"]
|
|
1020
|
+
]);
|
|
1021
|
+
|
|
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
|
+
}
|
|
1028
|
+
|
|
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: ""
|
|
1039
|
+
};
|
|
1040
|
+
const valueFlags = new Map([
|
|
1041
|
+
["--title", "title"],
|
|
1042
|
+
["--slug", "slug"],
|
|
1043
|
+
["--verify-cmd", "verifyCmd"]
|
|
1044
|
+
]);
|
|
1045
|
+
|
|
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
|
+
}
|
|
1052
|
+
|
|
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
|
+
}
|
|
1058
|
+
|
|
1059
|
+
function parseProjectVerificationArgs(args) {
|
|
1060
|
+
return parseCompleteFeatureArgs(args);
|
|
1061
|
+
}
|
|
1062
|
+
|
|
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
|
+
}
|
|
1073
|
+
|
|
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
|
+
|
|
796
2766
|
function discoverMockups(rootPath = packageRoot) {
|
|
797
2767
|
const mockups = [];
|
|
798
2768
|
const featuresDir = path.join(rootPath, ".planning", "features");
|
|
@@ -1278,6 +3248,8 @@ function runVerifyGate() {
|
|
|
1278
3248
|
const verifyScript = path.join(packageRoot, "scripts", "verify.sh");
|
|
1279
3249
|
const evalsScript = path.join(packageRoot, "scripts", "run-evals.sh");
|
|
1280
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";
|
|
1281
3253
|
|
|
1282
3254
|
console.log("\x1b[1m\x1b[36m══════════════════════════════════════════════════════\x1b[0m");
|
|
1283
3255
|
console.log("\x1b[1m\x1b[36m GENESIS HARNESS — VERIFY-GATE (L09 Victory Blocker) \x1b[0m");
|
|
@@ -1287,36 +3259,49 @@ function runVerifyGate() {
|
|
|
1287
3259
|
const gates = [
|
|
1288
3260
|
{
|
|
1289
3261
|
name: "1. Structural verify (verify.sh)",
|
|
1290
|
-
run: () => spawnSync(bash, [verifyScript], { stdio: "inherit", env: process.env }).status
|
|
3262
|
+
run: () => spawnSync(bash, [verifyScript], { cwd: packageRoot, stdio: "inherit", env: process.env }).status
|
|
1291
3263
|
},
|
|
1292
3264
|
{
|
|
1293
|
-
name: "2.
|
|
1294
|
-
run: () => spawnSync(process.
|
|
1295
|
-
path.join(packageRoot, "tests", "unit", "feature_registry.test.js")
|
|
1296
|
-
], { stdio: "inherit", env: process.env }).status
|
|
3265
|
+
name: "2. Eval regression suite (run-evals.sh)",
|
|
3266
|
+
run: () => spawnSync(bash, [evalsScript], { cwd: packageRoot, stdio: "inherit", env: process.env }).status
|
|
1297
3267
|
},
|
|
1298
3268
|
{
|
|
1299
|
-
name: "3.
|
|
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)",
|
|
1300
3278
|
run: () => fs.existsSync(coldStartScript)
|
|
1301
|
-
? spawnSync(process.execPath, [coldStartScript], { stdio: "inherit", env: process.env }).status
|
|
3279
|
+
? spawnSync(process.execPath, [coldStartScript], { cwd: packageRoot, stdio: "inherit", env: process.env }).status
|
|
1302
3280
|
: 0
|
|
1303
3281
|
},
|
|
1304
3282
|
{
|
|
1305
|
-
name: "
|
|
1306
|
-
run: () => {
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
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
|
|
1317
3297
|
}
|
|
1318
3298
|
];
|
|
1319
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
|
+
|
|
1320
3305
|
let allPassed = true;
|
|
1321
3306
|
for (const gate of gates) {
|
|
1322
3307
|
process.stdout.write(`\n\x1b[33m▶ ${gate.name}\x1b[0m\n`);
|
|
@@ -1465,6 +3450,73 @@ function mcpSetupInteractive() {
|
|
|
1465
3450
|
});
|
|
1466
3451
|
}
|
|
1467
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
|
+
|
|
1468
3520
|
const command = process.argv[2] || "help";
|
|
1469
3521
|
const args = process.argv.slice(3);
|
|
1470
3522
|
|
|
@@ -1526,6 +3578,54 @@ switch (command) {
|
|
|
1526
3578
|
case "mcp":
|
|
1527
3579
|
mcpSetupInteractive();
|
|
1528
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;
|
|
1529
3629
|
case "sync":
|
|
1530
3630
|
syncContext();
|
|
1531
3631
|
break;
|