@isaacriehm/cairn-core 0.9.8 → 0.10.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/dist/.tsbuildinfo +1 -1
- package/dist/hooks/runners/stop.js +105 -0
- package/dist/hooks/runners/stop.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/mcp/errors.d.ts +1 -1
- package/dist/mcp/errors.js.map +1 -1
- package/dist/mcp/schemas.d.ts +101 -0
- package/dist/mcp/schemas.js +92 -0
- package/dist/mcp/schemas.js.map +1 -1
- package/dist/mcp/tools/index.js +19 -0
- package/dist/mcp/tools/index.js.map +1 -1
- package/dist/mcp/tools/mission-accept-draft.d.ts +16 -0
- package/dist/mcp/tools/mission-accept-draft.js +102 -0
- package/dist/mcp/tools/mission-accept-draft.js.map +1 -0
- package/dist/mcp/tools/mission-advance.d.ts +8 -0
- package/dist/mcp/tools/mission-advance.js +124 -0
- package/dist/mcp/tools/mission-advance.js.map +1 -0
- package/dist/mcp/tools/mission-close.d.ts +8 -0
- package/dist/mcp/tools/mission-close.js +48 -0
- package/dist/mcp/tools/mission-close.js.map +1 -0
- package/dist/mcp/tools/mission-get.d.ts +6 -0
- package/dist/mcp/tools/mission-get.js +64 -0
- package/dist/mcp/tools/mission-get.js.map +1 -0
- package/dist/mcp/tools/mission-reopen.d.ts +13 -0
- package/dist/mcp/tools/mission-reopen.js +56 -0
- package/dist/mcp/tools/mission-reopen.js.map +1 -0
- package/dist/mcp/tools/mission-resume.d.ts +15 -0
- package/dist/mcp/tools/mission-resume.js +219 -0
- package/dist/mcp/tools/mission-resume.js.map +1 -0
- package/dist/mcp/tools/mission-resync-accept.d.ts +19 -0
- package/dist/mcp/tools/mission-resync-accept.js +192 -0
- package/dist/mcp/tools/mission-resync-accept.js.map +1 -0
- package/dist/mcp/tools/mission-resync.d.ts +20 -0
- package/dist/mcp/tools/mission-resync.js +113 -0
- package/dist/mcp/tools/mission-resync.js.map +1 -0
- package/dist/mcp/tools/mission-start.d.ts +8 -0
- package/dist/mcp/tools/mission-start.js +76 -0
- package/dist/mcp/tools/mission-start.js.map +1 -0
- package/dist/mcp/tools/task-create.d.ts +2 -0
- package/dist/mcp/tools/task-create.js +40 -2
- package/dist/mcp/tools/task-create.js.map +1 -1
- package/dist/missions/cli.d.ts +106 -0
- package/dist/missions/cli.js +279 -0
- package/dist/missions/cli.js.map +1 -0
- package/dist/missions/cursor.d.ts +40 -0
- package/dist/missions/cursor.js +99 -0
- package/dist/missions/cursor.js.map +1 -0
- package/dist/missions/draft.d.ts +28 -0
- package/dist/missions/draft.js +112 -0
- package/dist/missions/draft.js.map +1 -0
- package/dist/missions/index.d.ts +9 -0
- package/dist/missions/index.js +10 -0
- package/dist/missions/index.js.map +1 -0
- package/dist/missions/task-link.d.ts +51 -0
- package/dist/missions/task-link.js +155 -0
- package/dist/missions/task-link.js.map +1 -0
- package/dist/session-start/build.d.ts +1 -1
- package/dist/session-start/build.js +58 -1
- package/dist/session-start/build.js.map +1 -1
- package/dist/status-line/format.d.ts +15 -1
- package/dist/status-line/format.js +12 -1
- package/dist/status-line/format.js.map +1 -1
- package/dist/status-line/index.d.ts +1 -1
- package/dist/status-line/index.js +1 -1
- package/dist/status-line/index.js.map +1 -1
- package/dist/status-line/reader.js +33 -2
- package/dist/status-line/reader.js.map +1 -1
- package/dist/tasks/lifecycle.js +11 -0
- package/dist/tasks/lifecycle.js.map +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mission cursor advance helpers — shared logic between the explicit
|
|
3
|
+
* `cairn_mission_advance` MCP tool, the Stop hook auto-advance path
|
|
4
|
+
* (exit_gate=auto), and the Stop hook prompt path (exit_gate=prompt
|
|
5
|
+
* after operator picks `a`).
|
|
6
|
+
*/
|
|
7
|
+
import { appendMissionJournal, archiveMission, countDonePhases, nextPendingPhase, readMissionState, readRoadmap, writeMissionState, } from "@isaacriehm/cairn-state";
|
|
8
|
+
/**
|
|
9
|
+
* Mark `phaseId` as done in `phase_progress` and pick the next pending
|
|
10
|
+
* phase whose `depends_on` set is satisfied. When no eligible phase
|
|
11
|
+
* remains, set `cursor.active_phase = null`, mark `outcome: done`, and
|
|
12
|
+
* archive the mission dirs.
|
|
13
|
+
*/
|
|
14
|
+
export function advancePhase(repoRoot, missionId, phaseId, now = new Date()) {
|
|
15
|
+
const roadmap = readRoadmap(repoRoot, missionId);
|
|
16
|
+
if (roadmap === null) {
|
|
17
|
+
return { ok: false, code: "ROADMAP_MISSING", message: `roadmap.md missing for ${missionId}` };
|
|
18
|
+
}
|
|
19
|
+
const state = readMissionState(repoRoot, missionId);
|
|
20
|
+
if (state === null) {
|
|
21
|
+
return { ok: false, code: "STATE_MISSING", message: `state.json missing for ${missionId}` };
|
|
22
|
+
}
|
|
23
|
+
const phaseDef = roadmap.frontmatter.phases.find((p) => p.id === phaseId);
|
|
24
|
+
if (phaseDef === undefined) {
|
|
25
|
+
return { ok: false, code: "PHASE_NOT_FOUND", message: `phase ${phaseId} not in roadmap` };
|
|
26
|
+
}
|
|
27
|
+
const progress = state.phase_progress[phaseId];
|
|
28
|
+
if (progress?.state === "done") {
|
|
29
|
+
return { ok: false, code: "PHASE_ALREADY_DONE", message: `phase ${phaseId} already done` };
|
|
30
|
+
}
|
|
31
|
+
const ts = now.toISOString();
|
|
32
|
+
state.phase_progress[phaseId] = {
|
|
33
|
+
state: "done",
|
|
34
|
+
task_ids: progress?.task_ids ?? [],
|
|
35
|
+
graduated_at: ts,
|
|
36
|
+
};
|
|
37
|
+
const next = nextPendingPhase(roadmap.frontmatter, state);
|
|
38
|
+
if (next !== null) {
|
|
39
|
+
state.cursor.active_phase = next.id;
|
|
40
|
+
state.cursor.active_phase_started_at = ts;
|
|
41
|
+
state.phase_progress[next.id] = {
|
|
42
|
+
state: "in_progress",
|
|
43
|
+
task_ids: state.phase_progress[next.id]?.task_ids ?? [],
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
state.cursor.active_phase = null;
|
|
48
|
+
state.cursor.active_phase_started_at = null;
|
|
49
|
+
state.outcome = "done";
|
|
50
|
+
state.closed_at = ts;
|
|
51
|
+
}
|
|
52
|
+
writeMissionState(repoRoot, missionId, state);
|
|
53
|
+
appendMissionJournal(repoRoot, missionId, {
|
|
54
|
+
ts,
|
|
55
|
+
kind: "phase-advanced",
|
|
56
|
+
phase_id: phaseId,
|
|
57
|
+
detail: next !== null ? `next: ${next.id}` : "mission complete",
|
|
58
|
+
});
|
|
59
|
+
let closed = false;
|
|
60
|
+
if (next === null) {
|
|
61
|
+
appendMissionJournal(repoRoot, missionId, {
|
|
62
|
+
ts,
|
|
63
|
+
kind: "closed",
|
|
64
|
+
detail: "auto-close on last phase complete",
|
|
65
|
+
});
|
|
66
|
+
archiveMission(repoRoot, missionId);
|
|
67
|
+
closed = true;
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
ok: true,
|
|
71
|
+
phase_advanced: phaseId,
|
|
72
|
+
next_phase: next,
|
|
73
|
+
closed,
|
|
74
|
+
donePhases: countDonePhases(state),
|
|
75
|
+
totalPhases: roadmap.frontmatter.phases.length,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Determine whether every linked task on a phase has graduated.
|
|
80
|
+
* Returns `false` when the phase has zero tasks (the operator has to
|
|
81
|
+
* `mission_advance --force` for empty phases — never auto-advance an
|
|
82
|
+
* empty phase silently).
|
|
83
|
+
*/
|
|
84
|
+
export function allPhaseTasksDone(state, phaseId, taskIsDone) {
|
|
85
|
+
const progress = state.phase_progress[phaseId];
|
|
86
|
+
if (progress === undefined)
|
|
87
|
+
return false;
|
|
88
|
+
if (progress.task_ids.length === 0)
|
|
89
|
+
return false;
|
|
90
|
+
return progress.task_ids.every(taskIsDone);
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Resolve a roadmap phase by id from a mission's roadmap. Returns null
|
|
94
|
+
* when the id was deleted in a mid-mission roadmap edit (drift case).
|
|
95
|
+
*/
|
|
96
|
+
export function lookupPhase(roadmap, phaseId) {
|
|
97
|
+
return roadmap.phases.find((p) => p.id === phaseId) ?? null;
|
|
98
|
+
}
|
|
99
|
+
//# sourceMappingURL=cursor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cursor.js","sourceRoot":"","sources":["../../src/missions/cursor.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAIL,oBAAoB,EACpB,cAAc,EACd,eAAe,EACf,gBAAgB,EAChB,gBAAgB,EAChB,WAAW,EACX,iBAAiB,GAClB,MAAM,yBAAyB,CAAC;AAuBjC;;;;;GAKG;AACH,MAAM,UAAU,YAAY,CAC1B,QAAgB,EAChB,SAAiB,EACjB,OAAe,EACf,MAAY,IAAI,IAAI,EAAE;IAEtB,MAAM,OAAO,GAAG,WAAW,CAAC,QAAQ,EAAE,SAAS,CAAC,CAAC;IACjD,IAAI,OAAO,KAAK,IAAI,EAAE,CAAC;QACrB,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,iBAAiB,EAAE,OAAO,EAAE,0BAA0B,SAAS,EAAE,EAAE,CAAC;IAChG,CAAC;IACD,MAAM,KAAK,GAAG,gBAAgB,CAAC,QAAQ,EAAE,SAAS,CAAC,CAAC;IACpD,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;QACnB,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,eAAe,EAAE,OAAO,EAAE,0BAA0B,SAAS,EAAE,EAAE,CAAC;IAC9F,CAAC;IACD,MAAM,QAAQ,GAAG,OAAO,CAAC,WAAW,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,OAAO,CAAC,CAAC;IAC1E,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;QAC3B,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,iBAAiB,EAAE,OAAO,EAAE,SAAS,OAAO,iBAAiB,EAAE,CAAC;IAC5F,CAAC;IACD,MAAM,QAAQ,GAAG,KAAK,CAAC,cAAc,CAAC,OAAO,CAAC,CAAC;IAC/C,IAAI,QAAQ,EAAE,KAAK,KAAK,MAAM,EAAE,CAAC;QAC/B,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,oBAAoB,EAAE,OAAO,EAAE,SAAS,OAAO,eAAe,EAAE,CAAC;IAC7F,CAAC;IAED,MAAM,EAAE,GAAG,GAAG,CAAC,WAAW,EAAE,CAAC;IAC7B,KAAK,CAAC,cAAc,CAAC,OAAO,CAAC,GAAG;QAC9B,KAAK,EAAE,MAAM;QACb,QAAQ,EAAE,QAAQ,EAAE,QAAQ,IAAI,EAAE;QAClC,YAAY,EAAE,EAAE;KACjB,CAAC;IAEF,MAAM,IAAI,GAAG,gBAAgB,CAAC,OAAO,CAAC,WAAW,EAAE,KAAK,CAAC,CAAC;IAC1D,IAAI,IAAI,KAAK,IAAI,EAAE,CAAC;QAClB,KAAK,CAAC,MAAM,CAAC,YAAY,GAAG,IAAI,CAAC,EAAE,CAAC;QACpC,KAAK,CAAC,MAAM,CAAC,uBAAuB,GAAG,EAAE,CAAC;QAC1C,KAAK,CAAC,cAAc,CAAC,IAAI,CAAC,EAAE,CAAC,GAAG;YAC9B,KAAK,EAAE,aAAa;YACpB,QAAQ,EAAE,KAAK,CAAC,cAAc,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,QAAQ,IAAI,EAAE;SACxD,CAAC;IACJ,CAAC;SAAM,CAAC;QACN,KAAK,CAAC,MAAM,CAAC,YAAY,GAAG,IAAI,CAAC;QACjC,KAAK,CAAC,MAAM,CAAC,uBAAuB,GAAG,IAAI,CAAC;QAC5C,KAAK,CAAC,OAAO,GAAG,MAAM,CAAC;QACvB,KAAK,CAAC,SAAS,GAAG,EAAE,CAAC;IACvB,CAAC;IAED,iBAAiB,CAAC,QAAQ,EAAE,SAAS,EAAE,KAAK,CAAC,CAAC;IAC9C,oBAAoB,CAAC,QAAQ,EAAE,SAAS,EAAE;QACxC,EAAE;QACF,IAAI,EAAE,gBAAgB;QACtB,QAAQ,EAAE,OAAO;QACjB,MAAM,EAAE,IAAI,KAAK,IAAI,CAAC,CAAC,CAAC,SAAS,IAAI,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,kBAAkB;KAChE,CAAC,CAAC;IAEH,IAAI,MAAM,GAAG,KAAK,CAAC;IACnB,IAAI,IAAI,KAAK,IAAI,EAAE,CAAC;QAClB,oBAAoB,CAAC,QAAQ,EAAE,SAAS,EAAE;YACxC,EAAE;YACF,IAAI,EAAE,QAAQ;YACd,MAAM,EAAE,mCAAmC;SAC5C,CAAC,CAAC;QACH,cAAc,CAAC,QAAQ,EAAE,SAAS,CAAC,CAAC;QACpC,MAAM,GAAG,IAAI,CAAC;IAChB,CAAC;IAED,OAAO;QACL,EAAE,EAAE,IAAI;QACR,cAAc,EAAE,OAAO;QACvB,UAAU,EAAE,IAAI;QAChB,MAAM;QACN,UAAU,EAAE,eAAe,CAAC,KAAK,CAAC;QAClC,WAAW,EAAE,OAAO,CAAC,WAAW,CAAC,MAAM,CAAC,MAAM;KAC/C,CAAC;AACJ,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,iBAAiB,CAC/B,KAAmB,EACnB,OAAe,EACf,UAAuC;IAEvC,MAAM,QAAQ,GAAG,KAAK,CAAC,cAAc,CAAC,OAAO,CAAC,CAAC;IAC/C,IAAI,QAAQ,KAAK,SAAS;QAAE,OAAO,KAAK,CAAC;IACzC,IAAI,QAAQ,CAAC,QAAQ,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IACjD,OAAO,QAAQ,CAAC,QAAQ,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC;AAC7C,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,WAAW,CACzB,OAAkC,EAClC,OAAe;IAEf,OAAO,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,OAAO,CAAC,IAAI,IAAI,CAAC;AAC9D,CAAC"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mission roadmap drafting — Haiku call that turns a planning doc into
|
|
3
|
+
* an ordered phase array. One LLM-cost touchpoint per mission
|
|
4
|
+
* lifecycle. All subsequent phase advance / linking is deterministic.
|
|
5
|
+
*
|
|
6
|
+
* The model receives ONLY the spec doc + a tight system prompt; no
|
|
7
|
+
* project ground state, no MCP tools, no ambient CLAUDE.md hierarchy.
|
|
8
|
+
* `isolateAmbientContext: true` enforces that.
|
|
9
|
+
*/
|
|
10
|
+
import type { MissionPhase } from "@isaacriehm/cairn-state";
|
|
11
|
+
export interface DraftRoadmapArgs {
|
|
12
|
+
repoRoot: string;
|
|
13
|
+
spec: string;
|
|
14
|
+
specPath: string;
|
|
15
|
+
}
|
|
16
|
+
export interface DraftRoadmapResult {
|
|
17
|
+
phases: MissionPhase[];
|
|
18
|
+
/** Char count of the spec slice actually fed to Haiku. */
|
|
19
|
+
spec_chars_used: number;
|
|
20
|
+
/** True when the spec was truncated to fit MAX_SPEC_CHARS. */
|
|
21
|
+
truncated: boolean;
|
|
22
|
+
}
|
|
23
|
+
export declare function draftRoadmapFromSpec(args: DraftRoadmapArgs): Promise<DraftRoadmapResult | null>;
|
|
24
|
+
/**
|
|
25
|
+
* Single-phase fallback used when `no_llm: true` or Haiku is offline.
|
|
26
|
+
* Lets the operator hand-edit roadmap.md before approving.
|
|
27
|
+
*/
|
|
28
|
+
export declare function stubRoadmap(): MissionPhase[];
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mission roadmap drafting — Haiku call that turns a planning doc into
|
|
3
|
+
* an ordered phase array. One LLM-cost touchpoint per mission
|
|
4
|
+
* lifecycle. All subsequent phase advance / linking is deterministic.
|
|
5
|
+
*
|
|
6
|
+
* The model receives ONLY the spec doc + a tight system prompt; no
|
|
7
|
+
* project ground state, no MCP tools, no ambient CLAUDE.md hierarchy.
|
|
8
|
+
* `isolateAmbientContext: true` enforces that.
|
|
9
|
+
*/
|
|
10
|
+
import { runClaude } from "../claude/index.js";
|
|
11
|
+
import { logger } from "../logger.js";
|
|
12
|
+
import { z } from "zod";
|
|
13
|
+
const log = logger("mission.draft");
|
|
14
|
+
const TIMEOUT_MS = 180_000;
|
|
15
|
+
const MAX_SPEC_CHARS = 60_000;
|
|
16
|
+
const MAX_PHASES = 24;
|
|
17
|
+
const SYSTEM_PROMPT = `You parse a software-project planning document into an ordered list of implementation phases.
|
|
18
|
+
|
|
19
|
+
Return STRICT JSON matching the schema. No prose, no markdown.
|
|
20
|
+
|
|
21
|
+
Phase rules:
|
|
22
|
+
- Each phase id is kebab-case, prefixed by an ordinal (e.g. "phase-1-schema", "phase-2-jwt"). Lowercase ASCII letters, digits, hyphens only.
|
|
23
|
+
- title is a tight noun-phrase summarizing the phase's deliverable (≤60 chars).
|
|
24
|
+
- depends_on lists prior phase ids that must complete before this phase starts. Empty array for the first phase.
|
|
25
|
+
- exit_criteria is one tight sentence describing the operator-verifiable end state of this phase. Cite concrete artifacts ("migration applied", "smoke green") not vague verbs ("works", "done").
|
|
26
|
+
- Maximum ${MAX_PHASES} phases. If the spec describes finer-grained sub-tasks, group them into phases.
|
|
27
|
+
- Linear or DAG, never cyclic. Every depends_on entry must be an earlier phase id.
|
|
28
|
+
|
|
29
|
+
Keep phase count tight — one phase per coherent deliverable, not one per file. The roadmap is the operator's mental map; pad it and you waste their attention.`;
|
|
30
|
+
const OUTPUT_SCHEMA = {
|
|
31
|
+
type: "object",
|
|
32
|
+
required: ["phases"],
|
|
33
|
+
properties: {
|
|
34
|
+
phases: {
|
|
35
|
+
type: "array",
|
|
36
|
+
minItems: 1,
|
|
37
|
+
maxItems: MAX_PHASES,
|
|
38
|
+
items: {
|
|
39
|
+
type: "object",
|
|
40
|
+
required: ["id", "title", "depends_on", "exit_criteria"],
|
|
41
|
+
properties: {
|
|
42
|
+
id: { type: "string", pattern: "^phase-[0-9]+-[a-z0-9-]+$" },
|
|
43
|
+
title: { type: "string", minLength: 1, maxLength: 80 },
|
|
44
|
+
depends_on: { type: "array", items: { type: "string" } },
|
|
45
|
+
exit_criteria: { type: "string", minLength: 1, maxLength: 200 },
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
const DraftResponseSchema = z.object({
|
|
52
|
+
phases: z.array(z.object({
|
|
53
|
+
id: z.string(),
|
|
54
|
+
title: z.string(),
|
|
55
|
+
depends_on: z.array(z.string()),
|
|
56
|
+
exit_criteria: z.string(),
|
|
57
|
+
})),
|
|
58
|
+
});
|
|
59
|
+
export async function draftRoadmapFromSpec(args) {
|
|
60
|
+
const truncated = args.spec.length > MAX_SPEC_CHARS;
|
|
61
|
+
const slice = truncated ? args.spec.slice(0, MAX_SPEC_CHARS) : args.spec;
|
|
62
|
+
const prompt = `Source planning document (${args.specPath}):\n\n${slice}\n\nReturn the phase array.`;
|
|
63
|
+
let result;
|
|
64
|
+
try {
|
|
65
|
+
result = await runClaude({
|
|
66
|
+
tier: "haiku",
|
|
67
|
+
prompt,
|
|
68
|
+
system: SYSTEM_PROMPT,
|
|
69
|
+
jsonSchema: OUTPUT_SCHEMA,
|
|
70
|
+
timeoutMs: TIMEOUT_MS,
|
|
71
|
+
repoRoot: args.repoRoot,
|
|
72
|
+
cacheable: false,
|
|
73
|
+
isolateAmbientContext: true,
|
|
74
|
+
purpose: "mission.draft",
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
catch (err) {
|
|
78
|
+
log.warn({ error: err instanceof Error ? err.message : String(err) }, "mission draft Haiku call failed");
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
const parsed = DraftResponseSchema.safeParse(result.parsed);
|
|
82
|
+
if (!parsed.success) {
|
|
83
|
+
log.warn({ error: parsed.error.message }, "mission draft response failed schema");
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
const phases = parsed.data.phases.map((p) => ({
|
|
87
|
+
id: p.id,
|
|
88
|
+
title: p.title,
|
|
89
|
+
depends_on: p.depends_on,
|
|
90
|
+
exit_criteria: p.exit_criteria,
|
|
91
|
+
}));
|
|
92
|
+
return {
|
|
93
|
+
phases,
|
|
94
|
+
spec_chars_used: slice.length,
|
|
95
|
+
truncated,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Single-phase fallback used when `no_llm: true` or Haiku is offline.
|
|
100
|
+
* Lets the operator hand-edit roadmap.md before approving.
|
|
101
|
+
*/
|
|
102
|
+
export function stubRoadmap() {
|
|
103
|
+
return [
|
|
104
|
+
{
|
|
105
|
+
id: "phase-1-todo",
|
|
106
|
+
title: "Implement (single-phase stub)",
|
|
107
|
+
depends_on: [],
|
|
108
|
+
exit_criteria: "Operator hand-edits this phase list before accepting the draft.",
|
|
109
|
+
},
|
|
110
|
+
];
|
|
111
|
+
}
|
|
112
|
+
//# sourceMappingURL=draft.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"draft.js","sourceRoot":"","sources":["../../src/missions/draft.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mission orchestration — helpers for the `cairn_mission_*` MCP tools.
|
|
3
|
+
* State + I/O lives in `@isaacriehm/cairn-state`; this module holds
|
|
4
|
+
* the LLM call (Haiku doc-parse), cursor advance, and task linkage.
|
|
5
|
+
*/
|
|
6
|
+
export { draftRoadmapFromSpec, stubRoadmap, type DraftRoadmapArgs, type DraftRoadmapResult, } from "./draft.js";
|
|
7
|
+
export { advancePhase as advanceMissionPhase, allPhaseTasksDone, lookupPhase, type AdvanceResult, type AdvanceError, } from "./cursor.js";
|
|
8
|
+
export { onTaskCompleted, readTaskMissionAnchor, linkTaskToPhase, type TaskMissionAnchor, type TaskCompletionLink, } from "./task-link.js";
|
|
9
|
+
export { listMissions, loadDraftFromFile, previewRoadmap, runMissionAccept, runMissionAdvance, runMissionClose, runMissionGet, runMissionReopen, runMissionStart, writeDraftToFile, } from "./cli.js";
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mission orchestration — helpers for the `cairn_mission_*` MCP tools.
|
|
3
|
+
* State + I/O lives in `@isaacriehm/cairn-state`; this module holds
|
|
4
|
+
* the LLM call (Haiku doc-parse), cursor advance, and task linkage.
|
|
5
|
+
*/
|
|
6
|
+
export { draftRoadmapFromSpec, stubRoadmap, } from "./draft.js";
|
|
7
|
+
export { advancePhase as advanceMissionPhase, allPhaseTasksDone, lookupPhase, } from "./cursor.js";
|
|
8
|
+
export { onTaskCompleted, readTaskMissionAnchor, linkTaskToPhase, } from "./task-link.js";
|
|
9
|
+
export { listMissions, loadDraftFromFile, previewRoadmap, runMissionAccept, runMissionAdvance, runMissionClose, runMissionGet, runMissionReopen, runMissionStart, writeDraftToFile, } from "./cli.js";
|
|
10
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/missions/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EACL,oBAAoB,EACpB,WAAW,GAGZ,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,YAAY,IAAI,mBAAmB,EACnC,iBAAiB,EACjB,WAAW,GAGZ,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,eAAe,EACf,qBAAqB,EACrB,eAAe,GAGhB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EACL,YAAY,EACZ,iBAAiB,EACjB,cAAc,EACd,gBAAgB,EAChB,iBAAiB,EACjB,eAAe,EACf,aAAa,EACb,gBAAgB,EAChB,eAAe,EACf,gBAAgB,GACjB,MAAM,UAAU,CAAC"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Task ↔ mission linkage. Called by `cairn_task_complete` and the Stop
|
|
3
|
+
* hook auto-graduator after a task transitions to a terminal phase.
|
|
4
|
+
*
|
|
5
|
+
* Reads the task's status.yaml for the `mission_id` + `phase_id`
|
|
6
|
+
* stamps, appends the task id to `phase_progress[phaseId].task_ids` if
|
|
7
|
+
* not already present, and decides whether the phase is now ready to
|
|
8
|
+
* exit.
|
|
9
|
+
*
|
|
10
|
+
* - exit_gate=auto → advance cursor immediately.
|
|
11
|
+
* - exit_gate=prompt → emit `phase_ready_to_exit` invalidation event;
|
|
12
|
+
* Stop hook surfaces the AskUserQuestion next tick.
|
|
13
|
+
* - exit_gate=manual → no-op; operator runs `cairn mission advance`.
|
|
14
|
+
*/
|
|
15
|
+
export interface TaskMissionAnchor {
|
|
16
|
+
mission_id: string;
|
|
17
|
+
phase_id: string;
|
|
18
|
+
}
|
|
19
|
+
/** Read the mission anchor stamped on a task's status.yaml. */
|
|
20
|
+
export declare function readTaskMissionAnchor(repoRoot: string, taskId: string): TaskMissionAnchor | null;
|
|
21
|
+
/** Append `taskId` to a phase's `task_ids` if not already linked. */
|
|
22
|
+
export declare function linkTaskToPhase(repoRoot: string, missionId: string, phaseId: string, taskId: string): boolean;
|
|
23
|
+
export type TaskCompletionLink = {
|
|
24
|
+
kind: "no-mission";
|
|
25
|
+
} | {
|
|
26
|
+
kind: "linked";
|
|
27
|
+
mission_id: string;
|
|
28
|
+
phase_id: string;
|
|
29
|
+
gate: "prompt" | "auto" | "manual";
|
|
30
|
+
} | {
|
|
31
|
+
kind: "phase-advanced";
|
|
32
|
+
mission_id: string;
|
|
33
|
+
phase_id: string;
|
|
34
|
+
next_phase: string | null;
|
|
35
|
+
mission_closed: boolean;
|
|
36
|
+
} | {
|
|
37
|
+
kind: "phase-ready-to-exit";
|
|
38
|
+
mission_id: string;
|
|
39
|
+
phase_id: string;
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* Called after a task graduates to `succeeded` (only). Reads the
|
|
43
|
+
* task's mission anchor, links the task id, and either advances the
|
|
44
|
+
* cursor (gate=auto) or emits a `phase_ready_to_exit` invalidation
|
|
45
|
+
* event for the Stop hook (gate=prompt).
|
|
46
|
+
*
|
|
47
|
+
* Failed/aborted tasks are NOT linked (a failed task does not satisfy
|
|
48
|
+
* a phase exit). Pass-through `no-mission` when the task wasn't
|
|
49
|
+
* mission-anchored.
|
|
50
|
+
*/
|
|
51
|
+
export declare function onTaskCompleted(repoRoot: string, taskId: string, outcome: "succeeded" | "failed" | "aborted", taskIsDone?: (id: string) => boolean): TaskCompletionLink;
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Task ↔ mission linkage. Called by `cairn_task_complete` and the Stop
|
|
3
|
+
* hook auto-graduator after a task transitions to a terminal phase.
|
|
4
|
+
*
|
|
5
|
+
* Reads the task's status.yaml for the `mission_id` + `phase_id`
|
|
6
|
+
* stamps, appends the task id to `phase_progress[phaseId].task_ids` if
|
|
7
|
+
* not already present, and decides whether the phase is now ready to
|
|
8
|
+
* exit.
|
|
9
|
+
*
|
|
10
|
+
* - exit_gate=auto → advance cursor immediately.
|
|
11
|
+
* - exit_gate=prompt → emit `phase_ready_to_exit` invalidation event;
|
|
12
|
+
* Stop hook surfaces the AskUserQuestion next tick.
|
|
13
|
+
* - exit_gate=manual → no-op; operator runs `cairn mission advance`.
|
|
14
|
+
*/
|
|
15
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
16
|
+
import { join } from "node:path";
|
|
17
|
+
import { parse as parseYaml } from "yaml";
|
|
18
|
+
/**
|
|
19
|
+
* `tasks/done/<id>/` exists → the task graduated to a terminal phase
|
|
20
|
+
* (succeeded / failed / aborted). The mission task-completion linkage
|
|
21
|
+
* only counts `succeeded`; the upstream caller filters by outcome
|
|
22
|
+
* before invoking onTaskCompleted, so a present `done/<id>/` here
|
|
23
|
+
* means a sibling already graduated successfully.
|
|
24
|
+
*/
|
|
25
|
+
function taskIsCompletedOnDisk(repoRoot, taskId) {
|
|
26
|
+
const dir = join(repoRoot, ".cairn", "tasks", "done", taskId);
|
|
27
|
+
if (!existsSync(dir))
|
|
28
|
+
return false;
|
|
29
|
+
try {
|
|
30
|
+
return statSync(dir).isDirectory();
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
import { appendMissionJournal, effectivePhaseExitGate, readMissionState, readRoadmap, writeMissionState, } from "@isaacriehm/cairn-state";
|
|
37
|
+
import { writeInvalidationEvent } from "../events/index.js";
|
|
38
|
+
import { advancePhase, allPhaseTasksDone } from "./cursor.js";
|
|
39
|
+
/** Read the mission anchor stamped on a task's status.yaml. */
|
|
40
|
+
export function readTaskMissionAnchor(repoRoot, taskId) {
|
|
41
|
+
const candidates = [
|
|
42
|
+
join(repoRoot, ".cairn", "tasks", "active", taskId, "status.yaml"),
|
|
43
|
+
join(repoRoot, ".cairn", "tasks", "done", taskId, "status.yaml"),
|
|
44
|
+
];
|
|
45
|
+
for (const path of candidates) {
|
|
46
|
+
if (!existsSync(path))
|
|
47
|
+
continue;
|
|
48
|
+
let raw;
|
|
49
|
+
try {
|
|
50
|
+
raw = readFileSync(path, "utf8");
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
let parsed;
|
|
56
|
+
try {
|
|
57
|
+
parsed = parseYaml(raw);
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (parsed === null || typeof parsed !== "object")
|
|
63
|
+
continue;
|
|
64
|
+
const obj = parsed;
|
|
65
|
+
const missionId = obj["mission_id"];
|
|
66
|
+
const phaseId = obj["phase_id"];
|
|
67
|
+
if (typeof missionId === "string" && typeof phaseId === "string" && missionId.length > 0) {
|
|
68
|
+
return { mission_id: missionId, phase_id: phaseId };
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
/** Append `taskId` to a phase's `task_ids` if not already linked. */
|
|
74
|
+
export function linkTaskToPhase(repoRoot, missionId, phaseId, taskId) {
|
|
75
|
+
const state = readMissionState(repoRoot, missionId);
|
|
76
|
+
if (state === null)
|
|
77
|
+
return false;
|
|
78
|
+
const progress = state.phase_progress[phaseId] ?? {
|
|
79
|
+
state: "in_progress",
|
|
80
|
+
task_ids: [],
|
|
81
|
+
};
|
|
82
|
+
if (!progress.task_ids.includes(taskId)) {
|
|
83
|
+
progress.task_ids = [...progress.task_ids, taskId];
|
|
84
|
+
state.phase_progress[phaseId] = progress;
|
|
85
|
+
writeMissionState(repoRoot, missionId, state);
|
|
86
|
+
appendMissionJournal(repoRoot, missionId, {
|
|
87
|
+
ts: new Date().toISOString(),
|
|
88
|
+
kind: "task-attached",
|
|
89
|
+
phase_id: phaseId,
|
|
90
|
+
task_id: taskId,
|
|
91
|
+
});
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Called after a task graduates to `succeeded` (only). Reads the
|
|
98
|
+
* task's mission anchor, links the task id, and either advances the
|
|
99
|
+
* cursor (gate=auto) or emits a `phase_ready_to_exit` invalidation
|
|
100
|
+
* event for the Stop hook (gate=prompt).
|
|
101
|
+
*
|
|
102
|
+
* Failed/aborted tasks are NOT linked (a failed task does not satisfy
|
|
103
|
+
* a phase exit). Pass-through `no-mission` when the task wasn't
|
|
104
|
+
* mission-anchored.
|
|
105
|
+
*/
|
|
106
|
+
export function onTaskCompleted(repoRoot, taskId, outcome, taskIsDone = (id) => taskIsCompletedOnDisk(repoRoot, id)) {
|
|
107
|
+
if (outcome !== "succeeded")
|
|
108
|
+
return { kind: "no-mission" };
|
|
109
|
+
const anchor = readTaskMissionAnchor(repoRoot, taskId);
|
|
110
|
+
if (anchor === null)
|
|
111
|
+
return { kind: "no-mission" };
|
|
112
|
+
linkTaskToPhase(repoRoot, anchor.mission_id, anchor.phase_id, taskId);
|
|
113
|
+
const roadmap = readRoadmap(repoRoot, anchor.mission_id);
|
|
114
|
+
const state = readMissionState(repoRoot, anchor.mission_id);
|
|
115
|
+
if (roadmap === null || state === null) {
|
|
116
|
+
return { kind: "linked", mission_id: anchor.mission_id, phase_id: anchor.phase_id, gate: "manual" };
|
|
117
|
+
}
|
|
118
|
+
const gate = effectivePhaseExitGate(roadmap.frontmatter, anchor.phase_id) ?? roadmap.frontmatter.exit_gate;
|
|
119
|
+
if (!allPhaseTasksDone(state, anchor.phase_id, taskIsDone)) {
|
|
120
|
+
return { kind: "linked", mission_id: anchor.mission_id, phase_id: anchor.phase_id, gate };
|
|
121
|
+
}
|
|
122
|
+
if (gate === "auto") {
|
|
123
|
+
const r = advancePhase(repoRoot, anchor.mission_id, anchor.phase_id);
|
|
124
|
+
if (r.ok) {
|
|
125
|
+
return {
|
|
126
|
+
kind: "phase-advanced",
|
|
127
|
+
mission_id: anchor.mission_id,
|
|
128
|
+
phase_id: anchor.phase_id,
|
|
129
|
+
next_phase: r.next_phase?.id ?? null,
|
|
130
|
+
mission_closed: r.closed,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
return { kind: "linked", mission_id: anchor.mission_id, phase_id: anchor.phase_id, gate };
|
|
134
|
+
}
|
|
135
|
+
if (gate === "prompt") {
|
|
136
|
+
try {
|
|
137
|
+
writeInvalidationEvent(repoRoot, {
|
|
138
|
+
kind: "phase-ready-to-exit",
|
|
139
|
+
refs: [{ kind: "task", id: taskId }],
|
|
140
|
+
source: { session_id: null, tool: "cairn_task_complete" },
|
|
141
|
+
path: `.cairn/missions/${anchor.mission_id}/state.json`,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
// best-effort
|
|
146
|
+
}
|
|
147
|
+
return {
|
|
148
|
+
kind: "phase-ready-to-exit",
|
|
149
|
+
mission_id: anchor.mission_id,
|
|
150
|
+
phase_id: anchor.phase_id,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
return { kind: "linked", mission_id: anchor.mission_id, phase_id: anchor.phase_id, gate };
|
|
154
|
+
}
|
|
155
|
+
//# sourceMappingURL=task-link.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"task-link.js","sourceRoot":"","sources":["../../src/missions/task-link.ts"],"names":[],"mappings":"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"}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* filesystem reads.
|
|
5
5
|
*/
|
|
6
6
|
type SessionStartSource = "startup" | "resume" | "clear" | "compact" | string;
|
|
7
|
-
export type SessionStartSection = "first_session_onboarding" | "run_handoff" | "header" | "code_change_contract" | "two_zone_reminder" | "brand_and_positioning" | "decisions_in_scope" | "invariants_active" | "current_task" | "quality_grades_tail" | "pending_drafts";
|
|
7
|
+
export type SessionStartSection = "first_session_onboarding" | "run_handoff" | "header" | "code_change_contract" | "two_zone_reminder" | "brand_and_positioning" | "decisions_in_scope" | "invariants_active" | "current_task" | "quality_grades_tail" | "pending_drafts" | "active_mission";
|
|
8
8
|
export interface BuildSessionStartContextArgs {
|
|
9
9
|
/** Resolved repo root (the dir containing `.cairn/`). */
|
|
10
10
|
repoRoot: string;
|
|
@@ -7,7 +7,7 @@ import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
|
7
7
|
import { basename, dirname, join, resolve } from "node:path";
|
|
8
8
|
import { parse as parseYaml } from "yaml";
|
|
9
9
|
import { buildHandoffBlock } from "../context/index.js";
|
|
10
|
-
import { buildDecisionsLedger, buildInvariantsLedger, parseFrontmatter, } from "@isaacriehm/cairn-state";
|
|
10
|
+
import { buildDecisionsLedger, buildInvariantsLedger, countDonePhases, detectRoadmapDrift, findActiveMission, parseFrontmatter, readMissionState, readRoadmap, } from "@isaacriehm/cairn-state";
|
|
11
11
|
import { loadSensorRegistry } from "../sensors/catalog.js";
|
|
12
12
|
import { CODE_CHANGE_CONTRACT, SESSION_START_HEADER, TWO_ZONE_REMINDER_BASE, } from "./templates.js";
|
|
13
13
|
const DEFAULT_MAX_CHARS = 12_000;
|
|
@@ -110,6 +110,8 @@ export async function buildSessionStartContext(args) {
|
|
|
110
110
|
const drafts = listPendingDrafts(args.repoRoot, warnings);
|
|
111
111
|
counts.pendingDrafts = drafts.length;
|
|
112
112
|
const pendingDraftsSection = renderPendingDraftsSection(drafts);
|
|
113
|
+
// ── Section 7 — active mission cursor ─────────────────────────────
|
|
114
|
+
const activeMissionSection = renderActiveMissionSection(args.repoRoot, warnings);
|
|
113
115
|
// Baseline findings drive the attention surface alongside drafts +
|
|
114
116
|
// drift. Only HARD findings count — soft findings (e.g. the
|
|
115
117
|
// commented-block-3-plus-lines pattern that fires on every
|
|
@@ -151,6 +153,9 @@ export async function buildSessionStartContext(args) {
|
|
|
151
153
|
if (brandAndPositioningSection !== null) {
|
|
152
154
|
orderedSections.push({ id: "brand_and_positioning", body: brandAndPositioningSection });
|
|
153
155
|
}
|
|
156
|
+
if (activeMissionSection !== null) {
|
|
157
|
+
orderedSections.push({ id: "active_mission", body: activeMissionSection });
|
|
158
|
+
}
|
|
154
159
|
if (currentTaskSection !== null) {
|
|
155
160
|
orderedSections.push({ id: "current_task", body: currentTaskSection });
|
|
156
161
|
}
|
|
@@ -176,6 +181,7 @@ export async function buildSessionStartContext(args) {
|
|
|
176
181
|
"invariants_active",
|
|
177
182
|
"decisions_in_scope",
|
|
178
183
|
"current_task",
|
|
184
|
+
"active_mission",
|
|
179
185
|
"two_zone_reminder",
|
|
180
186
|
"header",
|
|
181
187
|
"code_change_contract",
|
|
@@ -218,6 +224,57 @@ export async function buildSessionStartContext(args) {
|
|
|
218
224
|
warnings,
|
|
219
225
|
};
|
|
220
226
|
}
|
|
227
|
+
function renderActiveMissionSection(repoRoot, warnings) {
|
|
228
|
+
let missionId;
|
|
229
|
+
try {
|
|
230
|
+
missionId = findActiveMission(repoRoot);
|
|
231
|
+
}
|
|
232
|
+
catch (err) {
|
|
233
|
+
warnings.push(`mission lookup failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
if (missionId === null)
|
|
237
|
+
return null;
|
|
238
|
+
let roadmap;
|
|
239
|
+
let state;
|
|
240
|
+
try {
|
|
241
|
+
roadmap = readRoadmap(repoRoot, missionId);
|
|
242
|
+
state = readMissionState(repoRoot, missionId);
|
|
243
|
+
}
|
|
244
|
+
catch (err) {
|
|
245
|
+
warnings.push(`mission read failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
248
|
+
if (roadmap === null || state === null)
|
|
249
|
+
return null;
|
|
250
|
+
const cursorPhaseId = state.cursor.active_phase;
|
|
251
|
+
const cursorPhase = cursorPhaseId === null
|
|
252
|
+
? null
|
|
253
|
+
: roadmap.frontmatter.phases.find((p) => p.id === cursorPhaseId) ?? null;
|
|
254
|
+
const done = countDonePhases(state);
|
|
255
|
+
const total = roadmap.frontmatter.phases.length;
|
|
256
|
+
const drift = detectRoadmapDrift(roadmap.frontmatter, state);
|
|
257
|
+
const lines = [];
|
|
258
|
+
lines.push(`## Active mission — ${roadmap.frontmatter.title}`);
|
|
259
|
+
lines.push("");
|
|
260
|
+
lines.push(`- mission: \`${missionId}\``);
|
|
261
|
+
lines.push(`- progress: ${done}/${total} phases · exit_gate: ${roadmap.frontmatter.exit_gate}`);
|
|
262
|
+
if (cursorPhase !== null) {
|
|
263
|
+
lines.push(`- cursor: \`${cursorPhase.id}\` — ${cursorPhase.title}`);
|
|
264
|
+
lines.push(` - exit_criteria: ${cursorPhase.exit_criteria}`);
|
|
265
|
+
}
|
|
266
|
+
else {
|
|
267
|
+
lines.push(`- cursor: (no active phase — mission ready to close)`);
|
|
268
|
+
}
|
|
269
|
+
lines.push(`- spec: \`${roadmap.frontmatter.spec_path}\``);
|
|
270
|
+
if (drift.length > 0) {
|
|
271
|
+
lines.push("");
|
|
272
|
+
lines.push(`**Mission drift** — ${drift.length} phase id(s) in state.json no longer appear in roadmap.md: ${drift.map((id) => `\`${id}\``).join(", ")}. Resolve via \`cairn-attention\` (\`mission_drift\`).`);
|
|
273
|
+
}
|
|
274
|
+
lines.push("");
|
|
275
|
+
lines.push("Tasks under this cursor inherit `mission_id` + `phase_id` automatically. To work off-mission, pass `mission_id: \"\"` to `cairn_task_create` (side-task, no phase progress).");
|
|
276
|
+
return lines.join("\n");
|
|
277
|
+
}
|
|
221
278
|
function readBrandAndPositioning(repoRoot, warnings) {
|
|
222
279
|
const brandPath = join(repoRoot, ".cairn", "ground", "brand", "overview.md");
|
|
223
280
|
const positioningPath = join(repoRoot, ".cairn", "ground", "product", "positioning.md");
|