@structured-vibe-coding/core 0.5.2 → 0.5.4

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.
@@ -256,9 +256,6 @@ function defaultConfig() {
256
256
  return ConfigSchema.parse({});
257
257
  }
258
258
 
259
- // src/change.ts
260
- import { join as join3 } from "pathe";
261
-
262
259
  // src/slug.ts
263
260
  function slugify(input) {
264
261
  return input.toLowerCase().trim().replace(/['"]/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60).replace(/-+$/g, "");
@@ -453,113 +450,7 @@ async function recommendWorkflow(projectRoot, title, config, opts) {
453
450
  };
454
451
  }
455
452
 
456
- // src/change.ts
457
- async function existingChangeIds(projectRoot, config) {
458
- const open = await listDirs(changesDir(projectRoot, config));
459
- const archived = await listDirs(archiveDir(projectRoot, config));
460
- return /* @__PURE__ */ new Set([...open, ...archived]);
461
- }
462
- async function createChange(input) {
463
- const { projectRoot, config, title, workflowName } = input;
464
- const workflow = await loadWorkflow(projectRoot, workflowName);
465
- if (!workflow.stages.length) {
466
- throw new Error(`Workflow ${workflowName} has no stages`);
467
- }
468
- const baseId = changeDirName(title);
469
- const id = uniqueChangeDirName(baseId, await existingChangeIds(projectRoot, config));
470
- const dir = changePath(projectRoot, config, id);
471
- await ensureDir(dir);
472
- let meta = ChangeMetaSchema.parse({
473
- id,
474
- title,
475
- workflow: workflowName,
476
- created: nowIso(),
477
- updated: nowIso(),
478
- status: "in_progress",
479
- stage: workflow.stages[0].id,
480
- domain: input.domain,
481
- branch: input.branch,
482
- flags: input.flags ?? {},
483
- overrides: {
484
- skip_stages: [],
485
- gates: {},
486
- extra_stages: []
487
- },
488
- skipped: [],
489
- gates: {}
490
- });
491
- const startId = firstActiveStageId(workflow, meta) ?? workflow.stages[0].id;
492
- meta = { ...meta, stage: startId };
493
- const firstStage = getStage(workflow, meta, startId);
494
- if (!firstStage) {
495
- throw new Error(`Workflow ${workflowName} has no active stages`);
496
- }
497
- await writeYaml(changeMetaPath(projectRoot, config, id), meta);
498
- await materializeStageArtifacts(projectRoot, config, id, workflow, meta, firstStage);
499
- await setActiveChange(projectRoot, config, id);
500
- try {
501
- const { refreshActiveAgentContext: refreshActiveAgentContext2 } = await import("./agents-MIMBQVHE.js");
502
- await refreshActiveAgentContext2(projectRoot);
503
- } catch {
504
- }
505
- return buildContext(projectRoot, config, id);
506
- }
507
- async function materializeStageArtifacts(projectRoot, config, changeId, workflow, meta, stage) {
508
- const created = [];
509
- const dir = changePath(projectRoot, config, changeId);
510
- const tdir = templatesDir(projectRoot);
511
- for (const artifact of stage.artifacts) {
512
- const target = join3(dir, artifact.path);
513
- if (await pathExists(target)) continue;
514
- if (artifact.path.endsWith("/")) {
515
- await ensureDir(target);
516
- created.push(artifact.path);
517
- continue;
518
- }
519
- let content = defaultArtifactContent(artifact.id, meta, stage, workflow);
520
- if (artifact.template) {
521
- const templatePath = join3(tdir, artifact.template.replace(/^templates\//, ""));
522
- const candidates = [
523
- join3(tdir, artifact.template),
524
- templatePath,
525
- join3(tdir, `${artifact.id}.md`)
526
- ];
527
- for (const c of candidates) {
528
- if (await pathExists(c)) {
529
- const raw = await readText(c);
530
- content = interpolate(raw, meta, stage, workflow);
531
- break;
532
- }
533
- }
534
- } else {
535
- const fallback = join3(tdir, `${artifact.id}.md`);
536
- if (await pathExists(fallback)) {
537
- content = interpolate(await readText(fallback), meta, stage, workflow);
538
- }
539
- }
540
- await writeText(target, content);
541
- created.push(artifact.path);
542
- }
543
- return created;
544
- }
545
- function interpolate(template, meta, stage, workflow) {
546
- return template.replaceAll("{{title}}", meta.title).replaceAll("{{id}}", meta.id).replaceAll("{{workflow}}", workflow.name).replaceAll("{{stage}}", stage.id).replaceAll("{{stage_title}}", stage.title ?? stage.id).replaceAll("{{date}}", meta.created.slice(0, 10));
547
- }
548
- function defaultArtifactContent(artifactId, meta, stage, workflow) {
549
- return `# ${artifactId}
550
-
551
- > Change: **${meta.title}** (\`${meta.id}\`)
552
- > Workflow: \`${workflow.name}\` \xB7 Stage: \`${stage.id}\`
553
-
554
- ## Summary
555
-
556
- <!-- Write the intent / content for this artifact -->
557
-
558
- ## Notes
559
-
560
- -
561
- `;
562
- }
453
+ // src/change-context.ts
563
454
  async function loadChangeMeta(projectRoot, config, changeId) {
564
455
  const path = changeMetaPath(projectRoot, config, changeId);
565
456
  if (!await pathExists(path)) {
@@ -633,231 +524,9 @@ async function resolveChangeId(projectRoot, config, changeId) {
633
524
  }
634
525
  return active;
635
526
  }
636
- async function canLeaveStage(ctx, config) {
637
- const stage = getStage(ctx.workflow, ctx.meta, ctx.meta.stage);
638
- const errors = [];
639
- const warnings = [];
640
- if (!stage) {
641
- return { ok: false, errors: [`Unknown stage: ${ctx.meta.stage}`], warnings };
642
- }
643
- for (const artifact of stage.artifacts) {
644
- if (!artifact.required) continue;
645
- const target = join3(ctx.path, artifact.path);
646
- if (!await pathExists(target)) {
647
- errors.push(`Missing required artifact: ${artifact.path}`);
648
- continue;
649
- }
650
- }
651
- const declared = stage.gate?.type ?? "soft";
652
- let effectiveGate = declared;
653
- if (config.policy.gates === "hard" && declared === "soft") {
654
- effectiveGate = "hard";
655
- }
656
- const gateState = ctx.meta.gates?.[stage.id];
657
- if (effectiveGate === "hard") {
658
- if (!gateState || gateState.status !== "approved" && gateState.status !== "waived") {
659
- errors.push(
660
- `Hard gate on stage "${stage.id}" is not approved. Run \`sdd gate approve\` or complete the checklist.`
661
- );
662
- }
663
- } else if (effectiveGate === "soft" && stage.gate?.checklist?.length) {
664
- if (!gateState || gateState.status === "pending") {
665
- warnings.push(
666
- `Soft gate checklist not signed off on "${stage.id}" (proceeding allowed).`
667
- );
668
- }
669
- }
670
- const requiredCmds = stage.verify?.commands?.filter((c) => c.required) ?? [];
671
- if (requiredCmds.length) {
672
- const verifyOk = ctx.meta.verify_results?.[stage.id]?.ok === true;
673
- const humanOverride = gateState?.status === "waived" || gateState?.status === "approved";
674
- if (!verifyOk && !humanOverride) {
675
- errors.push(
676
- `Required local verify commands have not passed for "${stage.id}". Run \`sdd verify\` (or \`sdd gate approve/waive\` with a note).`
677
- );
678
- } else if (!verifyOk && humanOverride) {
679
- warnings.push(
680
- `Stage "${stage.id}" gate signed off without a successful required verify run.`
681
- );
682
- }
683
- }
684
- return { ok: errors.length === 0, errors, warnings };
685
- }
686
- async function advanceStage(projectRoot, config, changeId, opts) {
687
- const ctx = await buildContext(projectRoot, config, changeId);
688
- const warnings = [];
689
- if (ctx.meta.status === "completed") {
690
- throw new Error("Change is already completed.");
691
- }
692
- const check = await canLeaveStage(ctx, config);
693
- warnings.push(...check.warnings);
694
- if (!check.ok && !opts?.force) {
695
- throw new Error(check.errors.join("\n"));
696
- }
697
- const from = ctx.meta.stage;
698
- const next = nextStageId(ctx.workflow, ctx.meta);
699
- if (!next) {
700
- return {
701
- ctx,
702
- from,
703
- to: null,
704
- completed: false,
705
- artifactsCreated: [],
706
- warnings: [...warnings, "No further stages. Run `sdd complete` when ready."]
707
- };
708
- }
709
- ctx.meta.stage = next;
710
- await saveChangeMeta(projectRoot, config, ctx.meta);
711
- const stage = getStage(ctx.workflow, ctx.meta, next);
712
- const artifactsCreated = await materializeStageArtifacts(
713
- projectRoot,
714
- config,
715
- changeId,
716
- ctx.workflow,
717
- ctx.meta,
718
- stage
719
- );
720
- try {
721
- const { refreshActiveAgentContext: refreshActiveAgentContext2 } = await import("./agents-MIMBQVHE.js");
722
- await refreshActiveAgentContext2(projectRoot);
723
- } catch {
724
- }
725
- const updated = await buildContext(projectRoot, config, changeId);
726
- return {
727
- ctx: updated,
728
- from,
729
- to: next,
730
- completed: false,
731
- artifactsCreated,
732
- warnings
733
- };
734
- }
735
- async function skipStage(projectRoot, config, changeId, stageId, reason) {
736
- if (!config.per_change.allow_skip) {
737
- throw new Error("Skipping stages is disabled in .sdd/config.yaml (per_change.allow_skip).");
738
- }
739
- if (config.per_change.require_reason_on_skip && !reason.trim()) {
740
- throw new Error("A reason is required to skip a stage.");
741
- }
742
- const ctx = await buildContext(projectRoot, config, changeId);
743
- const stage = getStage(ctx.workflow, ctx.meta, stageId);
744
- if (!stage) {
745
- throw new Error(`Stage "${stageId}" not found in workflow ${ctx.meta.workflow}`);
746
- }
747
- if (!stage.skippable) {
748
- throw new Error(`Stage "${stageId}" is not skippable.`);
749
- }
750
- const skip_stages = new Set(ctx.meta.overrides.skip_stages ?? []);
751
- skip_stages.add(stageId);
752
- ctx.meta.overrides.skip_stages = [...skip_stages];
753
- ctx.meta.skipped = [
754
- ...ctx.meta.skipped ?? [],
755
- { stage: stageId, reason, at: nowIso() }
756
- ];
757
- if (ctx.meta.stage === stageId) {
758
- const next = nextStageId(ctx.workflow, ctx.meta);
759
- if (next) {
760
- ctx.meta.stage = next;
761
- const nextStage = getStage(ctx.workflow, ctx.meta, next);
762
- if (nextStage) {
763
- await materializeStageArtifacts(
764
- projectRoot,
765
- config,
766
- changeId,
767
- ctx.workflow,
768
- ctx.meta,
769
- nextStage
770
- );
771
- }
772
- }
773
- }
774
- await saveChangeMeta(projectRoot, config, ctx.meta);
775
- return buildContext(projectRoot, config, changeId);
776
- }
777
- async function switchWorkflow(projectRoot, config, changeId, workflowName, reason) {
778
- const ctx = await buildContext(projectRoot, config, changeId);
779
- const workflow = await loadWorkflow(projectRoot, workflowName);
780
- if (config.allowed_workflows?.length && !config.allowed_workflows.includes(workflowName)) {
781
- throw new Error(
782
- `Workflow "${workflowName}" is not in allowed_workflows: ${config.allowed_workflows.join(", ")}`
783
- );
784
- }
785
- ctx.meta.workflow = workflowName;
786
- const hasStage = workflow.stages.some((s) => s.id === ctx.meta.stage);
787
- if (!hasStage) {
788
- ctx.meta.stage = workflow.stages[0].id;
789
- }
790
- if (reason) {
791
- ctx.meta.flags = {
792
- ...ctx.meta.flags,
793
- workflow_switch_reason: reason
794
- };
795
- }
796
- await saveChangeMeta(projectRoot, config, ctx.meta);
797
- const stage = getStage(workflow, ctx.meta, ctx.meta.stage);
798
- if (stage) {
799
- await materializeStageArtifacts(projectRoot, config, changeId, workflow, ctx.meta, stage);
800
- }
801
- return buildContext(projectRoot, config, changeId);
802
- }
803
- async function approveGate(projectRoot, config, changeId, stageId, note, status = "approved") {
804
- const ctx = await buildContext(projectRoot, config, changeId);
805
- const id = stageId ?? ctx.meta.stage;
806
- ctx.meta.gates = {
807
- ...ctx.meta.gates,
808
- [id]: { status, note, at: nowIso() }
809
- };
810
- await saveChangeMeta(projectRoot, config, ctx.meta);
811
- return buildContext(projectRoot, config, changeId);
812
- }
813
- async function completeChange(projectRoot, config, changeId) {
814
- const ctx = await buildContext(projectRoot, config, changeId);
815
- const next = nextStageId(ctx.workflow, ctx.meta);
816
- if (next) {
817
- const active = activeStages(ctx.workflow, ctx.meta);
818
- const last = active[active.length - 1];
819
- if (last && ctx.meta.stage !== last.id) {
820
- throw new Error(
821
- `Cannot complete: still on stage "${ctx.meta.stage}". Finish remaining stages or skip them. Next: ${next}`
822
- );
823
- }
824
- }
825
- const check = await canLeaveStage(ctx, config);
826
- if (!check.ok) {
827
- throw new Error(check.errors.join("\n"));
828
- }
829
- ctx.meta.status = "completed";
830
- ctx.meta.completed_at = nowIso();
831
- await saveChangeMeta(projectRoot, config, ctx.meta);
832
- await clearActiveIf(projectRoot, config, changeId);
833
- let archivedTo = null;
834
- const shouldArchive = config.persistence.archive_on_complete && ctx.workflow.on_complete?.archive !== false;
835
- if (shouldArchive) {
836
- const dest = join3(archiveDir(projectRoot, config), changeId);
837
- if (await pathExists(dest)) {
838
- throw new Error(`Archive already exists for change: ${changeId}`);
839
- }
840
- await ensureDir(archiveDir(projectRoot, config));
841
- await copyDir(ctx.path, dest);
842
- await removePath(ctx.path);
843
- archivedTo = dest;
844
- }
845
- if (archivedTo) {
846
- const meta = ChangeMetaSchema.parse(await readYaml(join3(archivedTo, "meta.yaml")));
847
- return {
848
- archivedTo,
849
- ctx: {
850
- id: changeId,
851
- path: archivedTo,
852
- meta,
853
- workflow: ctx.workflow,
854
- stages: ctx.stages,
855
- active: ctx.active
856
- }
857
- };
858
- }
859
- return { archivedTo, ctx: await buildContext(projectRoot, config, changeId) };
860
- }
527
+
528
+ // src/agent-handoff.ts
529
+ import { join as join3 } from "pathe";
861
530
  function formatStatus(ctx) {
862
531
  const lines = [];
863
532
  lines.push(`Change: ${ctx.meta.title}`);
@@ -881,7 +550,9 @@ function formatStatus(ctx) {
881
550
  const gate = ctx.meta.gates?.[stage.id];
882
551
  const gateInfo = gate ? ` [${gate.status}]` : "";
883
552
  const skipInfo = skipped ? " (skipped)" : "";
884
- lines.push(` [${mark}] ${stage.id}${stage.title ? ` \u2014 ${stage.title}` : ""}${gateInfo}${skipInfo}`);
553
+ lines.push(
554
+ ` [${mark}] ${stage.id}${stage.title ? ` \u2014 ${stage.title}` : ""}${gateInfo}${skipInfo}`
555
+ );
885
556
  }
886
557
  if (ctx.meta.overrides?.skip_stages?.length) {
887
558
  lines.push("");
@@ -900,7 +571,9 @@ async function buildAgentPrompt(ctx, config, projectRoot) {
900
571
  parts.push(`- Title: ${ctx.meta.title}`);
901
572
  parts.push(`- ID: ${ctx.id}`);
902
573
  parts.push(`- Workflow: ${ctx.meta.workflow}`);
903
- parts.push(`- Current stage: ${ctx.meta.stage}${stage?.title ? ` (${stage.title})` : ""}`);
574
+ parts.push(
575
+ `- Current stage: ${ctx.meta.stage}${stage?.title ? ` (${stage.title})` : ""}`
576
+ );
904
577
  parts.push(`- Change path: ${ctx.path}`);
905
578
  parts.push("");
906
579
  if (stage?.summary) {
@@ -921,6 +594,7 @@ async function buildAgentPrompt(ctx, config, projectRoot) {
921
594
  parts.push("");
922
595
  parts.push(`## Artifacts to read`);
923
596
  const memoryHint = join3(projectRoot, config.memory_path);
597
+ parts.push(`- Documentation map (if present): ${memoryHint}/index.md`);
924
598
  parts.push(`- Memory: ${memoryHint}/`);
925
599
  for (const s of ctx.stages) {
926
600
  for (const a of s.artifacts) {
@@ -935,64 +609,102 @@ async function buildAgentPrompt(ctx, config, projectRoot) {
935
609
  }
936
610
  }
937
611
  parts.push("");
938
- parts.push(`When done with this stage, the human will run \`sdd next\` or \`sdd verify\` / \`sdd complete\`.`);
612
+ parts.push(
613
+ `When done with this stage, the human will run \`sdd next\` or \`sdd verify\` / \`sdd complete\`.`
614
+ );
939
615
  return parts.join("\n");
940
616
  }
941
617
 
942
618
  // src/agents.ts
943
- var ALL_AGENT_TARGETS = ["copilot", "claude-code"];
944
- var DEFAULT_INIT_INTEGRATION = "copilot";
945
- var AGENT_TARGET_OPTIONS = [
619
+ var AGENT_INTEGRATIONS = [
946
620
  {
947
621
  id: "copilot",
948
622
  key: "copilot",
623
+ aliases: ["github-copilot", "gh-copilot"],
949
624
  label: "GitHub Copilot",
950
625
  hint: ".github/agents/*.agent.md (VS Code, Cursor, JetBrains, \u2026)",
951
626
  requiresCli: false,
952
- installUrl: "https://github.com/features/copilot"
627
+ installUrl: "https://github.com/features/copilot",
628
+ rolePath: (roleId) => `.github/agents/${roleId}.agent.md`,
629
+ agentsMdRow: "| GitHub Copilot | `.github/agents/*.agent.md` (same roles; any IDE that supports Copilot agents) |"
953
630
  },
954
631
  {
955
632
  id: "claude-code",
956
633
  key: "claude",
634
+ aliases: ["claude-code", "claudecode"],
957
635
  label: "Claude Code",
958
636
  hint: ".claude/agents/*.md (terminal agent)",
959
637
  requiresCli: true,
960
- installUrl: "https://docs.anthropic.com/en/docs/claude-code"
638
+ cliBinary: "claude",
639
+ installUrl: "https://docs.anthropic.com/en/docs/claude-code",
640
+ rolePath: (roleId) => `.claude/agents/${roleId}.md`,
641
+ agentsMdRow: "| Claude Code | `.claude/agents/` (`sdd`, `sdd-planner`, `sdd-implementer`, `sdd-reviewer`) |"
961
642
  }
962
643
  ];
644
+ var ALL_AGENT_TARGETS = AGENT_INTEGRATIONS.map((i) => i.id);
645
+ var DEFAULT_INIT_INTEGRATION = "copilot";
646
+ var AGENT_TARGET_OPTIONS = AGENT_INTEGRATIONS.map(
647
+ ({ id, key, label, hint, requiresCli, installUrl }) => ({
648
+ id,
649
+ key,
650
+ label,
651
+ hint,
652
+ requiresCli,
653
+ installUrl
654
+ })
655
+ );
656
+ var IDE_NAMES = /* @__PURE__ */ new Set([
657
+ "intellij",
658
+ "idea",
659
+ "jetbrains",
660
+ "vscode",
661
+ "vs-code",
662
+ "cursor"
663
+ ]);
664
+ function getIntegration(target) {
665
+ const found = AGENT_INTEGRATIONS.find((i) => i.id === target);
666
+ if (!found) throw new Error(`Unknown agent integration: ${target}`);
667
+ return found;
668
+ }
963
669
  function integrationKeyFor(target) {
964
- return AGENT_TARGET_OPTIONS.find((o) => o.id === target)?.key ?? target;
670
+ return getIntegration(target).key;
965
671
  }
966
672
  function optionForTarget(target) {
967
- return AGENT_TARGET_OPTIONS.find((o) => o.id === target);
673
+ return getIntegration(target);
674
+ }
675
+ function parseIntegration(raw) {
676
+ const p = raw.trim().toLowerCase();
677
+ if (!p) {
678
+ throw new Error(
679
+ `Expected one AI coding agent. Choose from: ${AGENT_INTEGRATIONS.map((i) => i.key).join(", ")}`
680
+ );
681
+ }
682
+ if (IDE_NAMES.has(p)) {
683
+ throw new Error(
684
+ `"${p}" is an IDE, not an AI coding agent. Choose: copilot or claude (Claude Code).`
685
+ );
686
+ }
687
+ for (const integ of AGENT_INTEGRATIONS) {
688
+ if (integ.id === p || integ.key === p || integ.aliases.includes(p)) {
689
+ return integ.id;
690
+ }
691
+ }
692
+ throw new Error(
693
+ `Unknown AI coding agent "${raw}". Choose from: ${AGENT_INTEGRATIONS.map((i) => i.key).join(", ")}`
694
+ );
968
695
  }
969
696
  function parseAgentTargets(raw) {
970
- const parts = (Array.isArray(raw) ? raw : [raw]).flatMap((s) => String(s).split(",")).map((s) => s.trim().toLowerCase()).filter(Boolean);
697
+ const parts = (Array.isArray(raw) ? raw : [raw]).flatMap((s) => String(s).split(",")).map((s) => s.trim()).filter(Boolean);
698
+ if (!parts.length) {
699
+ throw new Error("No AI coding agent specified");
700
+ }
971
701
  const out = [];
972
702
  for (const p of parts) {
973
- if (p === "intellij" || p === "idea" || p === "jetbrains" || p === "vscode" || p === "vs-code" || p === "cursor") {
974
- throw new Error(
975
- `"${p}" is an IDE, not an AI coding agent. Choose: copilot or claude (Claude Code).`
976
- );
977
- }
978
- const id = p === "claude" || p === "claude-code" || p === "claudecode" ? "claude-code" : p === "github-copilot" || p === "gh-copilot" || p === "copilot" ? "copilot" : p;
979
- if (!ALL_AGENT_TARGETS.includes(id)) {
980
- const keys = AGENT_TARGET_OPTIONS.map((o) => o.key).join(", ");
981
- throw new Error(`Unknown AI coding agent "${p}". Choose from: ${keys}`);
982
- }
703
+ const id = parseIntegration(p);
983
704
  if (!out.includes(id)) out.push(id);
984
705
  }
985
706
  return out;
986
707
  }
987
- function parseIntegration(raw) {
988
- const list = parseAgentTargets(raw);
989
- if (list.length !== 1) {
990
- throw new Error(
991
- `Expected one AI coding agent (like Speckit --integration), got: ${raw}`
992
- );
993
- }
994
- return list[0];
995
- }
996
708
  var SDD_AGENT_ROLES = [
997
709
  {
998
710
  id: "sdd",
@@ -1032,13 +744,8 @@ var SDD_AGENT_ROLES = [
1032
744
  ]
1033
745
  }
1034
746
  ];
1035
- async function installAgentIntegrations(opts) {
1036
- if (!opts.targets?.length) {
1037
- throw new Error(
1038
- `Specify at least one AI coding agent: ${ALL_AGENT_TARGETS.join(", ")} (not an IDE; do not install all by default)`
1039
- );
1040
- }
1041
- const targets = opts.targets;
747
+ async function installAgentIntegration(opts) {
748
+ const integ = getIntegration(opts.target);
1042
749
  const created = [];
1043
750
  const skipped = [];
1044
751
  const root = opts.projectRoot;
@@ -1055,47 +762,48 @@ async function installAgentIntegrations(opts) {
1055
762
  await write(join4(".sdd", "protocol.md"), PROTOCOL_MD);
1056
763
  await refreshActiveAgentContext(root);
1057
764
  for (const role of SDD_AGENT_ROLES) {
1058
- const body = renderThinAgent(role);
1059
- if (targets.includes("claude-code")) {
1060
- await write(`.claude/agents/${role.id}.md`, body);
1061
- }
1062
- if (targets.includes("copilot")) {
1063
- await write(`.github/agents/${role.id}.agent.md`, body);
1064
- }
765
+ await write(integ.rolePath(role.id), renderThinAgent(role));
1065
766
  }
1066
- await write("AGENTS.md", renderAgentsMd(targets));
767
+ await write("AGENTS.md", renderAgentsMd(integ));
1067
768
  await write(
1068
769
  join4(".sdd", "agents.json"),
1069
770
  JSON.stringify(
1070
771
  {
1071
- version: 2,
772
+ version: 3,
1072
773
  mode: "agents-only",
1073
774
  protocol: ".sdd/protocol.md",
1074
775
  activeContext: ".sdd/active-context.md",
1075
776
  roles: SDD_AGENT_ROLES.map((r) => r.id),
1076
- installed: targets,
1077
- /** Speckit-style primary integration key (first target). */
1078
- integration: targets[0] ? integrationKeyFor(targets[0]) : null,
777
+ /** Speckit-style public key */
778
+ ai: integ.key,
779
+ integration: integ.key,
780
+ installed: [integ.id],
1079
781
  updated: (/* @__PURE__ */ new Date()).toISOString()
1080
782
  },
1081
783
  null,
1082
784
  2
1083
785
  ) + "\n"
1084
786
  );
1085
- await write(
1086
- join4(".sdd", "init-options.json"),
1087
- JSON.stringify(
1088
- {
1089
- ai: targets[0] ? integrationKeyFor(targets[0]) : null,
1090
- integration: targets[0] ? integrationKeyFor(targets[0]) : null,
1091
- installed: targets.map(integrationKeyFor),
1092
- updated: (/* @__PURE__ */ new Date()).toISOString()
1093
- },
1094
- null,
1095
- 2
1096
- ) + "\n"
1097
- );
1098
- return { created, skipped, targets };
787
+ return { created, skipped, target: opts.target };
788
+ }
789
+ async function installAgentIntegrations(opts) {
790
+ if (!opts.targets?.length) {
791
+ throw new Error(
792
+ `Specify one AI coding agent: ${AGENT_INTEGRATIONS.map((i) => i.key).join(", ")}`
793
+ );
794
+ }
795
+ const created = [];
796
+ const skipped = [];
797
+ for (const target of opts.targets) {
798
+ const r = await installAgentIntegration({
799
+ projectRoot: opts.projectRoot,
800
+ target,
801
+ force: opts.force
802
+ });
803
+ created.push(...r.created);
804
+ skipped.push(...r.skipped);
805
+ }
806
+ return { created, skipped, targets: opts.targets };
1099
807
  }
1100
808
  function renderThinAgent(role) {
1101
809
  const rules = role.roleRules.map((r) => `- ${r}`).join("\n");
@@ -1178,7 +886,7 @@ This repo uses **Structured Vibe Coding** (\`sdd\`). Agents must follow this fil
1178
886
  |------|--------|
1179
887
  | Process state | \`changes/<id>/meta.yaml\` (workflow, stage, gates) |
1180
888
  | Current task snapshot | \`.sdd/active-context.md\` |
1181
- | Stable product/tech rules | \`memory/*.md\` |
889
+ | Stable product/tech rules | \`memory/*.md\` (start at \`memory/index.md\` if present) |
1182
890
  | Stage artifacts | files in \`changes/<id>/\` for the active change |
1183
891
 
1184
892
  ## Required read order
@@ -1186,7 +894,7 @@ This repo uses **Structured Vibe Coding** (\`sdd\`). Agents must follow this fil
1186
894
  1. \`.sdd/active-context.md\`
1187
895
  2. This file (\`.sdd/protocol.md\`)
1188
896
  3. \`meta.yaml\` + artifacts for the **current stage** (and prior stages if needed)
1189
- 4. \`memory/\` when architecture or conventions matter
897
+ 4. \`memory/index.md\` (documentation map) if present, then linked memory pages when architecture or conventions matter
1190
898
 
1191
899
  ## Stage behavior
1192
900
 
@@ -1223,18 +931,7 @@ sdd complete
1223
931
  - Deleting \`.sdd\` / archive history
1224
932
  - Claiming complete without verify when required
1225
933
  `;
1226
- function renderAgentsMd(targets) {
1227
- const rows = [];
1228
- if (targets.includes("claude-code")) {
1229
- rows.push(
1230
- "| Claude Code | `.claude/agents/` (`sdd`, `sdd-planner`, `sdd-implementer`, `sdd-reviewer`) |"
1231
- );
1232
- }
1233
- if (targets.includes("copilot")) {
1234
- rows.push(
1235
- "| GitHub Copilot | `.github/agents/*.agent.md` (same roles; any IDE that supports Copilot agents) |"
1236
- );
1237
- }
934
+ function renderAgentsMd(integ) {
1238
935
  return `# Agents
1239
936
 
1240
937
  This repo uses **SDD agents only** (no skills).
@@ -1245,10 +942,11 @@ AI coding agents are **not** the same as IDEs: VS Code, Cursor, and IntelliJ hos
1245
942
  |------------|--|
1246
943
  | Live task | \`.sdd/active-context.md\` |
1247
944
  | Playbook | \`.sdd/protocol.md\` |
945
+ | Doc map (stable) | \`memory/index.md\` if present |
1248
946
 
1249
947
  | AI agent | Files |
1250
948
  |----------|--------|
1251
- ${rows.join("\n")}
949
+ ${integ.agentsMdRow}
1252
950
 
1253
951
  Refresh context: \`sdd agents refresh\`.
1254
952
  `;
@@ -1304,33 +1002,29 @@ export {
1304
1002
  nowIso,
1305
1003
  changeDirName,
1306
1004
  uniqueChangeDirName,
1005
+ loadChangeMeta,
1006
+ saveChangeMeta,
1007
+ buildContext,
1008
+ listChanges,
1009
+ getActiveChangeId,
1010
+ clearActiveIf,
1011
+ setActiveChange,
1012
+ resolveChangeId,
1013
+ formatStatus,
1014
+ buildAgentPrompt,
1015
+ AGENT_INTEGRATIONS,
1307
1016
  ALL_AGENT_TARGETS,
1308
1017
  DEFAULT_INIT_INTEGRATION,
1309
1018
  AGENT_TARGET_OPTIONS,
1019
+ getIntegration,
1310
1020
  integrationKeyFor,
1311
1021
  optionForTarget,
1312
- parseAgentTargets,
1313
1022
  parseIntegration,
1023
+ parseAgentTargets,
1314
1024
  SDD_AGENT_ROLES,
1025
+ installAgentIntegration,
1315
1026
  installAgentIntegrations,
1316
1027
  renderThinAgent,
1317
1028
  refreshActiveAgentContext,
1318
- PROTOCOL_MD,
1319
- createChange,
1320
- loadChangeMeta,
1321
- saveChangeMeta,
1322
- buildContext,
1323
- listChanges,
1324
- getActiveChangeId,
1325
- clearActiveIf,
1326
- setActiveChange,
1327
- resolveChangeId,
1328
- canLeaveStage,
1329
- advanceStage,
1330
- skipStage,
1331
- switchWorkflow,
1332
- approveGate,
1333
- completeChange,
1334
- formatStatus,
1335
- buildAgentPrompt
1029
+ PROTOCOL_MD
1336
1030
  };