@isaacriehm/cairn-state 0.22.5

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 (94) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +11 -0
  3. package/dist/.tsbuildinfo +1 -0
  4. package/dist/alignment-pending.d.ts +28 -0
  5. package/dist/alignment-pending.js +83 -0
  6. package/dist/alignment-pending.js.map +1 -0
  7. package/dist/anchor-map.d.ts +14 -0
  8. package/dist/anchor-map.js +56 -0
  9. package/dist/anchor-map.js.map +1 -0
  10. package/dist/archive.d.ts +48 -0
  11. package/dist/archive.js +96 -0
  12. package/dist/archive.js.map +1 -0
  13. package/dist/cache.d.ts +48 -0
  14. package/dist/cache.js +241 -0
  15. package/dist/cache.js.map +1 -0
  16. package/dist/component-registry.d.ts +93 -0
  17. package/dist/component-registry.js +0 -0
  18. package/dist/component-registry.js.map +1 -0
  19. package/dist/components.d.ts +192 -0
  20. package/dist/components.js +603 -0
  21. package/dist/components.js.map +1 -0
  22. package/dist/config.d.ts +9 -0
  23. package/dist/config.js +26 -0
  24. package/dist/config.js.map +1 -0
  25. package/dist/drift.d.ts +8 -0
  26. package/dist/drift.js +23 -0
  27. package/dist/drift.js.map +1 -0
  28. package/dist/file-candidates-map.d.ts +23 -0
  29. package/dist/file-candidates-map.js +76 -0
  30. package/dist/file-candidates-map.js.map +1 -0
  31. package/dist/frontmatter.d.ts +32 -0
  32. package/dist/frontmatter.js +77 -0
  33. package/dist/frontmatter.js.map +1 -0
  34. package/dist/fs.d.ts +36 -0
  35. package/dist/fs.js +47 -0
  36. package/dist/fs.js.map +1 -0
  37. package/dist/glob.d.ts +10 -0
  38. package/dist/glob.js +46 -0
  39. package/dist/glob.js.map +1 -0
  40. package/dist/home.d.ts +69 -0
  41. package/dist/home.js +168 -0
  42. package/dist/home.js.map +1 -0
  43. package/dist/index.d.ts +29 -0
  44. package/dist/index.js +30 -0
  45. package/dist/index.js.map +1 -0
  46. package/dist/languages.d.ts +113 -0
  47. package/dist/languages.js +512 -0
  48. package/dist/languages.js.map +1 -0
  49. package/dist/ledgers.d.ts +14 -0
  50. package/dist/ledgers.js +105 -0
  51. package/dist/ledgers.js.map +1 -0
  52. package/dist/logger.d.ts +13 -0
  53. package/dist/logger.js +17 -0
  54. package/dist/logger.js.map +1 -0
  55. package/dist/manifest.d.ts +10 -0
  56. package/dist/manifest.js +84 -0
  57. package/dist/manifest.js.map +1 -0
  58. package/dist/missions.d.ts +119 -0
  59. package/dist/missions.js +414 -0
  60. package/dist/missions.js.map +1 -0
  61. package/dist/paths.d.ts +117 -0
  62. package/dist/paths.js +241 -0
  63. package/dist/paths.js.map +1 -0
  64. package/dist/quality-grades.d.ts +11 -0
  65. package/dist/quality-grades.js +100 -0
  66. package/dist/quality-grades.js.map +1 -0
  67. package/dist/rejected.d.ts +42 -0
  68. package/dist/rejected.js +100 -0
  69. package/dist/rejected.js.map +1 -0
  70. package/dist/schemas.d.ts +789 -0
  71. package/dist/schemas.js +506 -0
  72. package/dist/schemas.js.map +1 -0
  73. package/dist/scope-index.d.ts +96 -0
  74. package/dist/scope-index.js +299 -0
  75. package/dist/scope-index.js.map +1 -0
  76. package/dist/slug.d.ts +81 -0
  77. package/dist/slug.js +138 -0
  78. package/dist/slug.js.map +1 -0
  79. package/dist/sot-bindings.d.ts +14 -0
  80. package/dist/sot-bindings.js +79 -0
  81. package/dist/sot-bindings.js.map +1 -0
  82. package/dist/sot-cache.d.ts +18 -0
  83. package/dist/sot-cache.js +62 -0
  84. package/dist/sot-cache.js.map +1 -0
  85. package/dist/text.d.ts +27 -0
  86. package/dist/text.js +63 -0
  87. package/dist/text.js.map +1 -0
  88. package/dist/topic-index.d.ts +27 -0
  89. package/dist/topic-index.js +82 -0
  90. package/dist/topic-index.js.map +1 -0
  91. package/dist/walk.d.ts +7 -0
  92. package/dist/walk.js +34 -0
  93. package/dist/walk.js.map +1 -0
  94. package/package.json +35 -0
@@ -0,0 +1,13 @@
1
+ /** Minimal logger interface for the state package. */
2
+ export interface StateLogger {
3
+ debug: (obj: object, msg: string) => void;
4
+ info: (obj: object, msg: string) => void;
5
+ warn: (obj: object, msg: string) => void;
6
+ error: (obj: object, msg: string) => void;
7
+ }
8
+ /** No-op logger as default. */
9
+ export declare const nullLogger: StateLogger;
10
+ /** Set the active logger for the state package. */
11
+ export declare function setStateLogger(l: StateLogger): void;
12
+ /** Get the active logger. */
13
+ export declare function getLogger(): StateLogger;
package/dist/logger.js ADDED
@@ -0,0 +1,17 @@
1
+ /** No-op logger as default. */
2
+ export const nullLogger = {
3
+ debug: () => { },
4
+ info: () => { },
5
+ warn: () => { },
6
+ error: () => { },
7
+ };
8
+ let activeLogger = nullLogger;
9
+ /** Set the active logger for the state package. */
10
+ export function setStateLogger(l) {
11
+ activeLogger = l;
12
+ }
13
+ /** Get the active logger. */
14
+ export function getLogger() {
15
+ return activeLogger;
16
+ }
17
+ //# sourceMappingURL=logger.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logger.js","sourceRoot":"","sources":["../src/logger.ts"],"names":[],"mappings":"AAQA,+BAA+B;AAC/B,MAAM,CAAC,MAAM,UAAU,GAAgB;IACrC,KAAK,EAAE,GAAG,EAAE,GAAE,CAAC;IACf,IAAI,EAAE,GAAG,EAAE,GAAE,CAAC;IACd,IAAI,EAAE,GAAG,EAAE,GAAE,CAAC;IACd,KAAK,EAAE,GAAG,EAAE,GAAE,CAAC;CAChB,CAAC;AAEF,IAAI,YAAY,GAAgB,UAAU,CAAC;AAE3C,mDAAmD;AACnD,MAAM,UAAU,cAAc,CAAC,CAAc;IAC3C,YAAY,GAAG,CAAC,CAAC;AACnB,CAAC;AAED,6BAA6B;AAC7B,MAAM,UAAU,SAAS;IACvB,OAAO,YAAY,CAAC;AACtB,CAAC"}
@@ -0,0 +1,10 @@
1
+ import type { Manifest } from "./schemas.js";
2
+ export interface BuildManifestOptions {
3
+ repoRoot: string;
4
+ generator?: string;
5
+ }
6
+ export declare function buildManifest(opts: BuildManifestOptions): Manifest;
7
+ export declare function writeManifest(opts: BuildManifestOptions): {
8
+ manifest: Manifest;
9
+ path: string;
10
+ };
@@ -0,0 +1,84 @@
1
+ import { createHash } from "node:crypto";
2
+ import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
+ import { dirname, resolve } from "node:path";
4
+ import { stringify as stringifyYaml } from "yaml";
5
+ import { getLogger } from "./logger.js";
6
+ import { readFrontmatter } from "./frontmatter.js";
7
+ import { groundDir, manifestPath } from "./paths.js";
8
+ import { walkCanonical } from "./walk.js";
9
+ const log = getLogger();
10
+ export function buildManifest(opts) {
11
+ const { repoRoot } = opts;
12
+ const files = walkCanonical(repoRoot);
13
+ const entries = files.map((rel) => makeEntry(repoRoot, rel));
14
+ return {
15
+ version: 1,
16
+ generated: new Date().toISOString(),
17
+ ...(opts.generator !== undefined ? { generator: opts.generator } : {}),
18
+ files: entries,
19
+ };
20
+ }
21
+ export function writeManifest(opts) {
22
+ const manifest = buildManifest(opts);
23
+ const path = manifestPath(opts.repoRoot);
24
+ mkdirSync(dirname(path), { recursive: true });
25
+ mkdirSync(groundDir(opts.repoRoot), { recursive: true });
26
+ writeFileSync(path, stringifyYaml(manifest), "utf8");
27
+ log.debug({ path, count: manifest.files.length }, "wrote manifest");
28
+ return { manifest, path };
29
+ }
30
+ function makeEntry(repoRoot, rel) {
31
+ const abs = resolve(repoRoot, rel);
32
+ const buf = readFileSync(abs);
33
+ const sha256 = createHash("sha256").update(buf).digest("hex");
34
+ const classification = classify(rel);
35
+ // For markdown, lift audience + verified_at + generator from frontmatter.
36
+ let audience;
37
+ let verifiedAt;
38
+ let generator;
39
+ let source;
40
+ if (rel.endsWith(".md")) {
41
+ const fm = readFrontmatter(abs).frontmatter;
42
+ if (fm) {
43
+ audience = fm.audience;
44
+ verifiedAt = fm["verified-at"];
45
+ const generatorValue = fm["generator"];
46
+ generator = typeof generatorValue === "string" ? generatorValue : undefined;
47
+ const sourceValue = fm["source"];
48
+ source = typeof sourceValue === "string" ? sourceValue : undefined;
49
+ }
50
+ }
51
+ return {
52
+ path: rel,
53
+ sha256,
54
+ classification,
55
+ ...(audience !== undefined ? { audience } : {}),
56
+ ...(verifiedAt !== undefined ? { verified_at: verifiedAt } : {}),
57
+ ...(generator !== undefined ? { generator } : {}),
58
+ ...(source !== undefined ? { source } : {}),
59
+ };
60
+ }
61
+ function classify(rel) {
62
+ if (rel === "AGENTS.md" || rel === "CLAUDE.md")
63
+ return "orientation";
64
+ if (rel.startsWith(".claude/rules/"))
65
+ return "rule";
66
+ if (rel.startsWith(".claude/agents/"))
67
+ return "agent-def";
68
+ if (rel.startsWith(".claude/skills/"))
69
+ return "skill";
70
+ if (rel.startsWith(".cairn/ground/decisions/"))
71
+ return "decision";
72
+ if (rel.startsWith(".cairn/ground/invariants/"))
73
+ return "invariant";
74
+ if (rel.startsWith(".cairn/config/"))
75
+ return "cairn-config";
76
+ if (rel.startsWith(".cairn/ground/"))
77
+ return "ground";
78
+ if (rel.startsWith(".cairn/tasks/"))
79
+ return "task";
80
+ if (rel.startsWith("docs/"))
81
+ return "doc";
82
+ return "other";
83
+ }
84
+ //# sourceMappingURL=manifest.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"manifest.js","sourceRoot":"","sources":["../src/manifest.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Mission-system I/O — schema-validated reads + writes for the
3
+ * mission surfaces: `.cairn/ground/missions/<id>/roadmap.md`
4
+ * (committed) and `.cairn/missions/<id>/{state.json,spec.md,journal.jsonl}`
5
+ * (per-clone).
6
+ *
7
+ * Pure low-level I/O; no MCP, no hooks, no LLM calls. The MCP layer in
8
+ * `cairn-core` composes these primitives into the operator-facing
9
+ * `cairn_mission_*` tools.
10
+ */
11
+ import { type MissionExitGate, type MissionJournalEntry, type MissionPhase, type MissionPhaseProgressEntry, MissionPhaseBrief, MissionRoadmapFrontmatter, MissionState } from "./schemas.js";
12
+ export interface ParsedRoadmap {
13
+ frontmatter: MissionRoadmapFrontmatter;
14
+ prose: string;
15
+ }
16
+ /**
17
+ * Parse a roadmap.md source string into validated frontmatter + prose
18
+ * body. Throws when frontmatter is missing or fails schema validation —
19
+ * roadmap.md is the contract; an unreadable roadmap is a hard error,
20
+ * not a soft fallback.
21
+ */
22
+ export declare function parseRoadmap(source: string): ParsedRoadmap;
23
+ export declare function serializeRoadmap(frontmatter: MissionRoadmapFrontmatter, prose?: string): string;
24
+ export declare function readRoadmap(repoRoot: string, missionId: string): ParsedRoadmap | null;
25
+ export declare function writeRoadmap(repoRoot: string, missionId: string, frontmatter: MissionRoadmapFrontmatter, prose?: string): void;
26
+ export interface ParsedPhaseBrief {
27
+ frontmatter: MissionPhaseBrief;
28
+ prose: string;
29
+ }
30
+ export declare function parsePhaseBrief(source: string): ParsedPhaseBrief;
31
+ /**
32
+ * Render a human-readable brief: YAML frontmatter (the canonical
33
+ * machine surface) plus a mirrored markdown body so the file reads
34
+ * cleanly in a diff or editor. The body is derived from the frontmatter
35
+ * on every write — it is never the source of truth.
36
+ */
37
+ export declare function serializePhaseBrief(brief: MissionPhaseBrief, prose?: string): string;
38
+ export declare function readPhaseBrief(repoRoot: string, missionId: string, phaseId: string): MissionPhaseBrief | null;
39
+ export declare function writePhaseBrief(repoRoot: string, missionId: string, brief: MissionPhaseBrief, prose?: string): void;
40
+ export declare function readMissionState(repoRoot: string, missionId: string): MissionState | null;
41
+ export declare function writeMissionState(repoRoot: string, missionId: string, state: MissionState): void;
42
+ export declare function readMissionSpec(repoRoot: string, missionId: string): string | null;
43
+ export declare function writeMissionSpec(repoRoot: string, missionId: string, source: string): void;
44
+ export declare function appendMissionJournal(repoRoot: string, missionId: string, entry: MissionJournalEntry): void;
45
+ export declare function readMissionJournal(repoRoot: string, missionId: string): MissionJournalEntry[];
46
+ /** Mission ids present in `.cairn/ground/missions/` (excluding `_done`). */
47
+ export declare function listActiveMissionIds(repoRoot: string): string[];
48
+ /** Mission ids present in `.cairn/ground/missions/_done/`. */
49
+ export declare function listDoneMissionIds(repoRoot: string): string[];
50
+ /**
51
+ * Find the single active mission id for this clone, if any. v1 enforces
52
+ * one active mission per repo. When multiple mission dirs exist,
53
+ * returns the one whose state.json reports `outcome: active` and a
54
+ * non-null `cursor.active_phase`. Falls back to the lexically first
55
+ * active id when ambiguous; mismatch is a state corruption operators
56
+ * surface via `cairn doctor`.
57
+ */
58
+ export declare function findActiveMission(repoRoot: string): string | null;
59
+ /**
60
+ * Resolve the effective exit gate for a phase: per-phase override on the
61
+ * roadmap entry takes precedence, falls through to the mission-level
62
+ * `frontmatter.exit_gate`.
63
+ */
64
+ export declare function effectivePhaseExitGate(roadmap: MissionRoadmapFrontmatter, phaseId: string): MissionExitGate | null;
65
+ /**
66
+ * Compute the next pending phase whose `depends_on` set is fully
67
+ * satisfied (all listed phases sit at `state: done` in
68
+ * `phase_progress`). Returns null when no eligible phase exists —
69
+ * either every phase is done, or all remaining are blocked by
70
+ * unresolved dependencies (which is a soft conflict surfaced as
71
+ * mission_drift).
72
+ *
73
+ * Walks roadmap order; the first eligible pending phase wins. Operators
74
+ * can re-order phases by hand-editing roadmap.md.
75
+ */
76
+ export declare function nextPendingPhase(roadmap: MissionRoadmapFrontmatter, state: MissionState): MissionPhase | null;
77
+ /**
78
+ * Number of phases at `state: done`. Used by statusline `(N/M)` and the
79
+ * SessionStart cursor banner.
80
+ */
81
+ export declare function countDonePhases(state: MissionState): number;
82
+ /**
83
+ * Initial empty `phase_progress` map for a fresh mission — every
84
+ * roadmap phase id present at `state: pending` with no task_ids.
85
+ */
86
+ export declare function initialPhaseProgress(roadmap: MissionRoadmapFrontmatter): Record<string, MissionPhaseProgressEntry>;
87
+ /**
88
+ * Move a closed mission's ground roadmap dir + per-clone runtime dir
89
+ * under their respective `_done/` archives. Idempotent on already-
90
+ * archived missions (returns false).
91
+ */
92
+ export declare function archiveMission(repoRoot: string, missionId: string): boolean;
93
+ /**
94
+ * Reverse of `archiveMission` — move the mission's archived dirs back
95
+ * into the live locations. Returns false if neither archive is
96
+ * present.
97
+ */
98
+ export declare function restoreMission(repoRoot: string, missionId: string): boolean;
99
+ /**
100
+ * Locate a mission id whether it's live or archived. Returns the
101
+ * matching scope so callers can decide to read from `_done` paths.
102
+ */
103
+ export declare function locateMission(repoRoot: string, missionId: string): "active" | "done" | null;
104
+ /**
105
+ * Re-parse the live roadmap and return phase ids the mission's current
106
+ * `phase_progress` references that no longer appear in roadmap.md.
107
+ * These are the `mission_drift` candidates — phases the operator
108
+ * deleted from the roadmap mid-mission. Empty array on no drift.
109
+ */
110
+ export declare function detectRoadmapDrift(roadmap: MissionRoadmapFrontmatter, state: MissionState): string[];
111
+ /**
112
+ * Slice the per-clone spec.md by phase heading. Returns the body text
113
+ * under the `## <phase title>` heading or `## <phase id>` heading,
114
+ * stopping at the next `##` heading. Returns null if no slice matches.
115
+ *
116
+ * Used by cairn_mission_resume to prime fresh chats with only the
117
+ * relevant phase section instead of the whole spec doc.
118
+ */
119
+ export declare function slicePhaseSection(spec: string, phase: MissionPhase): string | null;
@@ -0,0 +1,414 @@
1
+ /**
2
+ * Mission-system I/O — schema-validated reads + writes for the
3
+ * mission surfaces: `.cairn/ground/missions/<id>/roadmap.md`
4
+ * (committed) and `.cairn/missions/<id>/{state.json,spec.md,journal.jsonl}`
5
+ * (per-clone).
6
+ *
7
+ * Pure low-level I/O; no MCP, no hooks, no LLM calls. The MCP layer in
8
+ * `cairn-core` composes these primitives into the operator-facing
9
+ * `cairn_mission_*` tools.
10
+ */
11
+ import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, writeFileSync, } from "node:fs";
12
+ import { dirname } from "node:path";
13
+ import { stringify as stringifyYaml } from "yaml";
14
+ import { parseFrontmatterRecord } from "./frontmatter.js";
15
+ import { missionBriefPath, missionGroundDir, missionRoadmapPath, missionRuntimeDir, missionSpecPath, missionStatePath, missionJournalPath, missionsGroundDoneRoot, missionsGroundRoot, missionsRuntimeDoneRoot, missionsRuntimeRoot, } from "./paths.js";
16
+ import { MissionPhaseBrief, MissionRoadmapFrontmatter, MissionState, } from "./schemas.js";
17
+ /* -------------------------------------------------------------------------- */
18
+ /* Roadmap (committed `.cairn/ground/missions/<id>/roadmap.md`) */
19
+ /* -------------------------------------------------------------------------- */
20
+ /**
21
+ * Parse a roadmap.md source string into validated frontmatter + prose
22
+ * body. Throws when frontmatter is missing or fails schema validation —
23
+ * roadmap.md is the contract; an unreadable roadmap is a hard error,
24
+ * not a soft fallback.
25
+ */
26
+ export function parseRoadmap(source) {
27
+ const { fm, body } = parseFrontmatterRecord(source);
28
+ if (Object.keys(fm).length === 0) {
29
+ throw new Error("roadmap.md is missing frontmatter");
30
+ }
31
+ const result = MissionRoadmapFrontmatter.safeParse(fm);
32
+ if (!result.success) {
33
+ throw new Error(`roadmap.md frontmatter invalid: ${result.error.message}`);
34
+ }
35
+ return { frontmatter: result.data, prose: body };
36
+ }
37
+ export function serializeRoadmap(frontmatter, prose = "") {
38
+ const yaml = stringifyYaml(frontmatter);
39
+ const trimmedProse = prose.replace(/^\n+/, "");
40
+ return `---\n${yaml}---\n\n${trimmedProse}`;
41
+ }
42
+ export function readRoadmap(repoRoot, missionId) {
43
+ const path = missionRoadmapPath(repoRoot, missionId);
44
+ if (!existsSync(path))
45
+ return null;
46
+ const source = readFileSync(path, "utf8");
47
+ return parseRoadmap(source);
48
+ }
49
+ export function writeRoadmap(repoRoot, missionId, frontmatter, prose = "") {
50
+ const path = missionRoadmapPath(repoRoot, missionId);
51
+ mkdirSync(dirname(path), { recursive: true });
52
+ writeFileSync(path, serializeRoadmap(frontmatter, prose), "utf8");
53
+ }
54
+ export function parsePhaseBrief(source) {
55
+ const { fm, body } = parseFrontmatterRecord(source);
56
+ if (Object.keys(fm).length === 0) {
57
+ throw new Error("phase brief is missing frontmatter");
58
+ }
59
+ const result = MissionPhaseBrief.safeParse(fm);
60
+ if (!result.success) {
61
+ throw new Error(`phase brief frontmatter invalid: ${result.error.message}`);
62
+ }
63
+ return { frontmatter: result.data, prose: body };
64
+ }
65
+ /**
66
+ * Render a human-readable brief: YAML frontmatter (the canonical
67
+ * machine surface) plus a mirrored markdown body so the file reads
68
+ * cleanly in a diff or editor. The body is derived from the frontmatter
69
+ * on every write — it is never the source of truth.
70
+ */
71
+ export function serializePhaseBrief(brief, prose = "") {
72
+ const yaml = stringifyYaml(brief);
73
+ const sections = [`# Phase brief — ${brief.phase_id}`, ""];
74
+ if (brief.decisions.length > 0) {
75
+ sections.push("## Decisions", "");
76
+ for (const d of brief.decisions) {
77
+ sections.push(`- **${d.question}** → ${d.choice}${d.rationale ? ` _(${d.rationale})_` : ""}`);
78
+ }
79
+ sections.push("");
80
+ }
81
+ if (brief.constraints.length > 0) {
82
+ sections.push("## Constraints", "");
83
+ for (const c of brief.constraints)
84
+ sections.push(`- ${c}`);
85
+ sections.push("");
86
+ }
87
+ if (brief.acceptance.length > 0) {
88
+ sections.push("## Acceptance", "");
89
+ for (const a of brief.acceptance)
90
+ sections.push(`- ${a}`);
91
+ sections.push("");
92
+ }
93
+ const cites = [...brief.cite_decisions, ...brief.cite_invariants];
94
+ if (cites.length > 0) {
95
+ sections.push("## In-scope ground state", "");
96
+ sections.push(cites.map((c) => `\`${c}\``).join(" · "));
97
+ sections.push("");
98
+ }
99
+ const extraProse = prose.replace(/^\n+/, "").trimEnd();
100
+ const body = extraProse.length > 0
101
+ ? `${sections.join("\n")}\n${extraProse}\n`
102
+ : `${sections.join("\n")}\n`;
103
+ return `---\n${yaml}---\n\n${body}`;
104
+ }
105
+ export function readPhaseBrief(repoRoot, missionId, phaseId) {
106
+ const path = missionBriefPath(repoRoot, missionId, phaseId);
107
+ if (!existsSync(path))
108
+ return null;
109
+ try {
110
+ return parsePhaseBrief(readFileSync(path, "utf8")).frontmatter;
111
+ }
112
+ catch {
113
+ return null;
114
+ }
115
+ }
116
+ export function writePhaseBrief(repoRoot, missionId, brief, prose = "") {
117
+ const path = missionBriefPath(repoRoot, missionId, brief.phase_id);
118
+ mkdirSync(dirname(path), { recursive: true });
119
+ writeFileSync(path, serializePhaseBrief(brief, prose), "utf8");
120
+ }
121
+ /* -------------------------------------------------------------------------- */
122
+ /* Mission state (per-clone `.cairn/missions/<id>/state.json`) */
123
+ /* -------------------------------------------------------------------------- */
124
+ export function readMissionState(repoRoot, missionId) {
125
+ const path = missionStatePath(repoRoot, missionId);
126
+ if (!existsSync(path))
127
+ return null;
128
+ let raw;
129
+ try {
130
+ raw = readFileSync(path, "utf8");
131
+ }
132
+ catch {
133
+ return null;
134
+ }
135
+ let parsed;
136
+ try {
137
+ parsed = JSON.parse(raw);
138
+ }
139
+ catch {
140
+ return null;
141
+ }
142
+ const result = MissionState.safeParse(parsed);
143
+ return result.success ? result.data : null;
144
+ }
145
+ export function writeMissionState(repoRoot, missionId, state) {
146
+ const path = missionStatePath(repoRoot, missionId);
147
+ mkdirSync(dirname(path), { recursive: true });
148
+ writeFileSync(path, JSON.stringify(state, null, 2) + "\n", "utf8");
149
+ }
150
+ /* -------------------------------------------------------------------------- */
151
+ /* Spec snapshot (per-clone `.cairn/missions/<id>/spec.md`) */
152
+ /* -------------------------------------------------------------------------- */
153
+ export function readMissionSpec(repoRoot, missionId) {
154
+ const path = missionSpecPath(repoRoot, missionId);
155
+ if (!existsSync(path))
156
+ return null;
157
+ try {
158
+ return readFileSync(path, "utf8");
159
+ }
160
+ catch {
161
+ return null;
162
+ }
163
+ }
164
+ export function writeMissionSpec(repoRoot, missionId, source) {
165
+ const path = missionSpecPath(repoRoot, missionId);
166
+ mkdirSync(dirname(path), { recursive: true });
167
+ writeFileSync(path, source, "utf8");
168
+ }
169
+ /* -------------------------------------------------------------------------- */
170
+ /* Mission journal (per-clone `.cairn/missions/<id>/journal.jsonl`) */
171
+ /* -------------------------------------------------------------------------- */
172
+ export function appendMissionJournal(repoRoot, missionId, entry) {
173
+ const path = missionJournalPath(repoRoot, missionId);
174
+ mkdirSync(dirname(path), { recursive: true });
175
+ appendFileSync(path, JSON.stringify(entry) + "\n", "utf8");
176
+ }
177
+ export function readMissionJournal(repoRoot, missionId) {
178
+ const path = missionJournalPath(repoRoot, missionId);
179
+ if (!existsSync(path))
180
+ return [];
181
+ let raw;
182
+ try {
183
+ raw = readFileSync(path, "utf8");
184
+ }
185
+ catch {
186
+ return [];
187
+ }
188
+ const out = [];
189
+ for (const line of raw.split(/\r?\n/)) {
190
+ if (line.trim().length === 0)
191
+ continue;
192
+ try {
193
+ const parsed = JSON.parse(line);
194
+ if (typeof parsed === "object" && parsed !== null && typeof parsed.ts === "string") {
195
+ out.push(parsed);
196
+ }
197
+ }
198
+ catch {
199
+ // skip malformed lines
200
+ }
201
+ }
202
+ return out;
203
+ }
204
+ /* -------------------------------------------------------------------------- */
205
+ /* Discovery + cursor helpers */
206
+ /* -------------------------------------------------------------------------- */
207
+ function listMissionsIn(rootDir) {
208
+ if (!existsSync(rootDir))
209
+ return [];
210
+ let entries;
211
+ try {
212
+ entries = readdirSync(rootDir, { withFileTypes: true, encoding: "utf8" });
213
+ }
214
+ catch {
215
+ return [];
216
+ }
217
+ return entries
218
+ .filter((e) => e.isDirectory() && e.name.startsWith("MIS-"))
219
+ .map((e) => e.name);
220
+ }
221
+ /** Mission ids present in `.cairn/ground/missions/` (excluding `_done`). */
222
+ export function listActiveMissionIds(repoRoot) {
223
+ return listMissionsIn(missionsGroundRoot(repoRoot));
224
+ }
225
+ /** Mission ids present in `.cairn/ground/missions/_done/`. */
226
+ export function listDoneMissionIds(repoRoot) {
227
+ return listMissionsIn(missionsGroundDoneRoot(repoRoot));
228
+ }
229
+ /**
230
+ * Find the single active mission id for this clone, if any. v1 enforces
231
+ * one active mission per repo. When multiple mission dirs exist,
232
+ * returns the one whose state.json reports `outcome: active` and a
233
+ * non-null `cursor.active_phase`. Falls back to the lexically first
234
+ * active id when ambiguous; mismatch is a state corruption operators
235
+ * surface via `cairn doctor`.
236
+ */
237
+ export function findActiveMission(repoRoot) {
238
+ const ids = listActiveMissionIds(repoRoot);
239
+ for (const id of ids) {
240
+ const state = readMissionState(repoRoot, id);
241
+ if (state === null)
242
+ continue;
243
+ if (state.outcome === "active")
244
+ return id;
245
+ }
246
+ return null;
247
+ }
248
+ /**
249
+ * Resolve the effective exit gate for a phase: per-phase override on the
250
+ * roadmap entry takes precedence, falls through to the mission-level
251
+ * `frontmatter.exit_gate`.
252
+ */
253
+ export function effectivePhaseExitGate(roadmap, phaseId) {
254
+ const phase = roadmap.phases.find((p) => p.id === phaseId);
255
+ if (phase === undefined)
256
+ return null;
257
+ return phase.exit_gate ?? roadmap.exit_gate;
258
+ }
259
+ /**
260
+ * Compute the next pending phase whose `depends_on` set is fully
261
+ * satisfied (all listed phases sit at `state: done` in
262
+ * `phase_progress`). Returns null when no eligible phase exists —
263
+ * either every phase is done, or all remaining are blocked by
264
+ * unresolved dependencies (which is a soft conflict surfaced as
265
+ * mission_drift).
266
+ *
267
+ * Walks roadmap order; the first eligible pending phase wins. Operators
268
+ * can re-order phases by hand-editing roadmap.md.
269
+ */
270
+ export function nextPendingPhase(roadmap, state) {
271
+ for (const phase of roadmap.phases) {
272
+ const progress = state.phase_progress[phase.id];
273
+ const phaseState = progress?.state ?? "pending";
274
+ if (phaseState !== "pending")
275
+ continue;
276
+ const depsSatisfied = phase.depends_on.every((dep) => {
277
+ const depProgress = state.phase_progress[dep];
278
+ return depProgress?.state === "done";
279
+ });
280
+ if (depsSatisfied)
281
+ return phase;
282
+ }
283
+ return null;
284
+ }
285
+ /**
286
+ * Number of phases at `state: done`. Used by statusline `(N/M)` and the
287
+ * SessionStart cursor banner.
288
+ */
289
+ export function countDonePhases(state) {
290
+ let n = 0;
291
+ for (const entry of Object.values(state.phase_progress)) {
292
+ if (entry.state === "done")
293
+ n += 1;
294
+ }
295
+ return n;
296
+ }
297
+ /**
298
+ * Initial empty `phase_progress` map for a fresh mission — every
299
+ * roadmap phase id present at `state: pending` with no task_ids.
300
+ */
301
+ export function initialPhaseProgress(roadmap) {
302
+ const out = {};
303
+ for (const phase of roadmap.phases) {
304
+ out[phase.id] = { state: "pending", task_ids: [] };
305
+ }
306
+ return out;
307
+ }
308
+ /* -------------------------------------------------------------------------- */
309
+ /* Archive / restore */
310
+ /* -------------------------------------------------------------------------- */
311
+ /**
312
+ * Move a closed mission's ground roadmap dir + per-clone runtime dir
313
+ * under their respective `_done/` archives. Idempotent on already-
314
+ * archived missions (returns false).
315
+ */
316
+ export function archiveMission(repoRoot, missionId) {
317
+ const groundFrom = missionGroundDir(repoRoot, missionId);
318
+ const runtimeFrom = missionRuntimeDir(repoRoot, missionId);
319
+ let moved = false;
320
+ if (existsSync(groundFrom)) {
321
+ const dest = `${missionsGroundDoneRoot(repoRoot)}/${missionId}`;
322
+ mkdirSync(missionsGroundDoneRoot(repoRoot), { recursive: true });
323
+ renameSync(groundFrom, dest);
324
+ moved = true;
325
+ }
326
+ if (existsSync(runtimeFrom)) {
327
+ const dest = `${missionsRuntimeDoneRoot(repoRoot)}/${missionId}`;
328
+ mkdirSync(missionsRuntimeDoneRoot(repoRoot), { recursive: true });
329
+ renameSync(runtimeFrom, dest);
330
+ moved = true;
331
+ }
332
+ return moved;
333
+ }
334
+ /**
335
+ * Reverse of `archiveMission` — move the mission's archived dirs back
336
+ * into the live locations. Returns false if neither archive is
337
+ * present.
338
+ */
339
+ export function restoreMission(repoRoot, missionId) {
340
+ const groundFrom = `${missionsGroundDoneRoot(repoRoot)}/${missionId}`;
341
+ const runtimeFrom = `${missionsRuntimeDoneRoot(repoRoot)}/${missionId}`;
342
+ let moved = false;
343
+ if (existsSync(groundFrom)) {
344
+ mkdirSync(missionsGroundRoot(repoRoot), { recursive: true });
345
+ renameSync(groundFrom, missionGroundDir(repoRoot, missionId));
346
+ moved = true;
347
+ }
348
+ if (existsSync(runtimeFrom)) {
349
+ mkdirSync(missionsRuntimeRoot(repoRoot), { recursive: true });
350
+ renameSync(runtimeFrom, missionRuntimeDir(repoRoot, missionId));
351
+ moved = true;
352
+ }
353
+ return moved;
354
+ }
355
+ /**
356
+ * Locate a mission id whether it's live or archived. Returns the
357
+ * matching scope so callers can decide to read from `_done` paths.
358
+ */
359
+ export function locateMission(repoRoot, missionId) {
360
+ if (existsSync(missionGroundDir(repoRoot, missionId)))
361
+ return "active";
362
+ if (existsSync(`${missionsGroundDoneRoot(repoRoot)}/${missionId}`))
363
+ return "done";
364
+ return null;
365
+ }
366
+ /* -------------------------------------------------------------------------- */
367
+ /* Validation helpers */
368
+ /* -------------------------------------------------------------------------- */
369
+ /**
370
+ * Re-parse the live roadmap and return phase ids the mission's current
371
+ * `phase_progress` references that no longer appear in roadmap.md.
372
+ * These are the `mission_drift` candidates — phases the operator
373
+ * deleted from the roadmap mid-mission. Empty array on no drift.
374
+ */
375
+ export function detectRoadmapDrift(roadmap, state) {
376
+ const liveIds = new Set(roadmap.phases.map((p) => p.id));
377
+ return Object.keys(state.phase_progress).filter((id) => !liveIds.has(id));
378
+ }
379
+ /**
380
+ * Slice the per-clone spec.md by phase heading. Returns the body text
381
+ * under the `## <phase title>` heading or `## <phase id>` heading,
382
+ * stopping at the next `##` heading. Returns null if no slice matches.
383
+ *
384
+ * Used by cairn_mission_resume to prime fresh chats with only the
385
+ * relevant phase section instead of the whole spec doc.
386
+ */
387
+ export function slicePhaseSection(spec, phase) {
388
+ const lines = spec.split(/\r?\n/);
389
+ const matchTitles = [phase.title, phase.id]
390
+ .filter((s) => s.length > 0)
391
+ .map((s) => s.toLowerCase());
392
+ let start = -1;
393
+ for (let i = 0; i < lines.length; i++) {
394
+ const m = lines[i]?.match(/^##\s+(.+?)\s*$/);
395
+ if (m === null || m === undefined)
396
+ continue;
397
+ const heading = (m[1] ?? "").toLowerCase();
398
+ if (matchTitles.some((t) => heading.includes(t))) {
399
+ start = i;
400
+ break;
401
+ }
402
+ }
403
+ if (start === -1)
404
+ return null;
405
+ let end = lines.length;
406
+ for (let i = start + 1; i < lines.length; i++) {
407
+ if (lines[i]?.match(/^##\s+/)) {
408
+ end = i;
409
+ break;
410
+ }
411
+ }
412
+ return lines.slice(start, end).join("\n").trim();
413
+ }
414
+ //# sourceMappingURL=missions.js.map