@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.
Files changed (29) hide show
  1. package/defaults/teams/research/roles/researcher.md +29 -0
  2. package/defaults/teams/research/team.yaml +7 -0
  3. package/defaults/teams/software/roles/architect.md +31 -0
  4. package/defaults/teams/software/roles/dev-lead.md +32 -0
  5. package/defaults/teams/software/roles/developer.md +30 -0
  6. package/defaults/teams/software/roles/product-manager.md +32 -0
  7. package/defaults/teams/software/roles/product-owner.md +30 -0
  8. package/defaults/teams/software/roles/qa.md +35 -0
  9. package/defaults/teams/software/routing.yaml +28 -0
  10. package/defaults/teams/software/team.yaml +24 -0
  11. package/defaults/teams/solo/roles/developer.md +28 -0
  12. package/defaults/teams/solo/team.yaml +7 -0
  13. package/defaults/templates/plan.md +17 -0
  14. package/defaults/templates/review.md +11 -0
  15. package/defaults/templates/story.md +13 -0
  16. package/defaults/templates/task.md +11 -0
  17. package/defaults/workflows/team-software.yaml +153 -0
  18. package/dist/{agents-AKSUXQHN.js → agents-PSIOQEYH.js} +3 -3
  19. package/dist/{change-context-REE7UKU7.js → change-context-EZJMSGSP.js} +1 -1
  20. package/dist/{chunk-4PIRWYRA.js → chunk-26YG6A4M.js} +166 -81
  21. package/dist/{chunk-LHSR7JG2.js → chunk-2FJBFYVJ.js} +306 -31
  22. package/dist/{chunk-QGP43XCN.js → chunk-H55RFAEI.js} +1 -1
  23. package/dist/{chunk-QHV5KZKX.js → chunk-HG7TPW5V.js} +93 -8
  24. package/dist/{chunk-44LQ5BKZ.js → chunk-TGRV6WGS.js} +812 -4
  25. package/dist/{gather-ZNV2NAUN.js → gather-7TSVKQO2.js} +1 -1
  26. package/dist/{greenfield-I5S5OS2D.js → greenfield-QFA5WVHE.js} +3 -3
  27. package/dist/index.d.ts +804 -10
  28. package/dist/index.js +2765 -524
  29. 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-QHV5KZKX.js";
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 join4 } from "pathe";
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 join3 } from "pathe";
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 = join2(changePath2, artifact.path);
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 = join2(changePath2, artifact.path);
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
- if (effectiveGate === "hard") {
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-AKSUXQHN.js");
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 = join3(dir, artifact.path);
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 = join3(tdir, artifact.template.replace(/^templates\//, ""));
993
+ const templatePath = join5(tdir, artifact.template.replace(/^templates\//, ""));
727
994
  const candidates = [
728
- join3(tdir, artifact.template),
995
+ join5(tdir, artifact.template),
729
996
  templatePath,
730
- join3(tdir, `${artifact.id}.md`)
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 = join3(tdir, `${artifact.id}.md`);
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-AKSUXQHN.js");
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-AKSUXQHN.js");
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-I5S5OS2D.js");
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 = join3(archiveDir(projectRoot, config), changeId);
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(join3(archivedTo, "meta.yaml")));
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 = join4(ctx.path, "vision.md");
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 = join4(memoryDir(projectRoot, config), "features.md");
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-REE7UKU7.js");
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 = join4(changePath(projectRoot, config, active), "features.md");
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 = join4(changePath(projectRoot, config, id), "features.md");
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 = join4(ctx.path, art.path);
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 = join4(dir, from);
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 = join4(mem, to);
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(join4(config.memory_path, to));
1528
+ written.push(join6(config.memory_path, to));
1258
1529
  }
1259
- const indexPath = join4(mem, "index.md");
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,
@@ -26,7 +26,7 @@ function toProcessEnv(base, extra) {
26
26
  }
27
27
 
28
28
  // src/version.ts
29
- var SDD_CORE_VERSION = "0.18.0" ? "0.18.0" : "0.0.0-dev";
29
+ var SDD_CORE_VERSION = "0.19.0" ? "0.19.0" : "0.0.0-dev";
30
30
 
31
31
  // src/mcp/client.ts
32
32
  function mcpTimeoutMs() {