codecartographer-pi 0.14.1 → 0.16.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/.codecarto/BACKLOG.md +4 -12
- package/.codecarto/GUIDE.md +35 -36
- package/.codecarto/NEW_THREAD_BLURB.md +3 -3
- package/.codecarto/skills/spec-delta-application/SKILL.md +2 -2
- package/.codecarto/templates/amendment.yaml +17 -0
- package/.codecarto/templates/conventions-template.md +6 -5
- package/.codecarto/templates/decisions-template.md +13 -10
- package/.codecarto/templates/phase-handoff.yaml +7 -0
- package/.codecarto/templates/spike-report.md +51 -0
- package/.codecarto/workflow/scaffold-version.yaml +1 -1
- package/agent-skill/codecartographer/SKILL.md +7 -1
- package/agent-skill/codecartographer/references/handoff-contract.md +12 -1
- package/agent-skill/codecartographer/references/library.md +32 -0
- package/agent-skill/codecartographer/references/orchestration.md +45 -0
- package/dist/core/amendment.d.ts +41 -0
- package/dist/core/amendment.js +143 -0
- package/dist/core/completion.d.ts +16 -0
- package/dist/core/completion.js +196 -5
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.js +1 -0
- package/dist/core/pipeline.js +18 -0
- package/dist/core/prompts.js +53 -0
- package/dist/core/status.d.ts +16 -1
- package/dist/core/status.js +49 -1
- package/dist/core/types.d.ts +24 -0
- package/dist/core/usage.d.ts +7 -0
- package/dist/core/usage.js +2 -1
- package/dist/core/workspace.d.ts +39 -0
- package/dist/core/workspace.js +92 -4
- package/dist/extensions/codecarto/auto-runner.js +1 -0
- package/dist/extensions/codecarto/dashboard-writer.d.ts +8 -1
- package/dist/extensions/codecarto/dashboard-writer.js +10 -1
- package/dist/extensions/codecarto/index.js +4 -1
- package/dist/mcp-server/server.d.ts +23 -0
- package/dist/mcp-server/server.js +139 -7
- package/package.json +1 -1
package/dist/core/types.d.ts
CHANGED
|
@@ -85,6 +85,28 @@ export type ValidationResult = {
|
|
|
85
85
|
}>;
|
|
86
86
|
gaps: string[];
|
|
87
87
|
errors: string[];
|
|
88
|
+
/**
|
|
89
|
+
* The phase's declared secondary outputs with existence at validation time.
|
|
90
|
+
* Non-gating visibility: secondary outputs are created only when needed,
|
|
91
|
+
* but one that ends the phase absent and unaccounted-for is a silent drop.
|
|
92
|
+
*/
|
|
93
|
+
secondaryOutputs?: Array<{
|
|
94
|
+
path: string;
|
|
95
|
+
exists: boolean;
|
|
96
|
+
}>;
|
|
97
|
+
};
|
|
98
|
+
/**
|
|
99
|
+
* One convention a phase proposes for promotion. Completion stages these in
|
|
100
|
+
* CONVENTIONS.md under "## Pending proposals"; promoting a proposal into the
|
|
101
|
+
* canonical sections stays an orchestrator judgment at the phase boundary.
|
|
102
|
+
*/
|
|
103
|
+
export type ProposedConventionEntry = {
|
|
104
|
+
/** Short name for the pattern (becomes the staged bullet's label). */
|
|
105
|
+
name: string;
|
|
106
|
+
/** The rule itself, one or two sentences. */
|
|
107
|
+
rule: string;
|
|
108
|
+
/** Optional: where the pattern showed up (file, phase, incident). */
|
|
109
|
+
evidence?: string;
|
|
88
110
|
};
|
|
89
111
|
export type PhaseHandoff = {
|
|
90
112
|
phase_id: string;
|
|
@@ -101,6 +123,8 @@ export type PhaseHandoff = {
|
|
|
101
123
|
open_question_closures: string[];
|
|
102
124
|
post_pipeline: PostPipelineEntry[];
|
|
103
125
|
decisions: string[];
|
|
126
|
+
/** Conventions proposed for promotion; completion stages them in CONVENTIONS.md. Omitted defaults to empty. */
|
|
127
|
+
proposed_conventions: ProposedConventionEntry[];
|
|
104
128
|
closeout_content: string;
|
|
105
129
|
closeout_summary: string;
|
|
106
130
|
schema_version?: number;
|
package/dist/core/usage.d.ts
CHANGED
|
@@ -23,6 +23,13 @@ export interface UsageRun {
|
|
|
23
23
|
tokens: UsageTokens;
|
|
24
24
|
session_file?: string;
|
|
25
25
|
compactions?: CompactionTelemetry;
|
|
26
|
+
/**
|
|
27
|
+
* Which surface recorded the run. `pi-runner` entries carry real token and
|
|
28
|
+
* activity telemetry; `mcp-complete` entries are completion receipts with
|
|
29
|
+
* zeroed counters, because MCP hosts execute phases in their own context
|
|
30
|
+
* and report no usage (issue #100). Absent on entries from older versions.
|
|
31
|
+
*/
|
|
32
|
+
recorded_by?: "pi-runner" | "mcp-complete";
|
|
26
33
|
}
|
|
27
34
|
export interface UsageFile {
|
|
28
35
|
version: number;
|
package/dist/core/usage.js
CHANGED
|
@@ -105,7 +105,8 @@ function isUsageRun(x) {
|
|
|
105
105
|
isFiniteNumber(run.duration_ms) &&
|
|
106
106
|
isUsageTokens(run.tokens) &&
|
|
107
107
|
(run.session_file === undefined || typeof run.session_file === "string") &&
|
|
108
|
-
(run.compactions === undefined || isCompactionTelemetry(run.compactions))
|
|
108
|
+
(run.compactions === undefined || isCompactionTelemetry(run.compactions)) &&
|
|
109
|
+
(run.recorded_by === undefined || run.recorded_by === "pi-runner" || run.recorded_by === "mcp-complete"));
|
|
109
110
|
}
|
|
110
111
|
function isUsageTokens(x) {
|
|
111
112
|
if (!x || typeof x !== "object")
|
package/dist/core/workspace.d.ts
CHANGED
|
@@ -4,6 +4,45 @@ export declare const packageRoot: string;
|
|
|
4
4
|
export declare const packagedWorkspaceDir: string;
|
|
5
5
|
export declare const PACKAGE_VERSION: string;
|
|
6
6
|
export declare function getWorkspaceState(cwd: string): Promise<WorkspaceState | null>;
|
|
7
|
+
/** The orchestrator-maintained files init seeds and completion appends to. */
|
|
8
|
+
export declare const ORCHESTRATOR_FILES: readonly [{
|
|
9
|
+
readonly file: "CONVENTIONS.md";
|
|
10
|
+
readonly template: "conventions-template.md";
|
|
11
|
+
}, {
|
|
12
|
+
readonly file: "DECISIONS.md";
|
|
13
|
+
readonly template: "decisions-template.md";
|
|
14
|
+
}];
|
|
15
|
+
/**
|
|
16
|
+
* Seed the orchestrator-maintained files from the workspace's templates
|
|
17
|
+
* (issue #98): orchestration is on by default, so a fresh workspace starts
|
|
18
|
+
* with both skeletons instead of gating them behind a role ritual. Idempotent
|
|
19
|
+
* — existing files are never touched, and a scaffold without the templates
|
|
20
|
+
* (pre-template era) is left for completion's minimal-header fallback.
|
|
21
|
+
* @returns the file names created, for the caller's report.
|
|
22
|
+
*/
|
|
23
|
+
export declare function seedOrchestratorFiles(workspaceDir: string): Promise<string[]>;
|
|
24
|
+
/** One scaffold refresh's outcome. */
|
|
25
|
+
export type RefreshScaffoldResult = {
|
|
26
|
+
/** Workspace-relative paths written, sorted. */
|
|
27
|
+
written: string[];
|
|
28
|
+
/** The workspace's scaffold version before the refresh, if any. */
|
|
29
|
+
scaffoldVersionBefore?: string;
|
|
30
|
+
/** The running framework version the scaffold now matches. */
|
|
31
|
+
scaffoldVersionAfter: string;
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Refresh a workspace's framework-owned files from the packaged template
|
|
35
|
+
* (issue #102): both staleness notices instruct exactly this, and the only
|
|
36
|
+
* tool that previously touched scaffold files was init's force mode, which
|
|
37
|
+
* backs up the entire workspace. Copies every file the packaged template
|
|
38
|
+
* ships except project state (`workflow/status.yaml`), user configuration
|
|
39
|
+
* (`workflow/config.yaml`, usage log), user-owned top-level files
|
|
40
|
+
* (BACKLOG, THREAD_LOG, CONVENTIONS, DECISIONS), and session-written
|
|
41
|
+
* directories (`scratch/`, `inputs/`, `closeouts/`). Files the template no
|
|
42
|
+
* longer ships are left in place. Appends one THREAD_LOG entry naming the
|
|
43
|
+
* version transition.
|
|
44
|
+
*/
|
|
45
|
+
export declare function refreshScaffold(cwd: string): Promise<RefreshScaffoldResult>;
|
|
7
46
|
/**
|
|
8
47
|
* Human-readable staleness notice for the workspace's .codecarto/ scaffold,
|
|
9
48
|
* or null when the scaffold matches the running framework. A missing marker
|
package/dist/core/workspace.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// + normalizes the per-project workspace state from disk, and provides the
|
|
4
4
|
// atomic status-update primitive used by /codecarto-complete.
|
|
5
5
|
import { existsSync, readFileSync } from "node:fs";
|
|
6
|
-
import { appendFile, readFile, rename, writeFile } from "node:fs/promises";
|
|
6
|
+
import { appendFile, copyFile, mkdir, readFile, readdir, rename, writeFile } from "node:fs/promises";
|
|
7
7
|
import { basename, dirname, join, relative } from "node:path";
|
|
8
8
|
import { fileURLToPath } from "node:url";
|
|
9
9
|
import { acquireLock, applyHandoff, createEmptyStatus, normalizeStatus, parseHandoff } from "./status.js";
|
|
@@ -98,6 +98,94 @@ export async function getWorkspaceState(cwd) {
|
|
|
98
98
|
...(scaffoldVersion !== undefined && { scaffoldVersion }),
|
|
99
99
|
};
|
|
100
100
|
}
|
|
101
|
+
/** The orchestrator-maintained files init seeds and completion appends to. */
|
|
102
|
+
export const ORCHESTRATOR_FILES = [
|
|
103
|
+
{ file: "CONVENTIONS.md", template: "conventions-template.md" },
|
|
104
|
+
{ file: "DECISIONS.md", template: "decisions-template.md" },
|
|
105
|
+
];
|
|
106
|
+
/**
|
|
107
|
+
* Seed the orchestrator-maintained files from the workspace's templates
|
|
108
|
+
* (issue #98): orchestration is on by default, so a fresh workspace starts
|
|
109
|
+
* with both skeletons instead of gating them behind a role ritual. Idempotent
|
|
110
|
+
* — existing files are never touched, and a scaffold without the templates
|
|
111
|
+
* (pre-template era) is left for completion's minimal-header fallback.
|
|
112
|
+
* @returns the file names created, for the caller's report.
|
|
113
|
+
*/
|
|
114
|
+
export async function seedOrchestratorFiles(workspaceDir) {
|
|
115
|
+
const created = [];
|
|
116
|
+
for (const { file, template } of ORCHESTRATOR_FILES) {
|
|
117
|
+
const target = join(workspaceDir, file);
|
|
118
|
+
if (await pathExists(target))
|
|
119
|
+
continue;
|
|
120
|
+
const templatePath = join(workspaceDir, "templates", template);
|
|
121
|
+
if (!(await pathExists(templatePath)))
|
|
122
|
+
continue;
|
|
123
|
+
await copyFile(templatePath, target);
|
|
124
|
+
created.push(file);
|
|
125
|
+
}
|
|
126
|
+
return created;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Workspace paths refresh never touches: project state, user configuration,
|
|
130
|
+
* user-owned top-level files, and the directories sessions write into.
|
|
131
|
+
* Everything else present in the packaged template is framework-owned.
|
|
132
|
+
*/
|
|
133
|
+
const REFRESH_EXCLUDED_TOP_LEVEL = new Set(["BACKLOG.md", "THREAD_LOG.md", "CONVENTIONS.md", "DECISIONS.md"]);
|
|
134
|
+
const REFRESH_EXCLUDED_DIRS = new Set(["scratch", "inputs", "closeouts"]);
|
|
135
|
+
const REFRESH_EXCLUDED_WORKFLOW_FILES = new Set(["status.yaml", "config.yaml", ".usage.local.yaml"]);
|
|
136
|
+
async function listTemplateFiles(dir, relativeDir = "") {
|
|
137
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
138
|
+
const files = [];
|
|
139
|
+
for (const entry of entries) {
|
|
140
|
+
const relativePath = relativeDir ? `${relativeDir}/${entry.name}` : entry.name;
|
|
141
|
+
if (entry.isDirectory()) {
|
|
142
|
+
if (!relativeDir && REFRESH_EXCLUDED_DIRS.has(entry.name))
|
|
143
|
+
continue;
|
|
144
|
+
files.push(...await listTemplateFiles(join(dir, entry.name), relativePath));
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
if (!relativeDir && REFRESH_EXCLUDED_TOP_LEVEL.has(entry.name))
|
|
148
|
+
continue;
|
|
149
|
+
if (relativeDir === "workflow" && REFRESH_EXCLUDED_WORKFLOW_FILES.has(entry.name))
|
|
150
|
+
continue;
|
|
151
|
+
files.push(relativePath);
|
|
152
|
+
}
|
|
153
|
+
return files;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Refresh a workspace's framework-owned files from the packaged template
|
|
157
|
+
* (issue #102): both staleness notices instruct exactly this, and the only
|
|
158
|
+
* tool that previously touched scaffold files was init's force mode, which
|
|
159
|
+
* backs up the entire workspace. Copies every file the packaged template
|
|
160
|
+
* ships except project state (`workflow/status.yaml`), user configuration
|
|
161
|
+
* (`workflow/config.yaml`, usage log), user-owned top-level files
|
|
162
|
+
* (BACKLOG, THREAD_LOG, CONVENTIONS, DECISIONS), and session-written
|
|
163
|
+
* directories (`scratch/`, `inputs/`, `closeouts/`). Files the template no
|
|
164
|
+
* longer ships are left in place. Appends one THREAD_LOG entry naming the
|
|
165
|
+
* version transition.
|
|
166
|
+
*/
|
|
167
|
+
export async function refreshScaffold(cwd) {
|
|
168
|
+
const state = await getWorkspaceState(cwd);
|
|
169
|
+
if (!state)
|
|
170
|
+
throw new Error("CodeCartographer workspace not found. Run /codecarto-init first.");
|
|
171
|
+
if (!existsSync(packagedWorkspaceDir)) {
|
|
172
|
+
throw new Error("Packaged .codecarto template is missing. Reinstall codecartographer-pi.");
|
|
173
|
+
}
|
|
174
|
+
const scaffoldVersionBefore = state.scaffoldVersion;
|
|
175
|
+
const files = (await listTemplateFiles(packagedWorkspaceDir)).sort();
|
|
176
|
+
for (const relativePath of files) {
|
|
177
|
+
const target = join(state.workspaceDir, relativePath);
|
|
178
|
+
await mkdir(dirname(target), { recursive: true });
|
|
179
|
+
await copyFile(join(packagedWorkspaceDir, relativePath), target);
|
|
180
|
+
}
|
|
181
|
+
const entry = `- ${new Date().toISOString().slice(0, 10)} — scaffold-refresh — Refreshed ${files.length} framework-owned file(s) from the packaged template (${scaffoldVersionBefore ?? "unversioned"} → ${PACKAGE_VERSION}); project state, user config, and session outputs untouched.`;
|
|
182
|
+
await appendFile(join(state.workspaceDir, "THREAD_LOG.md"), `${entry}\n`, "utf8");
|
|
183
|
+
return {
|
|
184
|
+
written: files,
|
|
185
|
+
...(scaffoldVersionBefore !== undefined && { scaffoldVersionBefore }),
|
|
186
|
+
scaffoldVersionAfter: PACKAGE_VERSION,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
101
189
|
// Numeric x.y.z comparison; null when either side is not a plain dotted triple.
|
|
102
190
|
function compareDottedVersions(a, b) {
|
|
103
191
|
const parse = (version) => {
|
|
@@ -125,7 +213,7 @@ function compareDottedVersions(a, b) {
|
|
|
125
213
|
export function describeScaffoldStaleness(state) {
|
|
126
214
|
const scaffold = state.scaffoldVersion;
|
|
127
215
|
if (!scaffold) {
|
|
128
|
-
return "This workspace's .codecarto/ scaffold has no workflow/scaffold-version.yaml marker (introduced after v0.12.11), so its framework-owned files (GUIDE.md, templates/, workflow/ pipelines and VALIDATE.md) may predate the v0.12.0 handoff contract. Refresh them from the packaged CodeCartographer template.";
|
|
216
|
+
return "This workspace's .codecarto/ scaffold has no workflow/scaffold-version.yaml marker (introduced after v0.12.11), so its framework-owned files (GUIDE.md, templates/, workflow/ pipelines and VALIDATE.md) may predate the v0.12.0 handoff contract. Refresh them from the packaged CodeCartographer template (codecarto_refresh_scaffold does exactly this without touching project state).";
|
|
129
217
|
}
|
|
130
218
|
const comparison = compareDottedVersions(scaffold, PACKAGE_VERSION);
|
|
131
219
|
if (comparison === 0)
|
|
@@ -133,10 +221,10 @@ export function describeScaffoldStaleness(state) {
|
|
|
133
221
|
if (comparison === null) {
|
|
134
222
|
return scaffold === PACKAGE_VERSION
|
|
135
223
|
? null
|
|
136
|
-
: `This workspace's scaffold version (${scaffold}) does not match the running framework (${PACKAGE_VERSION}). Refresh the framework-owned files (GUIDE.md, templates/, workflow/) from the packaged template.`;
|
|
224
|
+
: `This workspace's scaffold version (${scaffold}) does not match the running framework (${PACKAGE_VERSION}). Refresh the framework-owned files (GUIDE.md, templates/, workflow/) from the packaged template — codecarto_refresh_scaffold does exactly this without touching project state.`;
|
|
137
225
|
}
|
|
138
226
|
if (comparison < 0) {
|
|
139
|
-
return `This workspace's scaffold (v${scaffold}) is older than the running framework (v${PACKAGE_VERSION}). Refresh the framework-owned files (GUIDE.md, templates/, workflow/) from the packaged template to pick up pipeline and template fixes.`;
|
|
227
|
+
return `This workspace's scaffold (v${scaffold}) is older than the running framework (v${PACKAGE_VERSION}). Refresh the framework-owned files (GUIDE.md, templates/, workflow/) from the packaged template to pick up pipeline and template fixes — codecarto_refresh_scaffold does exactly this without touching project state.`;
|
|
140
228
|
}
|
|
141
229
|
return `This workspace's scaffold (v${scaffold}) is newer than the running framework (v${PACKAGE_VERSION}). Upgrade CodeCartographer to at least v${scaffold}.`;
|
|
142
230
|
}
|
|
@@ -357,6 +357,7 @@ async function recordUsage(workspaceDir, phaseId, status, activity, sessionFile)
|
|
|
357
357
|
output: activity.lifetimeUsage.output,
|
|
358
358
|
cache_write: activity.lifetimeUsage.cacheWrite,
|
|
359
359
|
},
|
|
360
|
+
recorded_by: "pi-runner",
|
|
360
361
|
compactions: activity.compactions,
|
|
361
362
|
...(sessionFile ? { session_file: sessionFile } : {}),
|
|
362
363
|
});
|
|
@@ -1 +1,8 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Render and atomically replace `.codecarto/dashboard.html`.
|
|
3
|
+
* @returns true when a fresh dashboard landed on disk; false when the
|
|
4
|
+
* workspace is missing or any gather/render/write step failed (swallowed —
|
|
5
|
+
* lifecycle callers must never fail on a dashboard problem, but they may
|
|
6
|
+
* report truthfully whether a refresh happened).
|
|
7
|
+
*/
|
|
8
|
+
export declare function writeDashboard(cwd: string, packageVersion: string): Promise<boolean>;
|
|
@@ -6,11 +6,18 @@ import { readdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
|
6
6
|
import { join } from "node:path";
|
|
7
7
|
import { DASHBOARD_RELATIVE_PATH, getWorkspaceState, loadUsage, NARRATION_CACHE_RELATIVE_PATH, parseSimpleYaml, pathExists, renderDashboard, } from "../../core/index.js";
|
|
8
8
|
const CLOSEOUT_FILENAME_RE = /^(\d{4}-\d{2}-\d{2})-(.+)\.md$/;
|
|
9
|
+
/**
|
|
10
|
+
* Render and atomically replace `.codecarto/dashboard.html`.
|
|
11
|
+
* @returns true when a fresh dashboard landed on disk; false when the
|
|
12
|
+
* workspace is missing or any gather/render/write step failed (swallowed —
|
|
13
|
+
* lifecycle callers must never fail on a dashboard problem, but they may
|
|
14
|
+
* report truthfully whether a refresh happened).
|
|
15
|
+
*/
|
|
9
16
|
export async function writeDashboard(cwd, packageVersion) {
|
|
10
17
|
try {
|
|
11
18
|
const state = await getWorkspaceState(cwd);
|
|
12
19
|
if (!state)
|
|
13
|
-
return;
|
|
20
|
+
return false;
|
|
14
21
|
const workspaceDir = state.workspaceDir;
|
|
15
22
|
const [usage, closeouts, outputsPresent, narration] = await Promise.all([
|
|
16
23
|
loadUsage(workspaceDir),
|
|
@@ -33,11 +40,13 @@ export async function writeDashboard(cwd, packageVersion) {
|
|
|
33
40
|
const tempPath = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
34
41
|
await writeFile(tempPath, html, "utf8");
|
|
35
42
|
await rename(tempPath, path);
|
|
43
|
+
return true;
|
|
36
44
|
}
|
|
37
45
|
catch {
|
|
38
46
|
// Best-effort: a failed dashboard write must not surface as a phase
|
|
39
47
|
// error. The user's pipeline state is unaffected; the next state
|
|
40
48
|
// change will trigger another render attempt.
|
|
49
|
+
return false;
|
|
41
50
|
}
|
|
42
51
|
}
|
|
43
52
|
async function listCloseouts(workspaceDir) {
|
|
@@ -8,7 +8,7 @@ import { narrateDashboard } from "./dashboard-narrator.js";
|
|
|
8
8
|
import { writeDashboard } from "./dashboard-writer.js";
|
|
9
9
|
import { parseNextFlags } from "./next-flags.js";
|
|
10
10
|
import { phaseCompactionExtension } from "./phase-compaction.js";
|
|
11
|
-
import { buildPhasePrompt, buildSkillPrompt, buildValidationSummary, canonicalPath, computePerPhaseTotals, computeTotals, createEmptyStatus, DEFAULT_PIPELINE_PATH, describeScaffoldStaleness, deriveSlug, discoverLibrary, getNextEligiblePhase, getPipelineLabel, getWorkspaceState, isWithinPathResolved, listSkillNames, loadCodecartoConfig, loadUsage, loadYamlFile, normalizeForComparison, packagedWorkspaceDir, pathExists, PACKAGE_VERSION, PhasePreflightError, PIPELINE_ALIASES, publishEntry, resolvePhase, resolvePipelineChoice, runPhasePreflight, stringifySimpleYaml, switchPipeline, validatePhaseOutput, writeLibraryConfig, } from "../../core/index.js";
|
|
11
|
+
import { buildPhasePrompt, buildSkillPrompt, buildValidationSummary, canonicalPath, computePerPhaseTotals, computeTotals, createEmptyStatus, DEFAULT_PIPELINE_PATH, describeScaffoldStaleness, deriveSlug, discoverLibrary, getNextEligiblePhase, getPipelineLabel, getWorkspaceState, isWithinPathResolved, listSkillNames, loadCodecartoConfig, loadUsage, loadYamlFile, normalizeForComparison, packagedWorkspaceDir, pathExists, PACKAGE_VERSION, PhasePreflightError, PIPELINE_ALIASES, publishEntry, resolvePhase, resolvePipelineChoice, runPhasePreflight, seedOrchestratorFiles, stringifySimpleYaml, switchPipeline, validatePhaseOutput, writeLibraryConfig, } from "../../core/index.js";
|
|
12
12
|
import { initLibrary } from "../../core/library.js";
|
|
13
13
|
import { resolveUserConfigPath } from "../../core/orchestrator-config.js";
|
|
14
14
|
const STATUS_WIDGET_ID = "codecarto-widget";
|
|
@@ -282,6 +282,9 @@ export default function codeCartographerExtension(pi) {
|
|
|
282
282
|
const normalizedStatus = createEmptyStatus(basename(ctx.cwd), selectedPipelinePath, pipeline);
|
|
283
283
|
normalizedStatus.last_updated = new Date().toISOString();
|
|
284
284
|
await writeFile(rawStatusPath, `${stringifySimpleYaml(normalizedStatus)}\n`, "utf8");
|
|
285
|
+
// Orchestration is on by default (issue #97/#98): seed the files its
|
|
286
|
+
// duties maintain so they exist from the first phase.
|
|
287
|
+
await seedOrchestratorFiles(targetWorkspaceDir);
|
|
285
288
|
codecartoModeActive = true;
|
|
286
289
|
lastFeedbackLines = [`Initialized workspace with pipeline: ${getPipelineLabel(selectedPipelinePath)}`];
|
|
287
290
|
ctx.ui.notify(`Initialized CodeCartographer (${getPipelineLabel(selectedPipelinePath)})`, "info");
|
|
@@ -207,6 +207,29 @@ export declare function handleListSkills(args: {
|
|
|
207
207
|
text: string;
|
|
208
208
|
};
|
|
209
209
|
}>;
|
|
210
|
+
export declare function handleRefreshScaffold(args: {
|
|
211
|
+
cwd: string;
|
|
212
|
+
}): Promise<{
|
|
213
|
+
content: {
|
|
214
|
+
type: "text";
|
|
215
|
+
text: string;
|
|
216
|
+
}[];
|
|
217
|
+
structuredContent: {
|
|
218
|
+
text: string;
|
|
219
|
+
};
|
|
220
|
+
}>;
|
|
221
|
+
export declare function handleAmend(args: {
|
|
222
|
+
cwd: string;
|
|
223
|
+
name: string;
|
|
224
|
+
}): Promise<{
|
|
225
|
+
content: {
|
|
226
|
+
type: "text";
|
|
227
|
+
text: string;
|
|
228
|
+
}[];
|
|
229
|
+
structuredContent: {
|
|
230
|
+
text: string;
|
|
231
|
+
};
|
|
232
|
+
}>;
|
|
210
233
|
export declare function handleGuide(args: {
|
|
211
234
|
topic?: string;
|
|
212
235
|
}): Promise<{
|
|
@@ -15,7 +15,9 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
15
15
|
import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } from "@modelcontextprotocol/sdk/types.js";
|
|
16
16
|
import { cp, mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
17
17
|
import { basename, isAbsolute, join } from "node:path";
|
|
18
|
-
import { buildPhasePrompt, buildSkillPrompt, buildValidationSummary, canonicalPath, completeValidatedPhase, computePerPhaseTotals, computeTotals, createEmptyStatus, DEFAULT_PIPELINE_PATH, deriveSlug, discoverLibrary, describeScaffoldStaleness, getNextEligiblePhase, getPipelineLabel, getWorkspaceState, isValidSlug, isWithinPathResolved, listEntries, listGuideTopics, readGuide, listSkillNames, loadCodecartoConfig, loadUsage, loadYamlFile, normalizeForComparison, PACKAGE_VERSION, packagedWorkspaceDir, pathExists, PhasePreflightError, publishEntry, reindex as libraryReindex, resolvePhase, resolvePipelineChoice, stringifySimpleYaml, switchPipeline, validatePhaseOutput, writeLibraryConfig, } from "../core/index.js";
|
|
18
|
+
import { buildPhasePrompt, buildSkillPrompt, buildValidationSummary, canonicalPath, completeValidatedPhase, computePerPhaseTotals, computeTotals, createEmptyStatus, DEFAULT_PIPELINE_PATH, deriveSlug, discoverLibrary, describeScaffoldStaleness, getNextEligiblePhase, getPipelineLabel, getWorkspaceState, isValidSlug, isWithinPathResolved, listEntries, listGuideTopics, readGuide, listSkillNames, loadCodecartoConfig, loadUsage, loadYamlFile, normalizeForComparison, PACKAGE_VERSION, packagedWorkspaceDir, pathExists, PhasePreflightError, publishEntry, reindex as libraryReindex, refreshScaffold, resolvePhase, resolvePipelineChoice, seedOrchestratorFiles, stringifySimpleYaml, switchPipeline, validatePhaseOutput, writeLibraryConfig, } from "../core/index.js";
|
|
19
|
+
import { applyAmendment } from "../core/amendment.js";
|
|
20
|
+
import { appendUsageRun } from "../core/usage.js";
|
|
19
21
|
import { initLibrary } from "../core/library.js";
|
|
20
22
|
import { loadUserConfig, resolveUserConfigPath } from "../core/orchestrator-config.js";
|
|
21
23
|
import { writeDashboard } from "../extensions/codecarto/dashboard-writer.js";
|
|
@@ -104,12 +106,23 @@ export async function handleInit(args) {
|
|
|
104
106
|
const normalizedStatus = createEmptyStatus(basename(cwd), selectedPipelinePath, pipeline);
|
|
105
107
|
normalizedStatus.last_updated = new Date().toISOString();
|
|
106
108
|
await writeFile(statusPath, `${stringifySimpleYaml(normalizedStatus)}\n`, "utf8");
|
|
109
|
+
// Orchestration is on by default (issue #97/#98): the driving chat holds the
|
|
110
|
+
// duties, so the files those duties maintain exist from the first phase.
|
|
111
|
+
const seeded = await seedOrchestratorFiles(targetWorkspaceDir);
|
|
107
112
|
const label = getPipelineLabel(selectedPipelinePath);
|
|
108
|
-
|
|
113
|
+
const initLines = [
|
|
114
|
+
`Initialized CodeCartographer workspace at ${targetWorkspaceDir}.`,
|
|
115
|
+
`Pipeline: ${label} (${selectedPipelinePath})`,
|
|
116
|
+
`First phase: ${normalizedStatus.current_phase}`,
|
|
117
|
+
];
|
|
118
|
+
if (seeded.length > 0)
|
|
119
|
+
initLines.push(`Seeded orchestrator files: ${seeded.join(", ")} (the driving chat holds the orchestrator duties — see GUIDE.md §Roles).`);
|
|
120
|
+
return textResult(initLines.join("\n"), {
|
|
109
121
|
workspaceDir: targetWorkspaceDir,
|
|
110
122
|
pipeline: selectedPipelinePath,
|
|
111
123
|
pipelineLabel: label,
|
|
112
124
|
firstPhase: normalizedStatus.current_phase,
|
|
125
|
+
seededOrchestratorFiles: seeded,
|
|
113
126
|
});
|
|
114
127
|
}
|
|
115
128
|
export async function handleStatus(args) {
|
|
@@ -131,7 +144,12 @@ export async function handleStatus(args) {
|
|
|
131
144
|
`Open questions (terminal unresolved): ${terminalOpenQuestions}`,
|
|
132
145
|
`Carry-forward (pipeline phases): ${totalCarryForward}`,
|
|
133
146
|
`Post-pipeline work: ${postPipelinePending} pending`,
|
|
134
|
-
|
|
147
|
+
// Render every stored action: the terminal list routes to several
|
|
148
|
+
// post-pipeline surfaces (issue #114), and a text-reading client that
|
|
149
|
+
// only ever sees actions[0] loses exactly the routing it exists for.
|
|
150
|
+
...(state.status.next_actions.length > 0
|
|
151
|
+
? state.status.next_actions.map((action, index) => `${index === 0 ? "Next: " : " "}${action}`)
|
|
152
|
+
: [`Next: ${nextPhase ? `Begin ${nextPhase.id}` : "All phases complete."}`]),
|
|
135
153
|
];
|
|
136
154
|
if (scaffoldNotice)
|
|
137
155
|
summaryLines.push(`Scaffold: ${scaffoldNotice}`);
|
|
@@ -216,6 +234,7 @@ export async function handleValidate(args) {
|
|
|
216
234
|
rows: validation.rows,
|
|
217
235
|
gaps: validation.gaps,
|
|
218
236
|
errors: validation.errors,
|
|
237
|
+
secondaryOutputs: validation.secondaryOutputs ?? [],
|
|
219
238
|
});
|
|
220
239
|
}
|
|
221
240
|
export async function handleComplete(args) {
|
|
@@ -227,20 +246,54 @@ export async function handleComplete(args) {
|
|
|
227
246
|
if (validation.overall === "FAIL" || validation.overall === "MISSING") {
|
|
228
247
|
throw new McpError(ErrorCode.InvalidRequest, `Cannot complete ${validation.phaseId}: validation is ${validation.overall}.\n${buildValidationSummary(validation).join("\n")}`);
|
|
229
248
|
}
|
|
230
|
-
const { updatedState, closeoutNotice } = await completeValidatedPhase(cwd, validation, "codecarto_complete").catch((error) => {
|
|
249
|
+
const { updatedState, closeoutNotice, orchestratorCheckpoint } = await completeValidatedPhase(cwd, validation, "codecarto_complete").catch((error) => {
|
|
231
250
|
throw new McpError(ErrorCode.InvalidParams, error instanceof Error ? error.message : String(error));
|
|
232
251
|
});
|
|
252
|
+
// Record the run in the usage log (issue #100). MCP hosts execute phases in
|
|
253
|
+
// their own context, so tokens and activity are unknowable here — this is a
|
|
254
|
+
// completion receipt, marked recorded_by so totals can tell it apart from
|
|
255
|
+
// the Pi runner's token-bearing entries. Recording lives in this handler,
|
|
256
|
+
// not core completion, because Pi-driven runs already record with real
|
|
257
|
+
// telemetry and must not double-count.
|
|
258
|
+
try {
|
|
259
|
+
await appendUsageRun(updatedState.workspaceDir, {
|
|
260
|
+
timestamp: new Date().toISOString(),
|
|
261
|
+
phase: validation.phaseId,
|
|
262
|
+
status: "completed",
|
|
263
|
+
turn_count: 0,
|
|
264
|
+
tool_uses: 0,
|
|
265
|
+
duration_ms: 0,
|
|
266
|
+
tokens: { input: 0, output: 0, cache_write: 0 },
|
|
267
|
+
recorded_by: "mcp-complete",
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
catch {
|
|
271
|
+
// Usage is best-effort telemetry: the phase completed and canonical
|
|
272
|
+
// state is already written, so a usage-log write failure must not fail
|
|
273
|
+
// the completion result. Nothing else can act on the error here.
|
|
274
|
+
}
|
|
275
|
+
// Dashboard freshness is a completion side effect (issue #112): the counts
|
|
276
|
+
// it renders change exactly here, and a stale dashboard misreports them
|
|
277
|
+
// confidently. writeDashboard never throws; its boolean says whether a
|
|
278
|
+
// fresh render actually landed, so the result only claims what happened.
|
|
279
|
+
const dashboardPath = (await writeDashboard(cwd, PACKAGE_VERSION)) ? ".codecarto/dashboard.html" : undefined;
|
|
233
280
|
const lines = [
|
|
234
281
|
`Marked ${validation.phaseId} complete (validation: ${validation.overall}).`,
|
|
235
282
|
`Next phase: ${updatedState.status.current_phase}`,
|
|
236
283
|
];
|
|
237
284
|
if (closeoutNotice)
|
|
238
285
|
lines.push(closeoutNotice);
|
|
286
|
+
if (orchestratorCheckpoint)
|
|
287
|
+
lines.push(orchestratorCheckpoint);
|
|
288
|
+
if (dashboardPath)
|
|
289
|
+
lines.push(`Dashboard refreshed: ${dashboardPath}`);
|
|
239
290
|
return textResult(lines.join("\n"), {
|
|
240
291
|
completedPhase: validation.phaseId,
|
|
241
292
|
validation: validation.overall,
|
|
242
293
|
nextPhase: updatedState.status.current_phase,
|
|
243
294
|
closeoutNotice,
|
|
295
|
+
orchestratorCheckpoint,
|
|
296
|
+
dashboardPath,
|
|
244
297
|
});
|
|
245
298
|
}
|
|
246
299
|
export async function handleSkill(args) {
|
|
@@ -619,18 +672,22 @@ export async function handleUsage(args) {
|
|
|
619
672
|
}
|
|
620
673
|
const totals = computeTotals(usage);
|
|
621
674
|
const perPhase = computePerPhaseTotals(usage);
|
|
675
|
+
const receiptRuns = usage.runs.filter((run) => run.recorded_by === "mcp-complete").length;
|
|
622
676
|
const lines = [
|
|
623
677
|
`Total runs: ${totals.runs}`,
|
|
624
678
|
`Total tokens: ${totals.tokens.input} in / ${totals.tokens.output} out / ${totals.tokens.cache_write} cache-write`,
|
|
625
679
|
`Total duration: ${totals.duration_ms}ms / ${totals.tool_uses} tool uses`,
|
|
626
|
-
"",
|
|
627
|
-
"Per-phase totals:",
|
|
628
680
|
];
|
|
681
|
+
if (receiptRuns > 0) {
|
|
682
|
+
lines.push(`Note: ${receiptRuns} run(s) recorded via codecarto_complete carry no token or activity data (MCP hosts execute phases in their own context) — zeros above are unknowns, not free runs.`);
|
|
683
|
+
}
|
|
684
|
+
lines.push("", "Per-phase totals:");
|
|
629
685
|
for (const [phaseId, t] of perPhase) {
|
|
630
686
|
lines.push(` ${phaseId}: ${t.runs} run(s), ${t.tokens.input + t.tokens.output} tokens, ${t.tool_uses} tool uses, ${t.duration_ms}ms`);
|
|
631
687
|
}
|
|
632
688
|
return textResult(lines.join("\n"), {
|
|
633
689
|
runs: totals.runs,
|
|
690
|
+
receiptRuns,
|
|
634
691
|
tokens: totals.tokens,
|
|
635
692
|
toolUses: totals.tool_uses,
|
|
636
693
|
durationMs: totals.duration_ms,
|
|
@@ -640,7 +697,9 @@ export async function handleUsage(args) {
|
|
|
640
697
|
export async function handleDashboard(args) {
|
|
641
698
|
const cwd = await validateCwd(args.cwd);
|
|
642
699
|
await requireWorkspace(cwd);
|
|
643
|
-
await writeDashboard(cwd, PACKAGE_VERSION)
|
|
700
|
+
if (!(await writeDashboard(cwd, PACKAGE_VERSION))) {
|
|
701
|
+
throw new McpError(ErrorCode.InvalidRequest, "Dashboard render failed: the workspace state could not be gathered or .codecarto/dashboard.html is not writable.");
|
|
702
|
+
}
|
|
644
703
|
return textResult("Dashboard regenerated: .codecarto/dashboard.html", { path: ".codecarto/dashboard.html" });
|
|
645
704
|
}
|
|
646
705
|
export async function handleListSkills(args) {
|
|
@@ -652,6 +711,56 @@ export async function handleListSkills(args) {
|
|
|
652
711
|
: ["No skills installed."];
|
|
653
712
|
return textResult(lines.join("\n"), { skills });
|
|
654
713
|
}
|
|
714
|
+
export async function handleRefreshScaffold(args) {
|
|
715
|
+
const cwd = await validateCwd(args.cwd);
|
|
716
|
+
await requireWorkspace(cwd);
|
|
717
|
+
const result = await refreshScaffold(cwd).catch((error) => {
|
|
718
|
+
throw new McpError(ErrorCode.InvalidRequest, error instanceof Error ? error.message : String(error));
|
|
719
|
+
});
|
|
720
|
+
const shown = result.written.slice(0, 20);
|
|
721
|
+
const lines = [
|
|
722
|
+
`Refreshed ${result.written.length} framework-owned file(s) from the packaged template (${result.scaffoldVersionBefore ?? "unversioned"} → ${result.scaffoldVersionAfter}).`,
|
|
723
|
+
"Project state, user config, findings outputs, scratch, closeouts, and orchestrator files were not touched.",
|
|
724
|
+
...shown.map((path) => ` - .codecarto/${path}`),
|
|
725
|
+
];
|
|
726
|
+
if (result.written.length > shown.length)
|
|
727
|
+
lines.push(` … +${result.written.length - shown.length} more`);
|
|
728
|
+
return textResult(lines.join("\n"), {
|
|
729
|
+
written: result.written,
|
|
730
|
+
scaffoldVersionBefore: result.scaffoldVersionBefore,
|
|
731
|
+
scaffoldVersionAfter: result.scaffoldVersionAfter,
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
export async function handleAmend(args) {
|
|
735
|
+
if (typeof args.name !== "string" || !args.name.trim()) {
|
|
736
|
+
throw new McpError(ErrorCode.InvalidParams, "name is required (the amendment file's slug under .codecarto/scratch/amendments/)");
|
|
737
|
+
}
|
|
738
|
+
const cwd = await validateCwd(args.cwd);
|
|
739
|
+
await requireWorkspace(cwd);
|
|
740
|
+
const { applied, closeoutNotice } = await applyAmendment(cwd, args.name).catch((error) => {
|
|
741
|
+
throw new McpError(ErrorCode.InvalidRequest, error instanceof Error ? error.message : String(error));
|
|
742
|
+
});
|
|
743
|
+
// An amendment exists precisely to change the numbers the dashboard shows
|
|
744
|
+
// (issue #112); refresh it, reporting only a render that actually landed.
|
|
745
|
+
const dashboardPath = (await writeDashboard(cwd, PACKAGE_VERSION)) ? ".codecarto/dashboard.html" : undefined;
|
|
746
|
+
const lines = [
|
|
747
|
+
`Amendment applied.`,
|
|
748
|
+
`Open questions closed: ${applied.openQuestionsClosed.length > 0 ? applied.openQuestionsClosed.join(", ") : "none"}`,
|
|
749
|
+
`Post-pipeline items closed: ${applied.postPipelineClosed.length > 0 ? applied.postPipelineClosed.join(", ") : "none"}`,
|
|
750
|
+
];
|
|
751
|
+
if (applied.unknownIds.length > 0)
|
|
752
|
+
lines.push(`Ids that matched nothing (already closed or unknown): ${applied.unknownIds.join(", ")}`);
|
|
753
|
+
lines.push(closeoutNotice);
|
|
754
|
+
if (dashboardPath)
|
|
755
|
+
lines.push(`Dashboard refreshed: ${dashboardPath}`);
|
|
756
|
+
return textResult(lines.join("\n"), {
|
|
757
|
+
openQuestionsClosed: applied.openQuestionsClosed,
|
|
758
|
+
postPipelineClosed: applied.postPipelineClosed,
|
|
759
|
+
unknownIds: applied.unknownIds,
|
|
760
|
+
closeoutNotice,
|
|
761
|
+
dashboardPath,
|
|
762
|
+
});
|
|
763
|
+
}
|
|
655
764
|
// ---------- tool registry ----------
|
|
656
765
|
const TOOLS = [
|
|
657
766
|
{
|
|
@@ -902,9 +1011,32 @@ const TOOLS = [
|
|
|
902
1011
|
required: ["cwd"],
|
|
903
1012
|
},
|
|
904
1013
|
},
|
|
1014
|
+
{
|
|
1015
|
+
name: "codecarto_amend",
|
|
1016
|
+
description: "Apply a post-pipeline amendment from .codecarto/scratch/amendments/<name>.yaml to workflow/status.yaml: close open questions resolved on evidence after the pipeline completed and retire finished post-pipeline backlog items, under the same lock completion uses. Writes an amendment closeout and THREAD_LOG entry. Refused while the pipeline is incomplete — mid-pipeline resolutions belong in the phase handoff. Idempotent: ids that no longer match are reported, not fatal.",
|
|
1017
|
+
inputSchema: {
|
|
1018
|
+
type: "object",
|
|
1019
|
+
properties: {
|
|
1020
|
+
cwd: { type: "string", description: "Absolute path to the target repository." },
|
|
1021
|
+
name: { type: "string", description: "Amendment file slug under .codecarto/scratch/amendments/ (with or without .yaml)." },
|
|
1022
|
+
},
|
|
1023
|
+
required: ["cwd", "name"],
|
|
1024
|
+
},
|
|
1025
|
+
},
|
|
1026
|
+
{
|
|
1027
|
+
name: "codecarto_refresh_scaffold",
|
|
1028
|
+
description: "Refresh a workspace's framework-owned files (GUIDE.md, templates/, workflow pipelines and VALIDATE.md, skills/, findings SKILL and README stubs) from the packaged template — the action every scaffold-staleness warning instructs. Never touches project state (status.yaml), user config (workflow/config.yaml, usage log), findings outputs, scratch/, closeouts/, or the orchestrator files (CONVENTIONS.md, DECISIONS.md, BACKLOG.md, THREAD_LOG.md). Appends one THREAD_LOG entry naming the version transition. Unlike codecarto_init force:true, nothing is backed up or lost.",
|
|
1029
|
+
inputSchema: {
|
|
1030
|
+
type: "object",
|
|
1031
|
+
properties: { cwd: { type: "string", description: "Absolute path to the target repository." } },
|
|
1032
|
+
required: ["cwd"],
|
|
1033
|
+
},
|
|
1034
|
+
},
|
|
905
1035
|
];
|
|
906
1036
|
const HANDLERS = {
|
|
1037
|
+
codecarto_amend: handleAmend,
|
|
907
1038
|
codecarto_init: handleInit,
|
|
1039
|
+
codecarto_refresh_scaffold: handleRefreshScaffold,
|
|
908
1040
|
codecarto_status: handleStatus,
|
|
909
1041
|
codecarto_switch_pipeline: handleSwitchPipeline,
|
|
910
1042
|
codecarto_next: handleNext,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codecartographer-pi",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
4
4
|
"mcpName": "io.github.HuginnIndustries/codecartographer",
|
|
5
5
|
"description": "Turn an unfamiliar codebase into a validated reimplementation spec, then synthesize confirmed specs and a product vision into a traceable plan.",
|
|
6
6
|
"type": "module",
|