@structured-vibe-coding/core 0.18.0 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/defaults/teams/research/roles/researcher.md +29 -0
- package/defaults/teams/research/team.yaml +7 -0
- package/defaults/teams/software/roles/architect.md +31 -0
- package/defaults/teams/software/roles/dev-lead.md +32 -0
- package/defaults/teams/software/roles/developer.md +30 -0
- package/defaults/teams/software/roles/product-manager.md +32 -0
- package/defaults/teams/software/roles/product-owner.md +30 -0
- package/defaults/teams/software/roles/qa.md +35 -0
- package/defaults/teams/software/routing.yaml +28 -0
- package/defaults/teams/software/team.yaml +24 -0
- package/defaults/teams/solo/roles/developer.md +28 -0
- package/defaults/teams/solo/team.yaml +7 -0
- package/defaults/templates/plan.md +17 -0
- package/defaults/templates/review.md +11 -0
- package/defaults/templates/story.md +13 -0
- package/defaults/templates/task.md +11 -0
- package/defaults/workflows/team-software.yaml +153 -0
- package/dist/{agents-AKSUXQHN.js → agents-PSIOQEYH.js} +3 -3
- package/dist/{change-context-REE7UKU7.js → change-context-EZJMSGSP.js} +1 -1
- package/dist/{chunk-4PIRWYRA.js → chunk-26YG6A4M.js} +166 -81
- package/dist/{chunk-LHSR7JG2.js → chunk-2FJBFYVJ.js} +306 -31
- package/dist/{chunk-QGP43XCN.js → chunk-H55RFAEI.js} +1 -1
- package/dist/{chunk-QHV5KZKX.js → chunk-HG7TPW5V.js} +93 -8
- package/dist/{chunk-44LQ5BKZ.js → chunk-TGRV6WGS.js} +812 -4
- package/dist/{gather-ZNV2NAUN.js → gather-7TSVKQO2.js} +1 -1
- package/dist/{greenfield-I5S5OS2D.js → greenfield-QFA5WVHE.js} +3 -3
- package/dist/index.d.ts +804 -10
- package/dist/index.js +2765 -524
- package/package.json +2 -2
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
import {
|
|
2
|
+
WORK_TYPES,
|
|
3
|
+
getHeadSha,
|
|
4
|
+
gitUserName,
|
|
5
|
+
loadConfig,
|
|
6
|
+
readTeamConfig
|
|
7
|
+
} from "./chunk-TGRV6WGS.js";
|
|
1
8
|
import {
|
|
2
9
|
ChangeMetaSchema,
|
|
3
10
|
activeStages,
|
|
@@ -5,6 +12,7 @@ import {
|
|
|
5
12
|
changeDirName,
|
|
6
13
|
clearActiveIf,
|
|
7
14
|
firstActiveStageId,
|
|
15
|
+
getActiveChangeId,
|
|
8
16
|
getStage,
|
|
9
17
|
isStageSkipped,
|
|
10
18
|
loadWorkflow,
|
|
@@ -14,7 +22,7 @@ import {
|
|
|
14
22
|
setActiveChange,
|
|
15
23
|
shouldAutoSkip,
|
|
16
24
|
uniqueChangeDirName
|
|
17
|
-
} from "./chunk-
|
|
25
|
+
} from "./chunk-HG7TPW5V.js";
|
|
18
26
|
import {
|
|
19
27
|
archiveDir,
|
|
20
28
|
changeMetaPath,
|
|
@@ -24,6 +32,7 @@ import {
|
|
|
24
32
|
displayArchivePath,
|
|
25
33
|
ensureDir,
|
|
26
34
|
listDirs,
|
|
35
|
+
listFiles,
|
|
27
36
|
memoryDir,
|
|
28
37
|
pathExists,
|
|
29
38
|
readText,
|
|
@@ -35,7 +44,7 @@ import {
|
|
|
35
44
|
} from "./chunk-O5SP6X5R.js";
|
|
36
45
|
|
|
37
46
|
// src/greenfield.ts
|
|
38
|
-
import { join as
|
|
47
|
+
import { join as join6 } from "pathe";
|
|
39
48
|
|
|
40
49
|
// src/artifacts.ts
|
|
41
50
|
var PLACEHOLDER_WORD = /^(?:TBD|TODO|N\/A|NA|none|tbd|todo|n\/a|\.{3}|…|-|—|–|_|\*)$/i;
|
|
@@ -149,7 +158,7 @@ function incompleteArtifactReason(path, content) {
|
|
|
149
158
|
}
|
|
150
159
|
|
|
151
160
|
// src/change.ts
|
|
152
|
-
import { join as
|
|
161
|
+
import { join as join5 } from "pathe";
|
|
153
162
|
|
|
154
163
|
// src/metrics.ts
|
|
155
164
|
import { createReadStream } from "fs";
|
|
@@ -175,10 +184,22 @@ var TokenUsageMetricSchema = z.object({
|
|
|
175
184
|
outputTokens: z.number().int().nonnegative().nullable(),
|
|
176
185
|
measured: z.literal(true)
|
|
177
186
|
});
|
|
187
|
+
var DecisionMetricSchema = z.object({
|
|
188
|
+
workType: z.enum(WORK_TYPES),
|
|
189
|
+
kind: z.enum(["reason", "decide"]),
|
|
190
|
+
provider: z.enum(["command", "ollama"]),
|
|
191
|
+
model: z.string().min(1).nullable(),
|
|
192
|
+
startedAt: UtcTimestampSchema,
|
|
193
|
+
inputTokens: z.number().int().nonnegative().nullable(),
|
|
194
|
+
cacheReadTokens: z.number().int().nonnegative().nullable(),
|
|
195
|
+
cacheWriteTokens: z.number().int().nonnegative().nullable(),
|
|
196
|
+
outputTokens: z.number().int().nonnegative().nullable()
|
|
197
|
+
});
|
|
178
198
|
var ChangeMetricsSchema = z.object({
|
|
179
199
|
version: z.literal(1),
|
|
180
200
|
phases: z.array(PhaseMetricSchema),
|
|
181
|
-
requests: z.array(TokenUsageMetricSchema)
|
|
201
|
+
requests: z.array(TokenUsageMetricSchema),
|
|
202
|
+
decisions: z.array(DecisionMetricSchema).optional()
|
|
182
203
|
});
|
|
183
204
|
var MODEL_ID = /^[A-Za-z0-9._:/-]+$/;
|
|
184
205
|
function metricsPath(projectRoot, config, changeId) {
|
|
@@ -198,6 +219,12 @@ async function writeChangeMetrics(projectRoot, config, changeId, metrics) {
|
|
|
198
219
|
await writeText(path, `${JSON.stringify(validated, null, 2)}
|
|
199
220
|
`);
|
|
200
221
|
}
|
|
222
|
+
async function recordDecisionMetric(projectRoot, config, changeId, draft) {
|
|
223
|
+
if (!config.metrics.enabled) return;
|
|
224
|
+
const metrics = await readChangeMetrics(projectRoot, config, changeId);
|
|
225
|
+
const decisions = [...metrics.decisions ?? [], DecisionMetricSchema.parse(draft)];
|
|
226
|
+
await writeChangeMetrics(projectRoot, config, changeId, { ...metrics, decisions });
|
|
227
|
+
}
|
|
201
228
|
async function startPhase(projectRoot, config, changeId, phase, startedAt = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
202
229
|
if (!config.metrics.enabled) return;
|
|
203
230
|
const metrics = await readChangeMetrics(projectRoot, config, changeId);
|
|
@@ -532,19 +559,173 @@ async function importSessionMetrics(projectRoot, config, changeId, sessionLogPat
|
|
|
532
559
|
}
|
|
533
560
|
|
|
534
561
|
// src/stage-gates.ts
|
|
562
|
+
import { join as join3 } from "pathe";
|
|
563
|
+
|
|
564
|
+
// src/role/signoff.ts
|
|
565
|
+
import { appendFile } from "fs/promises";
|
|
535
566
|
import { join as join2 } from "pathe";
|
|
567
|
+
function ok(stdout) {
|
|
568
|
+
return { exitCode: 0, stdout };
|
|
569
|
+
}
|
|
570
|
+
function fail(stdout) {
|
|
571
|
+
return { exitCode: 1, stdout };
|
|
572
|
+
}
|
|
573
|
+
function oneLine(note, empty, broken) {
|
|
574
|
+
const raw = note ?? "";
|
|
575
|
+
if (raw.includes("\n") || raw.includes("\r")) return fail(broken);
|
|
576
|
+
const words = raw.trim();
|
|
577
|
+
if (!words) return fail(empty);
|
|
578
|
+
return words;
|
|
579
|
+
}
|
|
580
|
+
function safeStageId(stage) {
|
|
581
|
+
return /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(stage);
|
|
582
|
+
}
|
|
583
|
+
function signoffFileName(stage, sha) {
|
|
584
|
+
return `reviews/${stage}-${sha}.md`;
|
|
585
|
+
}
|
|
586
|
+
function renderSignoff(record) {
|
|
587
|
+
return [
|
|
588
|
+
"---",
|
|
589
|
+
`kind: ${record.kind}`,
|
|
590
|
+
`sha: ${record.sha}`,
|
|
591
|
+
`reviewer: ${record.reviewer}`,
|
|
592
|
+
`verdict: ${record.verdict}`,
|
|
593
|
+
"---",
|
|
594
|
+
record.evidence,
|
|
595
|
+
""
|
|
596
|
+
].join("\n");
|
|
597
|
+
}
|
|
598
|
+
function parseSignoff(raw) {
|
|
599
|
+
const lines = raw.split("\n");
|
|
600
|
+
if (lines[0] !== "---") return null;
|
|
601
|
+
const end = lines.indexOf("---", 1);
|
|
602
|
+
if (end <= 1) return null;
|
|
603
|
+
const fields = /* @__PURE__ */ new Map();
|
|
604
|
+
for (const line of lines.slice(1, end)) {
|
|
605
|
+
const split = line.indexOf(":");
|
|
606
|
+
if (split <= 0) return null;
|
|
607
|
+
fields.set(line.slice(0, split).trim(), line.slice(split + 1).trim());
|
|
608
|
+
}
|
|
609
|
+
const kind = fields.get("kind") ?? "";
|
|
610
|
+
const sha = fields.get("sha") ?? "";
|
|
611
|
+
const reviewer = fields.get("reviewer") ?? "";
|
|
612
|
+
const verdict = fields.get("verdict") ?? "";
|
|
613
|
+
const evidence = lines.slice(end + 1).join("\n").trim();
|
|
614
|
+
if (!kind || !sha || !reviewer || !verdict || !evidence) return null;
|
|
615
|
+
return { kind, sha, reviewer, verdict, evidence };
|
|
616
|
+
}
|
|
617
|
+
async function headSha(root) {
|
|
618
|
+
try {
|
|
619
|
+
const sha = await getHeadSha(root);
|
|
620
|
+
if (!sha) return fail("Cannot sign off: Git HEAD is unavailable in this project.");
|
|
621
|
+
return sha;
|
|
622
|
+
} catch (error) {
|
|
623
|
+
const message = error instanceof Error ? error.message : "Git HEAD is unavailable.";
|
|
624
|
+
return fail(message);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
async function openChange(root, change) {
|
|
628
|
+
const config = await loadConfig(root);
|
|
629
|
+
const requested = change?.trim() ?? "";
|
|
630
|
+
const changeId = requested || await getActiveChangeId(root, config);
|
|
631
|
+
if (!changeId) return fail("No active change.");
|
|
632
|
+
try {
|
|
633
|
+
const ctx = await buildContext(root, config, changeId);
|
|
634
|
+
return { path: ctx.path, stage: ctx.meta.stage };
|
|
635
|
+
} catch (error) {
|
|
636
|
+
const message = error instanceof Error ? error.message : "Change not found.";
|
|
637
|
+
return fail(message);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
function isFailure(value) {
|
|
641
|
+
return "exitCode" in value;
|
|
642
|
+
}
|
|
643
|
+
async function recordSignoff(root, input) {
|
|
644
|
+
const words = oneLine(input.note, "A sign-off needs a note.", "A sign-off note is one line.");
|
|
645
|
+
if (typeof words !== "string") return words;
|
|
646
|
+
const opened = await openChange(root, input.change);
|
|
647
|
+
if (isFailure(opened)) return opened;
|
|
648
|
+
const stage = (input.stage?.trim() || opened.stage).trim();
|
|
649
|
+
if (!safeStageId(stage)) return fail("A stage id is not valid.");
|
|
650
|
+
const team = await readTeamConfig(root);
|
|
651
|
+
if ("error" in team) return fail(team.error);
|
|
652
|
+
const sha = await headSha(root);
|
|
653
|
+
if (typeof sha !== "string") return sha;
|
|
654
|
+
const gitUser = gitUserName(root);
|
|
655
|
+
if (!gitUser) return fail("Git user.name is not set.");
|
|
656
|
+
if (!team.team.humans.some((human) => human.name === gitUser)) {
|
|
657
|
+
return fail("Git user.name is not a human approver.");
|
|
658
|
+
}
|
|
659
|
+
const file = join2(opened.path, signoffFileName(stage, sha));
|
|
660
|
+
const existing = await pathExists(file) ? parseSignoff(await readText(file)) : null;
|
|
661
|
+
const same = existing !== null && existing.kind === stage && existing.sha === sha && existing.reviewer === gitUser && existing.verdict === "accept";
|
|
662
|
+
if (!same) {
|
|
663
|
+
await writeText(
|
|
664
|
+
file,
|
|
665
|
+
renderSignoff({ kind: stage, sha, reviewer: gitUser, verdict: "accept", evidence: words })
|
|
666
|
+
);
|
|
667
|
+
}
|
|
668
|
+
await appendFile(
|
|
669
|
+
join2(opened.path, "decisions.md"),
|
|
670
|
+
`${nowIso()} sign-off ${stage} ${sha} ${gitUser} accept ${words}
|
|
671
|
+
`
|
|
672
|
+
);
|
|
673
|
+
return ok(`Signed ${stage} at ${sha}.`);
|
|
674
|
+
}
|
|
675
|
+
async function recordDecisionRequest(root, input) {
|
|
676
|
+
const words = oneLine(input.note, "A decision needs a note.", "A decision note is one line.");
|
|
677
|
+
if (typeof words !== "string") return words;
|
|
678
|
+
const opened = await openChange(root, input.change);
|
|
679
|
+
if (isFailure(opened)) return opened;
|
|
680
|
+
await appendFile(join2(opened.path, "decisions.md"), `${nowIso()} decision-needed ${words}
|
|
681
|
+
`);
|
|
682
|
+
return ok("Decision needed.");
|
|
683
|
+
}
|
|
684
|
+
async function checkHumanSignoff(root, changePath2, stageId) {
|
|
685
|
+
if (!safeStageId(stageId)) return { ok: false, message: "A stage id is not valid." };
|
|
686
|
+
const team = await readTeamConfig(root);
|
|
687
|
+
if ("error" in team) return { ok: false, message: team.error };
|
|
688
|
+
let sha;
|
|
689
|
+
try {
|
|
690
|
+
sha = await getHeadSha(root);
|
|
691
|
+
} catch (error) {
|
|
692
|
+
const message = error instanceof Error ? error.message : "Git HEAD is unavailable.";
|
|
693
|
+
return { ok: false, message };
|
|
694
|
+
}
|
|
695
|
+
if (!sha) {
|
|
696
|
+
return {
|
|
697
|
+
ok: false,
|
|
698
|
+
message: `Cannot leave stage "${stageId}": Git HEAD is unavailable, so a human sign-off cannot be checked.`
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
const file = join2(changePath2, signoffFileName(stageId, sha));
|
|
702
|
+
if (!await pathExists(file)) {
|
|
703
|
+
return {
|
|
704
|
+
ok: false,
|
|
705
|
+
message: `Hard gate on stage "${stageId}" needs a human sign-off for this commit. Run \`sdd team signoff\`.`
|
|
706
|
+
};
|
|
707
|
+
}
|
|
708
|
+
const parsed = parseSignoff(await readText(file));
|
|
709
|
+
const listed = parsed !== null && parsed.kind === stageId && parsed.sha === sha && parsed.verdict === "accept" && team.team.humans.some((human) => human.name === parsed.reviewer);
|
|
710
|
+
if (!listed) {
|
|
711
|
+
return { ok: false, message: `Sign-off for stage "${stageId}" does not match this commit.` };
|
|
712
|
+
}
|
|
713
|
+
return { ok: true };
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
// src/stage-gates.ts
|
|
536
717
|
async function checkRequiredArtifacts(changePath2, stageId, artifacts) {
|
|
537
718
|
const errors = [];
|
|
538
719
|
for (const artifact of artifacts) {
|
|
539
720
|
if (!artifact.required) continue;
|
|
540
721
|
if (artifact.path.endsWith("/")) {
|
|
541
|
-
const target2 =
|
|
722
|
+
const target2 = join3(changePath2, artifact.path);
|
|
542
723
|
if (!await pathExists(target2)) {
|
|
543
724
|
errors.push(`Missing required directory: ${artifact.path} (stage ${stageId})`);
|
|
544
725
|
}
|
|
545
726
|
continue;
|
|
546
727
|
}
|
|
547
|
-
const target =
|
|
728
|
+
const target = join3(changePath2, artifact.path);
|
|
548
729
|
if (!await pathExists(target)) {
|
|
549
730
|
errors.push(incompleteArtifactReason(artifact.path, null));
|
|
550
731
|
continue;
|
|
@@ -579,7 +760,7 @@ async function incompletePriorStageErrors(ctx) {
|
|
|
579
760
|
}
|
|
580
761
|
return errors;
|
|
581
762
|
}
|
|
582
|
-
async function canLeaveStage(ctx, config) {
|
|
763
|
+
async function canLeaveStage(ctx, config, projectRoot) {
|
|
583
764
|
const stage = getStage(ctx.workflow, ctx.meta, ctx.meta.stage);
|
|
584
765
|
const errors = [];
|
|
585
766
|
const warnings = [];
|
|
@@ -594,7 +775,23 @@ async function canLeaveStage(ctx, config) {
|
|
|
594
775
|
effectiveGate = "hard";
|
|
595
776
|
}
|
|
596
777
|
const gateState = ctx.meta.gates?.[stage.id];
|
|
597
|
-
|
|
778
|
+
const humanGate = effectiveGate === "hard" && (stage.gate?.approvers?.includes("human") ?? false);
|
|
779
|
+
if (humanGate) {
|
|
780
|
+
if (gateState?.status === "failed") {
|
|
781
|
+
errors.push(
|
|
782
|
+
`Hard gate on stage "${stage.id}" is not approved. Run \`sdd gate approve\` or complete the checklist.`
|
|
783
|
+
);
|
|
784
|
+
} else if (gateState?.status !== "waived") {
|
|
785
|
+
if (!projectRoot) {
|
|
786
|
+
errors.push(
|
|
787
|
+
`Cannot leave stage "${stage.id}": the project root is required to check a human sign-off.`
|
|
788
|
+
);
|
|
789
|
+
} else {
|
|
790
|
+
const signed = await checkHumanSignoff(projectRoot, ctx.path, stage.id);
|
|
791
|
+
if (!signed.ok) errors.push(signed.message);
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
} else if (effectiveGate === "hard") {
|
|
598
795
|
if (!gateState || gateState.status !== "approved" && gateState.status !== "waived") {
|
|
599
796
|
errors.push(
|
|
600
797
|
`Hard gate on stage "${stage.id}" is not approved. Run \`sdd gate approve\` or complete the checklist.`
|
|
@@ -654,6 +851,76 @@ async function approveGate(projectRoot, config, changeId, stageId, note, status
|
|
|
654
851
|
return buildContext(projectRoot, config, changeId);
|
|
655
852
|
}
|
|
656
853
|
|
|
854
|
+
// src/stage-items.ts
|
|
855
|
+
import { join as join4 } from "pathe";
|
|
856
|
+
import { z as z2 } from "zod";
|
|
857
|
+
var itemSchema = z2.object({
|
|
858
|
+
id: z2.string().trim().min(1),
|
|
859
|
+
stage: z2.string().trim().min(1),
|
|
860
|
+
source: z2.string().trim().min(1),
|
|
861
|
+
status: z2.enum(["open", "done"])
|
|
862
|
+
}).strict();
|
|
863
|
+
var itemsSchema = z2.array(itemSchema);
|
|
864
|
+
function itemSourcePattern(pattern) {
|
|
865
|
+
if (pattern.includes("..") || pattern.startsWith("/") || pattern.includes("\\")) return null;
|
|
866
|
+
const slash = pattern.lastIndexOf("/");
|
|
867
|
+
if (slash <= 0) return null;
|
|
868
|
+
const dir = pattern.slice(0, slash);
|
|
869
|
+
const file = pattern.slice(slash + 1);
|
|
870
|
+
if (!/^[A-Za-z0-9._-]+$/.test(dir) || dir.includes("*")) return null;
|
|
871
|
+
const star = file.indexOf("*");
|
|
872
|
+
if (star < 0 || file.indexOf("*", star + 1) !== -1) return null;
|
|
873
|
+
return { dir, prefix: file.slice(0, star), suffix: file.slice(star + 1) };
|
|
874
|
+
}
|
|
875
|
+
function matches(name, prefix, suffix) {
|
|
876
|
+
if (!name.startsWith(prefix) || !name.endsWith(suffix)) return false;
|
|
877
|
+
return name.length > prefix.length + suffix.length;
|
|
878
|
+
}
|
|
879
|
+
function itemId(name) {
|
|
880
|
+
const dot = name.lastIndexOf(".");
|
|
881
|
+
if (dot <= 0) return name;
|
|
882
|
+
return name.slice(0, dot);
|
|
883
|
+
}
|
|
884
|
+
async function syncStageItems(changeDir, stage) {
|
|
885
|
+
if (!stage.items_from) return [];
|
|
886
|
+
const pattern = itemSourcePattern(stage.items_from);
|
|
887
|
+
if (!pattern) throw new Error("items_from is invalid");
|
|
888
|
+
const names = (await listFiles(join4(changeDir, pattern.dir))).filter((name) => matches(name, pattern.prefix, pattern.suffix)).sort();
|
|
889
|
+
const itemsPath = join4(changeDir, "items.yaml");
|
|
890
|
+
let existing = [];
|
|
891
|
+
if (await pathExists(itemsPath)) {
|
|
892
|
+
let raw;
|
|
893
|
+
try {
|
|
894
|
+
raw = await readYaml(itemsPath);
|
|
895
|
+
} catch {
|
|
896
|
+
throw new Error("items.yaml is invalid");
|
|
897
|
+
}
|
|
898
|
+
const parsed = itemsSchema.safeParse(raw);
|
|
899
|
+
if (!parsed.success) throw new Error("items.yaml is invalid");
|
|
900
|
+
existing = parsed.data;
|
|
901
|
+
}
|
|
902
|
+
const others = existing.filter((item) => item.stage !== stage.id);
|
|
903
|
+
const mine = new Map(
|
|
904
|
+
existing.filter((item) => item.stage === stage.id).map((item) => [item.id, item])
|
|
905
|
+
);
|
|
906
|
+
const seen = /* @__PURE__ */ new Set();
|
|
907
|
+
const next = [];
|
|
908
|
+
for (const name of names) {
|
|
909
|
+
const id = itemId(name);
|
|
910
|
+
if (seen.has(id)) continue;
|
|
911
|
+
seen.add(id);
|
|
912
|
+
const source = `${pattern.dir}/${name}`;
|
|
913
|
+
const prev = mine.get(id);
|
|
914
|
+
if (prev) next.push({ ...prev, source });
|
|
915
|
+
else next.push({ id, stage: stage.id, source, status: "open" });
|
|
916
|
+
}
|
|
917
|
+
const items = [...others, ...next].sort(
|
|
918
|
+
(a, b) => a.stage === b.stage ? a.id.localeCompare(b.id) : a.stage.localeCompare(b.stage)
|
|
919
|
+
);
|
|
920
|
+
await writeYaml(itemsPath, items);
|
|
921
|
+
return next.map((item) => item.id);
|
|
922
|
+
}
|
|
923
|
+
|
|
657
924
|
// src/change.ts
|
|
658
925
|
async function existingChangeIds(projectRoot, config) {
|
|
659
926
|
const open = await listDirs(changesDir(projectRoot, config));
|
|
@@ -703,7 +970,7 @@ async function createChange(input) {
|
|
|
703
970
|
await materializeStageArtifacts(projectRoot, config, id, workflow, meta, firstStage);
|
|
704
971
|
await setActiveChange(projectRoot, config, id);
|
|
705
972
|
try {
|
|
706
|
-
const { refreshActiveAgentContext } = await import("./agents-
|
|
973
|
+
const { refreshActiveAgentContext } = await import("./agents-PSIOQEYH.js");
|
|
707
974
|
await refreshActiveAgentContext(projectRoot);
|
|
708
975
|
} catch {
|
|
709
976
|
}
|
|
@@ -714,7 +981,7 @@ async function materializeStageArtifacts(projectRoot, config, changeId, workflow
|
|
|
714
981
|
const dir = changePath(projectRoot, config, changeId);
|
|
715
982
|
const tdir = templatesDir(projectRoot);
|
|
716
983
|
for (const artifact of stage.artifacts) {
|
|
717
|
-
const target =
|
|
984
|
+
const target = join5(dir, artifact.path);
|
|
718
985
|
if (await pathExists(target)) continue;
|
|
719
986
|
if (artifact.path.endsWith("/")) {
|
|
720
987
|
await ensureDir(target);
|
|
@@ -723,11 +990,11 @@ async function materializeStageArtifacts(projectRoot, config, changeId, workflow
|
|
|
723
990
|
}
|
|
724
991
|
let content = defaultArtifactContent(artifact.id, meta, stage, workflow);
|
|
725
992
|
if (artifact.template) {
|
|
726
|
-
const templatePath =
|
|
993
|
+
const templatePath = join5(tdir, artifact.template.replace(/^templates\//, ""));
|
|
727
994
|
const candidates = [
|
|
728
|
-
|
|
995
|
+
join5(tdir, artifact.template),
|
|
729
996
|
templatePath,
|
|
730
|
-
|
|
997
|
+
join5(tdir, `${artifact.id}.md`)
|
|
731
998
|
];
|
|
732
999
|
for (const c of candidates) {
|
|
733
1000
|
if (await pathExists(c)) {
|
|
@@ -738,7 +1005,7 @@ async function materializeStageArtifacts(projectRoot, config, changeId, workflow
|
|
|
738
1005
|
}
|
|
739
1006
|
}
|
|
740
1007
|
} else {
|
|
741
|
-
const fallback =
|
|
1008
|
+
const fallback = join5(tdir, `${artifact.id}.md`);
|
|
742
1009
|
if (await pathExists(fallback)) {
|
|
743
1010
|
content = interpolate(await readText(fallback), meta, stage, workflow, {
|
|
744
1011
|
idea: String(meta.flags?.idea ?? meta.title ?? "")
|
|
@@ -748,6 +1015,10 @@ async function materializeStageArtifacts(projectRoot, config, changeId, workflow
|
|
|
748
1015
|
await writeText(target, content);
|
|
749
1016
|
created.push(artifact.path);
|
|
750
1017
|
}
|
|
1018
|
+
if (stage.items_from) {
|
|
1019
|
+
await syncStageItems(dir, stage);
|
|
1020
|
+
created.push("items.yaml");
|
|
1021
|
+
}
|
|
751
1022
|
return created;
|
|
752
1023
|
}
|
|
753
1024
|
function interpolate(template, meta, stage, workflow, extra) {
|
|
@@ -801,7 +1072,7 @@ async function advanceStage(projectRoot, config, changeId, opts) {
|
|
|
801
1072
|
stage2
|
|
802
1073
|
);
|
|
803
1074
|
try {
|
|
804
|
-
const { refreshActiveAgentContext } = await import("./agents-
|
|
1075
|
+
const { refreshActiveAgentContext } = await import("./agents-PSIOQEYH.js");
|
|
805
1076
|
await refreshActiveAgentContext(projectRoot);
|
|
806
1077
|
} catch {
|
|
807
1078
|
}
|
|
@@ -819,7 +1090,7 @@ async function advanceStage(projectRoot, config, changeId, opts) {
|
|
|
819
1090
|
};
|
|
820
1091
|
}
|
|
821
1092
|
}
|
|
822
|
-
const check = await canLeaveStage(ctx, config);
|
|
1093
|
+
const check = await canLeaveStage(ctx, config, projectRoot);
|
|
823
1094
|
warnings.push(...check.warnings);
|
|
824
1095
|
if (!check.ok && !opts?.force) {
|
|
825
1096
|
throw new Error(check.errors.join("\n"));
|
|
@@ -858,7 +1129,7 @@ async function advanceStage(projectRoot, config, changeId, opts) {
|
|
|
858
1129
|
stage
|
|
859
1130
|
);
|
|
860
1131
|
try {
|
|
861
|
-
const { refreshActiveAgentContext } = await import("./agents-
|
|
1132
|
+
const { refreshActiveAgentContext } = await import("./agents-PSIOQEYH.js");
|
|
862
1133
|
await refreshActiveAgentContext(projectRoot);
|
|
863
1134
|
} catch {
|
|
864
1135
|
}
|
|
@@ -972,7 +1243,7 @@ async function completeChange(projectRoot, config, changeId) {
|
|
|
972
1243
|
);
|
|
973
1244
|
}
|
|
974
1245
|
}
|
|
975
|
-
const check = await canLeaveStage(ctx, config);
|
|
1246
|
+
const check = await canLeaveStage(ctx, config, projectRoot);
|
|
976
1247
|
if (!check.ok) {
|
|
977
1248
|
throw new Error(check.errors.join("\n"));
|
|
978
1249
|
}
|
|
@@ -991,7 +1262,7 @@ async function completeChange(projectRoot, config, changeId) {
|
|
|
991
1262
|
let promoted;
|
|
992
1263
|
if (ctx.meta.workflow === "greenfield" || ctx.meta.flags?.greenfield === true) {
|
|
993
1264
|
try {
|
|
994
|
-
const { promoteGreenfieldToMemory: promoteGreenfieldToMemory2 } = await import("./greenfield-
|
|
1265
|
+
const { promoteGreenfieldToMemory: promoteGreenfieldToMemory2 } = await import("./greenfield-QFA5WVHE.js");
|
|
995
1266
|
promoted = await promoteGreenfieldToMemory2(projectRoot, config, changeId);
|
|
996
1267
|
} catch {
|
|
997
1268
|
}
|
|
@@ -1000,7 +1271,7 @@ async function completeChange(projectRoot, config, changeId) {
|
|
|
1000
1271
|
let archivedTo = null;
|
|
1001
1272
|
const shouldArchive = config.persistence.archive_on_complete && ctx.workflow.on_complete?.archive !== false;
|
|
1002
1273
|
if (shouldArchive) {
|
|
1003
|
-
const dest =
|
|
1274
|
+
const dest = join5(archiveDir(projectRoot, config), changeId);
|
|
1004
1275
|
if (await pathExists(dest)) {
|
|
1005
1276
|
throw new Error(`Archive already exists for change: ${changeId}`);
|
|
1006
1277
|
}
|
|
@@ -1010,7 +1281,7 @@ async function completeChange(projectRoot, config, changeId) {
|
|
|
1010
1281
|
archivedTo = dest;
|
|
1011
1282
|
}
|
|
1012
1283
|
if (archivedTo) {
|
|
1013
|
-
const meta = ChangeMetaSchema.parse(await readYaml(
|
|
1284
|
+
const meta = ChangeMetaSchema.parse(await readYaml(join5(archivedTo, "meta.yaml")));
|
|
1014
1285
|
return {
|
|
1015
1286
|
archivedTo,
|
|
1016
1287
|
promoted,
|
|
@@ -1049,7 +1320,7 @@ async function startGreenfield(input) {
|
|
|
1049
1320
|
workflowName: "greenfield",
|
|
1050
1321
|
flags: { greenfield: true, idea }
|
|
1051
1322
|
});
|
|
1052
|
-
const visionPath =
|
|
1323
|
+
const visionPath = join6(ctx.path, "vision.md");
|
|
1053
1324
|
if (await pathExists(visionPath)) {
|
|
1054
1325
|
let body = await readText(visionPath);
|
|
1055
1326
|
if (body.includes("{{idea}}")) {
|
|
@@ -1118,18 +1389,18 @@ function parseFeaturesBacklog(markdown, sourcePath) {
|
|
|
1118
1389
|
return features;
|
|
1119
1390
|
}
|
|
1120
1391
|
async function findFeaturesBacklogPath(projectRoot, config) {
|
|
1121
|
-
const mem =
|
|
1392
|
+
const mem = join6(memoryDir(projectRoot, config), "features.md");
|
|
1122
1393
|
if (await pathExists(mem)) return mem;
|
|
1123
|
-
const { getActiveChangeId: getActiveChangeId2 } = await import("./change-context-
|
|
1394
|
+
const { getActiveChangeId: getActiveChangeId2 } = await import("./change-context-EZJMSGSP.js");
|
|
1124
1395
|
const active = await getActiveChangeId2(projectRoot, config);
|
|
1125
1396
|
if (active) {
|
|
1126
|
-
const p =
|
|
1397
|
+
const p = join6(changePath(projectRoot, config, active), "features.md");
|
|
1127
1398
|
if (await pathExists(p)) return p;
|
|
1128
1399
|
}
|
|
1129
1400
|
const ids = (await listDirs(changesDir(projectRoot, config))).sort().reverse();
|
|
1130
1401
|
for (const id of ids) {
|
|
1131
1402
|
try {
|
|
1132
|
-
const p =
|
|
1403
|
+
const p = join6(changePath(projectRoot, config, id), "features.md");
|
|
1133
1404
|
if (await pathExists(p)) {
|
|
1134
1405
|
const body = await readText(p);
|
|
1135
1406
|
if (parseFeaturesBacklog(body, p).length) return p;
|
|
@@ -1184,7 +1455,7 @@ async function startFeatureFromBacklog(input) {
|
|
|
1184
1455
|
const stage = ctx.workflow.stages.find((s) => s.id === ctx.meta.stage);
|
|
1185
1456
|
const art = stage?.artifacts.find((a) => a.required) ?? stage?.artifacts[0];
|
|
1186
1457
|
if (art) {
|
|
1187
|
-
const artPath =
|
|
1458
|
+
const artPath = join6(ctx.path, art.path);
|
|
1188
1459
|
if (await pathExists(artPath)) {
|
|
1189
1460
|
let body = await readText(artPath);
|
|
1190
1461
|
if (!isSubstantiveArtifactContent(body)) {
|
|
@@ -1240,11 +1511,11 @@ async function promoteGreenfieldToMemory(projectRoot, config, changeId) {
|
|
|
1240
1511
|
];
|
|
1241
1512
|
const written = [];
|
|
1242
1513
|
for (const { from, to } of map) {
|
|
1243
|
-
const src =
|
|
1514
|
+
const src = join6(dir, from);
|
|
1244
1515
|
if (!await pathExists(src)) continue;
|
|
1245
1516
|
const body = await readText(src);
|
|
1246
1517
|
if (!isSubstantiveArtifactContent(body)) continue;
|
|
1247
|
-
const dest =
|
|
1518
|
+
const dest = join6(mem, to);
|
|
1248
1519
|
await writeText(
|
|
1249
1520
|
dest,
|
|
1250
1521
|
body.includes("Promoted from greenfield") ? body : `${body.trim()}
|
|
@@ -1254,9 +1525,9 @@ async function promoteGreenfieldToMemory(projectRoot, config, changeId) {
|
|
|
1254
1525
|
_Promoted from greenfield change \`${changeId}\`._
|
|
1255
1526
|
`
|
|
1256
1527
|
);
|
|
1257
|
-
written.push(
|
|
1528
|
+
written.push(join6(config.memory_path, to));
|
|
1258
1529
|
}
|
|
1259
|
-
const indexPath =
|
|
1530
|
+
const indexPath = join6(mem, "index.md");
|
|
1260
1531
|
if (await pathExists(indexPath)) {
|
|
1261
1532
|
let index = await readText(indexPath);
|
|
1262
1533
|
if (!index.includes("features.md")) {
|
|
@@ -1274,14 +1545,18 @@ _Promoted from greenfield change \`${changeId}\`._
|
|
|
1274
1545
|
export {
|
|
1275
1546
|
PhaseMetricSchema,
|
|
1276
1547
|
TokenUsageMetricSchema,
|
|
1548
|
+
DecisionMetricSchema,
|
|
1277
1549
|
ChangeMetricsSchema,
|
|
1278
1550
|
emptyChangeMetrics,
|
|
1279
1551
|
readChangeMetrics,
|
|
1552
|
+
recordDecisionMetric,
|
|
1280
1553
|
startPhase,
|
|
1281
1554
|
transitionPhase,
|
|
1282
1555
|
assertSessionLogAllowed,
|
|
1283
1556
|
importSessionMetrics,
|
|
1284
1557
|
isSubstantiveArtifactContent,
|
|
1558
|
+
recordSignoff,
|
|
1559
|
+
recordDecisionRequest,
|
|
1285
1560
|
incompletePriorStageErrors,
|
|
1286
1561
|
canLeaveStage,
|
|
1287
1562
|
fallbackIncompleteStageId,
|