codecartographer-pi 0.1.0 → 0.1.2

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/README.md CHANGED
@@ -44,14 +44,18 @@ That's it. The LLM reads the guide, checks `workflow/status.yaml` for progress,
44
44
 
45
45
  This branch also packages CodeCartographer for [Pi](https://github.com/badlogic/pi-mono/tree/main/packages/coding-agent) without changing `.codecarto/` itself. Pi is an **optional peer dependency** — if you only want the template or the MCP server, you don't need Pi installed.
46
46
 
47
- Install from a local checkout or git URL:
47
+ Install from npm, a local checkout, or a git URL:
48
48
 
49
49
  ```bash
50
+ pi install npm:codecartographer-pi
51
+ # or, from a local checkout:
50
52
  pi install /absolute/path/to/CodeCartographer
51
- # or
53
+ # or, from a git URL:
52
54
  pi install git:github.com/your-user/CodeCartographer
53
55
  ```
54
56
 
57
+ > **Don't run `npm install codecartographer-pi` for the Pi use case.** Plain `npm install` puts the package on disk but doesn't register it with Pi, so it never appears in the TUI. Use `pi install npm:codecartographer-pi` instead — Pi handles the npm install internally and writes the package into its own `settings.json` (`~/.pi/agent/settings.json` by default). Plain `npm install` is the right command only for the MCP-server use case described below.
58
+
55
59
  For extension development, you can also point Pi directly at the extension entrypoint or place it in an auto-discovered extensions directory and use `/reload`:
56
60
 
57
61
  ```bash
@@ -84,6 +88,8 @@ What the Pi extension adds:
84
88
 
85
89
  The same framework is also packaged as a [Model Context Protocol](https://modelcontextprotocol.io) server, so any MCP-compatible host (Claude Code, Claude Desktop, etc.) can drive a CodeCartographer workflow without the Pi runtime. The server imports the same `core/` primitives the Pi extension uses, so phase prompts and validation are byte-identical across both surfaces.
86
90
 
91
+ Implements MCP spec revision [`2025-11-25`](https://modelcontextprotocol.io/specification/2025-11-25) via `@modelcontextprotocol/sdk` ≥ 1.29.0. The negotiated `protocolVersion` reflects whatever the connecting client requests; the server accepts every revision the SDK supports (currently `2025-11-25`, `2025-06-18`, `2025-03-26`, `2024-11-05`, `2024-10-07`).
92
+
87
93
  Install and wire it up:
88
94
 
89
95
  ```bash
@@ -1,7 +1,3 @@
1
- // Barrel re-export of the CodeCartographer framework core. Both wrappers
2
- // (extensions/codecarto for Pi, mcp-server for MCP) consume the framework
3
- // exclusively through this module.
4
-
5
1
  export * from "./types.ts";
6
2
  export * from "./utils.ts";
7
3
  export * from "./yaml.ts";
@@ -0,0 +1,10 @@
1
+ // Barrel re-export of the CodeCartographer framework core. Both wrappers
2
+ // (extensions/codecarto for Pi, mcp-server for MCP) consume the framework
3
+ // exclusively through this module.
4
+ export * from "./types.js";
5
+ export * from "./utils.js";
6
+ export * from "./yaml.js";
7
+ export * from "./status.js";
8
+ export * from "./pipeline.js";
9
+ export * from "./prompts.js";
10
+ export * from "./workspace.js";
@@ -0,0 +1,10 @@
1
+ import type { PipelineFile, PipelinePhase, ValidationResult, WorkspaceState } from "./types.ts";
2
+ export declare const PIPELINE_ALIASES: Record<string, string>;
3
+ export declare const DEFAULT_PIPELINE_PATH = "workflow/pipeline-full-with-deep-audit.yaml";
4
+ export declare function getPhaseMap(pipeline: PipelineFile): Map<string, PipelinePhase>;
5
+ export declare function getPipelineLabel(pipelinePath: string): string;
6
+ export declare function getNextEligiblePhase(state: WorkspaceState): PipelinePhase | null;
7
+ export declare function resolvePhase(state: WorkspaceState, phaseId?: string): PipelinePhase | null;
8
+ export declare function resolvePipelineChoice(input: string): string | null;
9
+ export declare function validatePhaseOutput(state: WorkspaceState, phaseId?: string): Promise<ValidationResult>;
10
+ export declare function buildValidationSummary(validation: ValidationResult): string[];
@@ -0,0 +1,160 @@
1
+ // Pipeline alias resolution, DAG walking, and phase-output validation.
2
+ import { readFile } from "node:fs/promises";
3
+ import { basename, join } from "node:path";
4
+ import { pathExists } from "./utils.js";
5
+ export const PIPELINE_ALIASES = {
6
+ "full-with-audit": "workflow/pipeline-full-with-audit.yaml",
7
+ "full-with-deep-audit": "workflow/pipeline-full-with-deep-audit.yaml",
8
+ full: "workflow/pipeline.yaml",
9
+ "defect-scan": "workflow/pipeline-defect-scan.yaml",
10
+ lite: "workflow/pipeline-lite.yaml",
11
+ "architecture-only": "workflow/pipeline-architecture-only.yaml",
12
+ };
13
+ export const DEFAULT_PIPELINE_PATH = "workflow/pipeline-full-with-deep-audit.yaml";
14
+ export function getPhaseMap(pipeline) {
15
+ return new Map(pipeline.phases.map((phase) => [phase.id, phase]));
16
+ }
17
+ export function getPipelineLabel(pipelinePath) {
18
+ const fileName = basename(pipelinePath, ".yaml");
19
+ if (fileName === "pipeline")
20
+ return "full";
21
+ return fileName.replace(/^pipeline-/, "");
22
+ }
23
+ export function getNextEligiblePhase(state) {
24
+ const phaseMap = getPhaseMap(state.pipeline);
25
+ for (const phaseId of state.pipeline.phase_order) {
26
+ const phaseStatus = state.status.phases[phaseId]?.status;
27
+ if (phaseStatus === "complete")
28
+ continue;
29
+ const phase = phaseMap.get(phaseId);
30
+ if (!phase)
31
+ continue;
32
+ const dependencies = phase.depends_on ?? [];
33
+ const ready = dependencies.every((dependencyId) => state.status.phases[dependencyId]?.status === "complete");
34
+ if (ready)
35
+ return phase;
36
+ }
37
+ return null;
38
+ }
39
+ export function resolvePhase(state, phaseId) {
40
+ const trimmed = phaseId?.trim();
41
+ if (trimmed) {
42
+ return getPhaseMap(state.pipeline).get(trimmed) ?? null;
43
+ }
44
+ return getNextEligiblePhase(state);
45
+ }
46
+ export function resolvePipelineChoice(input) {
47
+ const trimmed = input.trim();
48
+ if (!trimmed)
49
+ return null;
50
+ if (PIPELINE_ALIASES[trimmed])
51
+ return PIPELINE_ALIASES[trimmed];
52
+ return trimmed.endsWith(".yaml") ? trimmed : null;
53
+ }
54
+ export async function validatePhaseOutput(state, phaseId) {
55
+ const phase = resolvePhase(state, phaseId);
56
+ if (!phase) {
57
+ throw new Error(phaseId ? `Unknown phase: ${phaseId}` : "No eligible phase found.");
58
+ }
59
+ if (!phase.primary_output) {
60
+ throw new Error(`Phase ${phase.id} has no primary_output in the active pipeline.`);
61
+ }
62
+ const outputPath = join(state.workspaceDir, phase.primary_output);
63
+ if (!(await pathExists(outputPath))) {
64
+ return {
65
+ phaseId: phase.id,
66
+ primaryOutput: phase.primary_output,
67
+ outputPath,
68
+ exists: false,
69
+ hasValidationBlock: false,
70
+ overall: "MISSING",
71
+ rows: [],
72
+ gaps: [],
73
+ errors: [`Missing primary output: .codecarto/${phase.primary_output}`],
74
+ };
75
+ }
76
+ const content = await readFile(outputPath, "utf8");
77
+ const validationHeadingIndex = content.lastIndexOf("## Validation");
78
+ if (validationHeadingIndex === -1) {
79
+ return {
80
+ phaseId: phase.id,
81
+ primaryOutput: phase.primary_output,
82
+ outputPath,
83
+ exists: true,
84
+ hasValidationBlock: false,
85
+ overall: "FAIL",
86
+ rows: [],
87
+ gaps: [],
88
+ errors: ["Primary output exists but is missing a ## Validation block."],
89
+ };
90
+ }
91
+ const validationContent = content.slice(validationHeadingIndex);
92
+ const rows = [];
93
+ let overall = "FAIL";
94
+ for (const rawLine of validationContent.split(/\r?\n/)) {
95
+ const line = rawLine.trim();
96
+ if (line.startsWith("|")) {
97
+ const cells = line
98
+ .split("|")
99
+ .slice(1, -1)
100
+ .map((cell) => cell.trim());
101
+ if (cells.length >= 4 && cells[0] !== "#" && !/^[-:]+$/.test(cells[0])) {
102
+ rows.push({
103
+ criterion: cells[1] ?? "",
104
+ result: cells[2] ?? "",
105
+ evidence: cells[3] ?? "",
106
+ });
107
+ }
108
+ }
109
+ const overallMatch = line.match(/^\*\*Overall:\*\*\s*(.+)$/i);
110
+ if (overallMatch?.[1]) {
111
+ const normalizedOverall = overallMatch[1].trim().toUpperCase();
112
+ if (normalizedOverall === "PASS")
113
+ overall = "PASS";
114
+ else if (normalizedOverall === "PASS WITH GAPS")
115
+ overall = "PASS WITH GAPS";
116
+ else
117
+ overall = "FAIL";
118
+ }
119
+ }
120
+ const errors = [];
121
+ const gaps = rows
122
+ .filter((row) => row.result.toUpperCase().includes("PARTIAL"))
123
+ .map((row) => `${row.criterion}: ${row.evidence}`);
124
+ if (rows.length === 0) {
125
+ errors.push("Validation block found, but no validation rows could be parsed.");
126
+ }
127
+ if (rows.some((row) => row.result.toUpperCase().includes("FAIL"))) {
128
+ errors.push("One or more validation criteria are marked FAIL.");
129
+ overall = "FAIL";
130
+ }
131
+ if (overall === "FAIL" && errors.length === 0) {
132
+ errors.push("Validation overall result is FAIL.");
133
+ }
134
+ return {
135
+ phaseId: phase.id,
136
+ primaryOutput: phase.primary_output,
137
+ outputPath,
138
+ exists: true,
139
+ hasValidationBlock: true,
140
+ overall,
141
+ rows,
142
+ gaps,
143
+ errors,
144
+ };
145
+ }
146
+ export function buildValidationSummary(validation) {
147
+ const lines = [`Validation: ${validation.overall}`];
148
+ if (!validation.exists) {
149
+ lines.push(...validation.errors);
150
+ return lines;
151
+ }
152
+ lines.push(`Output: .codecarto/${validation.primaryOutput}`);
153
+ if (validation.gaps.length > 0) {
154
+ lines.push(`Gaps: ${validation.gaps.length}`);
155
+ }
156
+ if (validation.errors.length > 0) {
157
+ lines.push(...validation.errors.slice(0, 3));
158
+ }
159
+ return lines;
160
+ }
@@ -0,0 +1,9 @@
1
+ import type { CarryForwardEntry, OpenQuestionEntry, PipelinePhase, ValidationResult, WorkspaceState } from "./types.ts";
2
+ export declare function describeEntry(entry: OpenQuestionEntry | CarryForwardEntry): string;
3
+ export declare function collectRoutedCarryForward(state: WorkspaceState, targetPhaseId: string): CarryForwardEntry[];
4
+ export declare function buildPhasePrompt(state: WorkspaceState, phase: PipelinePhase, forced: boolean): Promise<string>;
5
+ export declare function closeoutFileName(date: string, phaseOrModule: string): string;
6
+ export declare function buildThreadLogEntry(phaseOrModule: string, validation: ValidationResult, timestamp: string): string;
7
+ export declare function ensureCloseoutStub(workspaceDir: string, phaseOrModule: string, timestamp: string): Promise<string | null>;
8
+ export declare function listSkillNames(workspaceDir: string): Promise<string[]>;
9
+ export declare function buildSkillPrompt(state: WorkspaceState, skillName: string): Promise<string>;
@@ -0,0 +1,166 @@
1
+ // Prompt builders + closeout/thread-log helpers. The phase prompt is the
2
+ // single biggest fidelity surface — both Pi and the MCP server emit
3
+ // byte-identical text by importing buildPhasePrompt from here.
4
+ import { copyFile, mkdir, readdir } from "node:fs/promises";
5
+ import { join } from "node:path";
6
+ import { dateOnly, pathExists } from "./utils.js";
7
+ export function describeEntry(entry) {
8
+ const parts = [];
9
+ if (entry.id)
10
+ parts.push(entry.id);
11
+ if (entry.kind)
12
+ parts.push(`(${entry.kind})`);
13
+ if (entry.description)
14
+ parts.push(entry.description);
15
+ else if (entry.deferred_reason)
16
+ parts.push(entry.deferred_reason);
17
+ return parts.join(" ").trim() || "(unlabeled entry)";
18
+ }
19
+ export function collectRoutedCarryForward(state, targetPhaseId) {
20
+ const routed = [];
21
+ for (const phase of Object.values(state.status.phases)) {
22
+ for (const entry of phase.carry_forward ?? []) {
23
+ if (entry.target_phase === targetPhaseId)
24
+ routed.push(entry);
25
+ }
26
+ }
27
+ return routed;
28
+ }
29
+ export async function buildPhasePrompt(state, phase, forced) {
30
+ const lines = [
31
+ `Read .codecarto/GUIDE.md and continue the CodeCartographer workflow for the phase \`${phase.id}\`.`,
32
+ `Work on this phase only. The analyzed source code is the repository outside .codecarto/.`,
33
+ "",
34
+ "Required reads before analysis:",
35
+ "- .codecarto/GUIDE.md",
36
+ "- .codecarto/workflow/status.yaml",
37
+ ];
38
+ const primaryOutput = phase.primary_output ? `.codecarto/${phase.primary_output}` : undefined;
39
+ if (primaryOutput) {
40
+ lines.push(`- ${primaryOutput} if it already exists (continue instead of duplicating work)`);
41
+ }
42
+ if (phase.skill_path)
43
+ lines.push(`- .codecarto/${phase.skill_path}`);
44
+ if (phase.output_template)
45
+ lines.push(`- .codecarto/${phase.output_template}`);
46
+ const staticReads = new Set(["GUIDE.md", "workflow/status.yaml"]);
47
+ const phaseReads = (phase.required_reads ?? []).filter((path) => path && !staticReads.has(path));
48
+ for (const path of phaseReads) {
49
+ lines.push(`- .codecarto/${path}`);
50
+ }
51
+ const conventionsPath = join(state.workspaceDir, "CONVENTIONS.md");
52
+ if (await pathExists(conventionsPath)) {
53
+ lines.push("- .codecarto/CONVENTIONS.md (cross-cutting patterns the orchestrator has promoted)");
54
+ }
55
+ const decisionsPath = join(state.workspaceDir, "DECISIONS.md");
56
+ if (await pathExists(decisionsPath)) {
57
+ lines.push("- .codecarto/DECISIONS.md (numbered project decisions; new entries are appended in your closeout)");
58
+ }
59
+ const routed = collectRoutedCarryForward(state, phase.id);
60
+ if (routed.length > 0) {
61
+ lines.push("", `Items routed to \`${phase.id}\` for closure (carry_forward from earlier phases):`);
62
+ for (const entry of routed) {
63
+ lines.push(`- ${describeEntry(entry)}`);
64
+ }
65
+ lines.push("Close each item by editing your phase output to address it, then remove the entry from the source phase's carry_forward in workflow/status.yaml.");
66
+ }
67
+ if (phase.id === "reimplementation-spec") {
68
+ lines.push("");
69
+ lines.push("Strategic Alignment Hook (run BEFORE producing the spec):");
70
+ lines.push("- Confirm with the user whether this spec should be language-agnostic or opinionated:");
71
+ lines.push(" - language-agnostic → use templates/reimplementation-spec.md (default).");
72
+ lines.push(" - opinionated (target stack locked) → use templates/reimplementation-spec-opinionated.md.");
73
+ lines.push("- Record the chosen variant in the spec front-matter and in your validation block.");
74
+ }
75
+ lines.push("", "Rules:");
76
+ lines.push("- Do not modify source files outside .codecarto/.");
77
+ lines.push("- Follow the active pipeline and validation protocol.");
78
+ lines.push("- Update findings under .codecarto/findings/ for this phase.");
79
+ lines.push("- Distinguish open_questions (genuinely unknown) from carry_forward (routed to a specific later phase) when updating workflow/status.yaml — see GUIDE.md \"Open Questions vs Carry-Forward\".");
80
+ if (forced) {
81
+ lines.push("- The user explicitly requested this phase even if it is not the next eligible phase.");
82
+ }
83
+ if (phase.depends_on && phase.depends_on.length > 0) {
84
+ const unmet = phase.depends_on.filter((dependencyId) => state.status.phases[dependencyId]?.status !== "complete");
85
+ if (unmet.length > 0) {
86
+ lines.push(`- Warning: dependencies not complete yet: ${unmet.join(", ")}`);
87
+ }
88
+ }
89
+ if (phase.handoff_requirements && phase.handoff_requirements.length > 0) {
90
+ lines.push("", "Handoff requirements (from the active pipeline):");
91
+ for (const requirement of phase.handoff_requirements) {
92
+ lines.push(`- ${requirement}`);
93
+ }
94
+ }
95
+ if (primaryOutput) {
96
+ lines.push("", `Primary output target: ${primaryOutput}`);
97
+ }
98
+ return lines.join("\n");
99
+ }
100
+ export function closeoutFileName(date, phaseOrModule) {
101
+ return `${date}-${phaseOrModule}.md`;
102
+ }
103
+ export function buildThreadLogEntry(phaseOrModule, validation, timestamp) {
104
+ const date = dateOnly(timestamp);
105
+ const file = closeoutFileName(date, phaseOrModule);
106
+ return `- ${date} — ${phaseOrModule} — Validation: ${validation.overall} — [closeout](closeouts/${file})\n`;
107
+ }
108
+ export async function ensureCloseoutStub(workspaceDir, phaseOrModule, timestamp) {
109
+ const date = dateOnly(timestamp);
110
+ const closeoutsDir = join(workspaceDir, "closeouts");
111
+ const closeoutPath = join(closeoutsDir, closeoutFileName(date, phaseOrModule));
112
+ if (await pathExists(closeoutPath))
113
+ return null;
114
+ const templatePath = join(workspaceDir, "templates", "closeout-template.md");
115
+ if (!(await pathExists(templatePath)))
116
+ return null;
117
+ await mkdir(closeoutsDir, { recursive: true });
118
+ await copyFile(templatePath, closeoutPath);
119
+ return closeoutPath;
120
+ }
121
+ export async function listSkillNames(workspaceDir) {
122
+ const skillsDir = join(workspaceDir, "skills");
123
+ if (!(await pathExists(skillsDir)))
124
+ return [];
125
+ try {
126
+ const entries = await readdir(skillsDir, { withFileTypes: true });
127
+ const names = [];
128
+ for (const entry of entries) {
129
+ if (!entry.isDirectory())
130
+ continue;
131
+ const skillFile = join(skillsDir, entry.name, "SKILL.md");
132
+ if (await pathExists(skillFile))
133
+ names.push(entry.name);
134
+ }
135
+ return names.sort();
136
+ }
137
+ catch {
138
+ return [];
139
+ }
140
+ }
141
+ export async function buildSkillPrompt(state, skillName) {
142
+ const lines = [
143
+ `Read .codecarto/GUIDE.md and run the post-pipeline skill \`${skillName}\`.`,
144
+ "This is post-pipeline work. The pipeline is `complete`. Do not change `current_phase` or `phase_order` in workflow/status.yaml.",
145
+ "",
146
+ "Required reads before starting:",
147
+ "- .codecarto/GUIDE.md",
148
+ "- .codecarto/workflow/status.yaml",
149
+ `- .codecarto/skills/${skillName}/SKILL.md`,
150
+ ];
151
+ const conventionsPath = join(state.workspaceDir, "CONVENTIONS.md");
152
+ if (await pathExists(conventionsPath)) {
153
+ lines.push("- .codecarto/CONVENTIONS.md (cross-cutting patterns the orchestrator has promoted)");
154
+ }
155
+ const decisionsPath = join(state.workspaceDir, "DECISIONS.md");
156
+ if (await pathExists(decisionsPath)) {
157
+ lines.push("- .codecarto/DECISIONS.md (numbered project decisions; new entries are appended in your closeout)");
158
+ }
159
+ lines.push("", "Rules:");
160
+ lines.push("- Do not modify source files outside .codecarto/.");
161
+ lines.push("- Follow the SKILL.md instructions exactly; the skill enforces its own discipline (see GUIDE.md).");
162
+ lines.push("- Update only the artifacts the skill calls for. Do NOT touch phase status entries.");
163
+ lines.push("- On completion, write a closeout at .codecarto/closeouts/<YYYY-MM-DD>-<skill-or-module>.md and append a one-line index entry to THREAD_LOG.md.");
164
+ lines.push("- If your work resolves entries in any phase's open_questions or carry_forward, remove only those resolved entries.");
165
+ return lines.join("\n");
166
+ }
@@ -0,0 +1,12 @@
1
+ import type { NormalizedStatus, OpenQuestionEntry, PipelineFile, StatusFile, StatusPhase } from "./types.ts";
2
+ export declare const LOCK_RETRY_MS = 125;
3
+ export declare const LOCK_TIMEOUT_MS = 5000;
4
+ export declare const STALE_LOCK_MS = 60000;
5
+ export declare function ensureArray(value: unknown): string[];
6
+ export declare function ensureEntryArray<T extends OpenQuestionEntry>(value: unknown, allowTargetPhase?: boolean): T[];
7
+ export declare function ensurePhaseRecord(value: unknown): Record<string, StatusPhase>;
8
+ export declare function createEmptyStatus(projectName: string, pipelinePath: string, pipeline: PipelineFile): NormalizedStatus;
9
+ export declare function normalizeStatus(status: StatusFile, pipeline: PipelineFile, pipelinePath: string, cwd: string): NormalizedStatus;
10
+ export declare function acquireLock(lockPath: string): Promise<{
11
+ release: () => Promise<void>;
12
+ }>;
@@ -0,0 +1,143 @@
1
+ // Status normalization, atomic writes, and file-lock primitives. Pure
2
+ // framework logic shared by every wrapper.
3
+ import { open, rm, stat } from "node:fs/promises";
4
+ import { basename } from "node:path";
5
+ import { sleep } from "./utils.js";
6
+ export const LOCK_RETRY_MS = 125;
7
+ export const LOCK_TIMEOUT_MS = 5000;
8
+ export const STALE_LOCK_MS = 60_000;
9
+ export function ensureArray(value) {
10
+ return Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : [];
11
+ }
12
+ function coerceEntry(value, allowTargetPhase) {
13
+ if (typeof value === "string") {
14
+ const trimmed = value.trim();
15
+ if (!trimmed)
16
+ return null;
17
+ return { description: trimmed };
18
+ }
19
+ if (!value || typeof value !== "object" || Array.isArray(value))
20
+ return null;
21
+ const raw = value;
22
+ const entry = {};
23
+ if (typeof raw.id === "string" && raw.id.trim())
24
+ entry.id = raw.id.trim();
25
+ if (typeof raw.kind === "string" && raw.kind.trim())
26
+ entry.kind = raw.kind.trim();
27
+ if (typeof raw.description === "string" && raw.description.trim())
28
+ entry.description = raw.description.trim();
29
+ if (typeof raw.deferred_reason === "string" && raw.deferred_reason.trim())
30
+ entry.deferred_reason = raw.deferred_reason.trim();
31
+ if (allowTargetPhase && typeof raw.target_phase === "string" && raw.target_phase.trim())
32
+ entry.target_phase = raw.target_phase.trim();
33
+ return Object.keys(entry).length > 0 ? entry : null;
34
+ }
35
+ export function ensureEntryArray(value, allowTargetPhase = false) {
36
+ if (!Array.isArray(value))
37
+ return [];
38
+ const result = [];
39
+ for (const item of value) {
40
+ const coerced = coerceEntry(item, allowTargetPhase);
41
+ if (coerced)
42
+ result.push(coerced);
43
+ }
44
+ return result;
45
+ }
46
+ export function ensurePhaseRecord(value) {
47
+ if (!value || typeof value !== "object")
48
+ return {};
49
+ const record = value;
50
+ const result = {};
51
+ for (const [phaseId, phaseValue] of Object.entries(record)) {
52
+ const phase = (phaseValue ?? {});
53
+ result[phaseId] = {
54
+ status: typeof phase.status === "string" ? phase.status : "pending",
55
+ owner_notes: ensureArray(phase.owner_notes),
56
+ outputs_present: ensureArray(phase.outputs_present),
57
+ open_questions: ensureEntryArray(phase.open_questions, false),
58
+ carry_forward: ensureEntryArray(phase.carry_forward, true),
59
+ };
60
+ }
61
+ return result;
62
+ }
63
+ export function createEmptyStatus(projectName, pipelinePath, pipeline) {
64
+ const phases = {};
65
+ for (const phaseId of pipeline.phase_order) {
66
+ phases[phaseId] = {
67
+ status: "pending",
68
+ owner_notes: [],
69
+ outputs_present: [],
70
+ open_questions: [],
71
+ carry_forward: [],
72
+ };
73
+ }
74
+ const firstPhase = pipeline.phase_order[0] ?? "complete";
75
+ const phaseMap = new Map(pipeline.phases.map((phase) => [phase.id, phase]));
76
+ const firstPhaseConfig = phaseMap.get(firstPhase);
77
+ return {
78
+ project_name: projectName,
79
+ pipeline: pipelinePath,
80
+ current_phase: firstPhase,
81
+ last_updated: "",
82
+ phases,
83
+ next_actions: firstPhaseConfig?.primary_output
84
+ ? [`Begin ${firstPhase} phase by producing ${firstPhaseConfig.primary_output}`]
85
+ : ["Begin the first pending phase."],
86
+ };
87
+ }
88
+ export function normalizeStatus(status, pipeline, pipelinePath, cwd) {
89
+ const phases = ensurePhaseRecord(status.phases);
90
+ for (const phaseId of pipeline.phase_order) {
91
+ if (!phases[phaseId]) {
92
+ phases[phaseId] = {
93
+ status: "pending",
94
+ owner_notes: [],
95
+ outputs_present: [],
96
+ open_questions: [],
97
+ carry_forward: [],
98
+ };
99
+ }
100
+ }
101
+ return {
102
+ project_name: status.project_name?.trim() || basename(cwd),
103
+ pipeline: status.pipeline?.trim() || pipelinePath,
104
+ current_phase: status.current_phase?.trim() || pipeline.phase_order[0] || "complete",
105
+ last_updated: status.last_updated?.trim() || "",
106
+ phases,
107
+ next_actions: ensureArray(status.next_actions),
108
+ };
109
+ }
110
+ export async function acquireLock(lockPath) {
111
+ const startedAt = Date.now();
112
+ while (true) {
113
+ try {
114
+ const handle = await open(lockPath, "wx");
115
+ await handle.writeFile(`${process.pid}\n${new Date().toISOString()}\n`, "utf8");
116
+ await handle.close();
117
+ return {
118
+ release: async () => {
119
+ await rm(lockPath, { force: true }).catch(() => undefined);
120
+ },
121
+ };
122
+ }
123
+ catch (error) {
124
+ const nodeError = error;
125
+ if (nodeError.code !== "EEXIST")
126
+ throw error;
127
+ try {
128
+ const lockStat = await stat(lockPath);
129
+ if (Date.now() - lockStat.mtimeMs > STALE_LOCK_MS) {
130
+ await rm(lockPath, { force: true }).catch(() => undefined);
131
+ continue;
132
+ }
133
+ }
134
+ catch {
135
+ continue;
136
+ }
137
+ if (Date.now() - startedAt > LOCK_TIMEOUT_MS) {
138
+ throw new Error(`Timed out waiting for lock: ${lockPath}`);
139
+ }
140
+ await sleep(LOCK_RETRY_MS);
141
+ }
142
+ }
143
+ }
@@ -0,0 +1,77 @@
1
+ export type PhaseStatusValue = "pending" | "complete" | "partial" | "in-progress";
2
+ export declare const OPEN_QUESTION_KINDS: readonly ["needs-runtime-test", "needs-maintainer-decision", "needs-spec-ruling", "defer-to-phase", "needs-fixture-capture"];
3
+ export type EntryKind = (typeof OPEN_QUESTION_KINDS)[number] | string;
4
+ export type OpenQuestionEntry = {
5
+ id?: string;
6
+ kind?: EntryKind;
7
+ description?: string;
8
+ deferred_reason?: string;
9
+ };
10
+ export type CarryForwardEntry = OpenQuestionEntry & {
11
+ target_phase?: string;
12
+ };
13
+ export type StatusPhase = {
14
+ status: PhaseStatusValue | string;
15
+ owner_notes: string[];
16
+ outputs_present: string[];
17
+ open_questions: OpenQuestionEntry[];
18
+ carry_forward: CarryForwardEntry[];
19
+ };
20
+ export type StatusFile = {
21
+ project_name?: string;
22
+ pipeline?: string;
23
+ current_phase?: string;
24
+ last_updated?: string;
25
+ phases?: Record<string, StatusPhase>;
26
+ next_actions?: string[];
27
+ };
28
+ export type SecondaryOutput = {
29
+ path: string;
30
+ mode?: string;
31
+ };
32
+ export type PipelinePhase = {
33
+ id: string;
34
+ purpose?: string;
35
+ skill_path?: string;
36
+ output_template?: string;
37
+ depends_on?: string[];
38
+ primary_output?: string;
39
+ secondary_outputs?: SecondaryOutput[];
40
+ required_reads?: string[];
41
+ completion_criteria?: string[];
42
+ handoff_requirements?: string[];
43
+ };
44
+ export type PipelineFile = {
45
+ workflow_name?: string;
46
+ workflow_version?: number;
47
+ workflow_goal?: string;
48
+ source_location?: string;
49
+ validation_protocol?: string;
50
+ phase_order: string[];
51
+ phases: PipelinePhase[];
52
+ };
53
+ export type NormalizedStatus = Required<Pick<StatusFile, "project_name" | "pipeline" | "current_phase" | "last_updated" | "phases" | "next_actions">>;
54
+ export type WorkspaceState = {
55
+ cwd: string;
56
+ workspaceDir: string;
57
+ statusPath: string;
58
+ pipelinePath: string;
59
+ status: NormalizedStatus;
60
+ pipeline: PipelineFile;
61
+ };
62
+ export type ValidationOverall = "PASS" | "PASS WITH GAPS" | "FAIL" | "MISSING";
63
+ export type ValidationResult = {
64
+ phaseId: string;
65
+ primaryOutput: string;
66
+ outputPath: string;
67
+ exists: boolean;
68
+ hasValidationBlock: boolean;
69
+ overall: ValidationOverall;
70
+ rows: Array<{
71
+ criterion: string;
72
+ result: string;
73
+ evidence: string;
74
+ }>;
75
+ gaps: string[];
76
+ errors: string[];
77
+ };
@@ -0,0 +1,10 @@
1
+ // Shared schema types for the CodeCartographer framework.
2
+ // Both the Pi extension and the MCP server import from here so the schema
3
+ // has a single source of truth.
4
+ export const OPEN_QUESTION_KINDS = [
5
+ "needs-runtime-test",
6
+ "needs-maintainer-decision",
7
+ "needs-spec-ruling",
8
+ "defer-to-phase",
9
+ "needs-fixture-capture",
10
+ ];
@@ -0,0 +1,8 @@
1
+ export declare function sleep(ms: number): Promise<void>;
2
+ export declare function pathExists(path: string): Promise<boolean>;
3
+ export declare function canonicalPath(path: string): Promise<string>;
4
+ export declare function normalizeForComparison(path: string): string;
5
+ export declare function isWithinPath(path: string, root: string): boolean;
6
+ export declare function isPlainObject(value: unknown): value is Record<string, unknown>;
7
+ export declare function uniqueStrings(items: string[]): string[];
8
+ export declare function dateOnly(timestamp: string): string;