codecartographer-pi 0.17.1 → 0.18.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/GUIDE.md +2 -2
- package/.codecarto/skills/spec-delta-application/SKILL.md +1 -1
- package/.codecarto/templates/amendment.yaml +3 -3
- package/.codecarto/templates/spike-report.md +2 -2
- package/.codecarto/workflow/scaffold-version.yaml +1 -1
- package/README.md +10 -6
- package/agent-skill/codecartographer/SKILL.md +1 -1
- package/agent-skill/codecartographer/references/library.md +2 -2
- package/agent-skill/codecartographer/references/phase-recovery.md +1 -1
- package/dist/core/amendment.d.ts +5 -0
- package/dist/core/amendment.js +21 -2
- package/dist/core/library.d.ts +73 -0
- package/dist/core/library.js +135 -37
- package/dist/core/orchestrator-config.d.ts +6 -0
- package/dist/core/orchestrator-config.js +2 -0
- package/dist/core/status.d.ts +2 -1
- package/dist/core/status.js +19 -5
- package/dist/core/workspace.d.ts +16 -0
- package/dist/core/workspace.js +31 -4
- package/dist/extensions/codecarto/index.js +355 -20
- package/dist/mcp-server/server.js +63 -4
- package/package.json +1 -1
package/dist/core/status.js
CHANGED
|
@@ -126,26 +126,40 @@ export function createEmptyStatus(projectName, pipelinePath, pipeline) {
|
|
|
126
126
|
post_pipeline: [],
|
|
127
127
|
};
|
|
128
128
|
}
|
|
129
|
+
/**
|
|
130
|
+
* Spell a tool for both executable surfaces — the shape the scaffold
|
|
131
|
+
* staleness notice adopted (#177). next_actions is canonical state rendered
|
|
132
|
+
* by codecarto_status on MCP and as the Pi widget's "Next:" line alike, and a
|
|
133
|
+
* Pi user handed only the MCP tool name has nothing to run. Several tools
|
|
134
|
+
* join with "then" so a sequence reads as one per surface. No backticks:
|
|
135
|
+
* these lines render in a plain TUI line and in the HTML dashboard.
|
|
136
|
+
*/
|
|
137
|
+
function onBothSurfaces(...tools) {
|
|
138
|
+
const mcp = tools.map((tool) => `codecarto_${tool}`).join(" then ");
|
|
139
|
+
const pi = tools.map((tool) => `/codecarto-${tool.replace(/_/g, "-")}`).join(" then ");
|
|
140
|
+
return `${mcp} on MCP, ${pi} on Pi`;
|
|
141
|
+
}
|
|
129
142
|
/**
|
|
130
143
|
* Route the terminal boundary to the post-pipeline surfaces (issue #114). The
|
|
131
144
|
* moment every phase completes is exactly when skills, amendments, publishing,
|
|
132
145
|
* and the dashboard apply; the prior static sentence left them undiscovered —
|
|
133
146
|
* the 0.15.0 field test finished two full runs with every one of them unused.
|
|
134
|
-
* Amendment recomputes this list so closure counts never go stale.
|
|
147
|
+
* Amendment recomputes this list so closure counts never go stale. Every tool
|
|
148
|
+
* named here is spelled for both surfaces (see onBothSurfaces).
|
|
135
149
|
*/
|
|
136
150
|
export function buildTerminalNextActions(status) {
|
|
137
151
|
const openQuestions = Object.values(status.phases).reduce((sum, phase) => sum + (phase.open_questions?.length ?? 0), 0);
|
|
138
152
|
const postPipeline = status.post_pipeline.length;
|
|
139
153
|
const actions = [
|
|
140
|
-
|
|
154
|
+
`All phases complete. Review findings; post-pipeline skills: ${onBothSurfaces("list_skills", "skill")}.`,
|
|
141
155
|
];
|
|
142
156
|
if (openQuestions > 0 || postPipeline > 0) {
|
|
143
|
-
actions.push(`${openQuestions} open question(s) and ${postPipeline} post-pipeline item(s) remain — apply resolutions with
|
|
157
|
+
actions.push(`${openQuestions} open question(s) and ${postPipeline} post-pipeline item(s) remain — apply resolutions with ${onBothSurfaces("amend")} (write scratch/amendments/<slug>.yaml from templates/amendment.yaml).`);
|
|
144
158
|
}
|
|
145
159
|
if ("reimplementation-spec" in status.phases) {
|
|
146
|
-
actions.push(
|
|
160
|
+
actions.push(`Publish the finished spec to a library: ${onBothSurfaces("publish")} (create one with ${onBothSurfaces("library_init")}; see the library guide topic).`);
|
|
147
161
|
}
|
|
148
|
-
actions.push(
|
|
162
|
+
actions.push(`Dashboard: .codecarto/dashboard.html (refreshed on completion and amendment; re-render on demand with ${onBothSurfaces("dashboard")}). Usage totals: ${onBothSurfaces("usage")}.`);
|
|
149
163
|
return actions;
|
|
150
164
|
}
|
|
151
165
|
export function normalizeStatus(status, pipeline, pipelinePath, cwd) {
|
package/dist/core/workspace.d.ts
CHANGED
|
@@ -38,6 +38,16 @@ export declare function copyPackagedWorkspace(targetWorkspaceDir: string, source
|
|
|
38
38
|
* @returns the file names created, for the caller's report.
|
|
39
39
|
*/
|
|
40
40
|
export declare function seedOrchestratorFiles(workspaceDir: string): Promise<string[]>;
|
|
41
|
+
/**
|
|
42
|
+
* What a scaffold refresh never touches, for a wrapper that asks before
|
|
43
|
+
* refreshing to show. The same sets drive {@link refreshScaffold}, so the
|
|
44
|
+
* preview and the write cannot disagree.
|
|
45
|
+
*/
|
|
46
|
+
export declare const SCAFFOLD_REFRESH_PROTECTED: Readonly<{
|
|
47
|
+
topLevel: readonly string[];
|
|
48
|
+
dirs: readonly string[];
|
|
49
|
+
workflowFiles: readonly string[];
|
|
50
|
+
}>;
|
|
41
51
|
/** One scaffold refresh's outcome. */
|
|
42
52
|
export type RefreshScaffoldResult = {
|
|
43
53
|
/** Workspace-relative paths written, sorted. */
|
|
@@ -47,6 +57,12 @@ export type RefreshScaffoldResult = {
|
|
|
47
57
|
/** The running framework version the scaffold now matches. */
|
|
48
58
|
scaffoldVersionAfter: string;
|
|
49
59
|
};
|
|
60
|
+
/**
|
|
61
|
+
* The workspace-relative paths a scaffold refresh would write, sorted — the
|
|
62
|
+
* exact set {@link refreshScaffold} copies, computed without writing anything.
|
|
63
|
+
* A wrapper that asks before refreshing shows this.
|
|
64
|
+
*/
|
|
65
|
+
export declare function listScaffoldRefreshFiles(): Promise<string[]>;
|
|
50
66
|
/**
|
|
51
67
|
* Refresh a workspace's framework-owned files from the packaged template
|
|
52
68
|
* (issue #102): both staleness notices instruct exactly this, and the only
|
package/dist/core/workspace.js
CHANGED
|
@@ -199,6 +199,16 @@ const REFRESH_EXCLUDED_TOP_LEVEL = new Set(["BACKLOG.md", "THREAD_LOG.md", "CONV
|
|
|
199
199
|
// generated results) — refresh must never overwrite it.
|
|
200
200
|
const REFRESH_EXCLUDED_DIRS = new Set(["scratch", "inputs", "closeouts", "broadside"]);
|
|
201
201
|
const REFRESH_EXCLUDED_WORKFLOW_FILES = new Set(["status.yaml", "config.yaml", ".usage.local.yaml"]);
|
|
202
|
+
/**
|
|
203
|
+
* What a scaffold refresh never touches, for a wrapper that asks before
|
|
204
|
+
* refreshing to show. The same sets drive {@link refreshScaffold}, so the
|
|
205
|
+
* preview and the write cannot disagree.
|
|
206
|
+
*/
|
|
207
|
+
export const SCAFFOLD_REFRESH_PROTECTED = Object.freeze({
|
|
208
|
+
topLevel: Object.freeze([...REFRESH_EXCLUDED_TOP_LEVEL]),
|
|
209
|
+
dirs: Object.freeze([...REFRESH_EXCLUDED_DIRS]),
|
|
210
|
+
workflowFiles: Object.freeze([...REFRESH_EXCLUDED_WORKFLOW_FILES]),
|
|
211
|
+
});
|
|
202
212
|
async function listTemplateFiles(dir, relativeDir = "") {
|
|
203
213
|
const entries = await readdir(dir, { withFileTypes: true });
|
|
204
214
|
const files = [];
|
|
@@ -218,6 +228,17 @@ async function listTemplateFiles(dir, relativeDir = "") {
|
|
|
218
228
|
}
|
|
219
229
|
return files;
|
|
220
230
|
}
|
|
231
|
+
/**
|
|
232
|
+
* The workspace-relative paths a scaffold refresh would write, sorted — the
|
|
233
|
+
* exact set {@link refreshScaffold} copies, computed without writing anything.
|
|
234
|
+
* A wrapper that asks before refreshing shows this.
|
|
235
|
+
*/
|
|
236
|
+
export async function listScaffoldRefreshFiles() {
|
|
237
|
+
if (!existsSync(packagedWorkspaceDir)) {
|
|
238
|
+
throw new Error("Packaged .codecarto template is missing. Reinstall codecartographer-pi.");
|
|
239
|
+
}
|
|
240
|
+
return (await listTemplateFiles(packagedWorkspaceDir)).sort();
|
|
241
|
+
}
|
|
221
242
|
/**
|
|
222
243
|
* Refresh a workspace's framework-owned files from the packaged template
|
|
223
244
|
* (issue #102): both staleness notices instruct exactly this, and the only
|
|
@@ -238,7 +259,7 @@ export async function refreshScaffold(cwd) {
|
|
|
238
259
|
throw new Error("Packaged .codecarto template is missing. Reinstall codecartographer-pi.");
|
|
239
260
|
}
|
|
240
261
|
const scaffoldVersionBefore = state.scaffoldVersion;
|
|
241
|
-
const files =
|
|
262
|
+
const files = await listScaffoldRefreshFiles();
|
|
242
263
|
for (const relativePath of files) {
|
|
243
264
|
const target = join(state.workspaceDir, relativePath);
|
|
244
265
|
await mkdir(dirname(target), { recursive: true });
|
|
@@ -261,6 +282,12 @@ export async function refreshScaffold(cwd) {
|
|
|
261
282
|
};
|
|
262
283
|
}
|
|
263
284
|
// Numeric x.y.z comparison; null when either side is not a plain dotted triple.
|
|
285
|
+
/**
|
|
286
|
+
* The remedy every staleness notice points at, spelled for both executable
|
|
287
|
+
* surfaces: this text renders inside Pi's widget and inside MCP's status
|
|
288
|
+
* result alike, and a Pi user handed only the MCP tool name has nothing to run.
|
|
289
|
+
*/
|
|
290
|
+
const SCAFFOLD_REFRESH_REMEDY = "`codecarto_refresh_scaffold` on MCP, `/codecarto-refresh-scaffold` on Pi";
|
|
264
291
|
/**
|
|
265
292
|
* Human-readable staleness notice for the workspace's .codecarto/ scaffold,
|
|
266
293
|
* or null when the scaffold matches the running framework. A missing marker
|
|
@@ -272,7 +299,7 @@ export async function refreshScaffold(cwd) {
|
|
|
272
299
|
export function describeScaffoldStaleness(state) {
|
|
273
300
|
const scaffold = state.scaffoldVersion;
|
|
274
301
|
if (!scaffold) {
|
|
275
|
-
return
|
|
302
|
+
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 (${SCAFFOLD_REFRESH_REMEDY}) — that refresh does not touch project state.`;
|
|
276
303
|
}
|
|
277
304
|
const comparison = compareDottedVersions(scaffold, PACKAGE_VERSION);
|
|
278
305
|
if (comparison === 0)
|
|
@@ -280,10 +307,10 @@ export function describeScaffoldStaleness(state) {
|
|
|
280
307
|
if (comparison === null) {
|
|
281
308
|
return scaffold === PACKAGE_VERSION
|
|
282
309
|
? null
|
|
283
|
-
: `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 —
|
|
310
|
+
: `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 (${SCAFFOLD_REFRESH_REMEDY}) — that refresh does not touch project state.`;
|
|
284
311
|
}
|
|
285
312
|
if (comparison < 0) {
|
|
286
|
-
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 —
|
|
313
|
+
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 (${SCAFFOLD_REFRESH_REMEDY}) — that refresh does not touch project state.`;
|
|
287
314
|
}
|
|
288
315
|
return `This workspace's scaffold (v${scaffold}) is newer than the running framework (v${PACKAGE_VERSION}). Upgrade CodeCartographer to at least v${scaffold}.`;
|
|
289
316
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
|
-
import { basename, join, resolve } from "node:path";
|
|
3
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
4
4
|
import { autoCompletePhase, buildAutoSummary, isPhaseRunning, runAuto, runSinglePhase } from "./auto-runner.js";
|
|
5
5
|
import { disposeAgentsWidget } from "./agent-widget.js";
|
|
6
6
|
import { parseDashboardFlags } from "./dashboard-flags.js";
|
|
@@ -9,7 +9,7 @@ import { writeDashboard } from "./dashboard-writer.js";
|
|
|
9
9
|
import { parseBroadsideFlags, KNOWN_BROADSIDE_TOKENS } from "./broadside-flags.js";
|
|
10
10
|
import { parseNextFlags } from "./next-flags.js";
|
|
11
11
|
import { phaseCompactionExtension } from "./phase-compaction.js";
|
|
12
|
-
import { buildPhasePrompt, buildSkillPrompt, buildValidationSummary, canonicalPath, copyPackagedWorkspace, computePerPhaseTotals, computeTotals, ConfidentialityMismatchError, createEmptyStatus, DEFAULT_PIPELINE_PATH, describeScaffoldStaleness, deriveSlug, discoverLibrary, getNextEligiblePhase, getPipelineLabel, getWorkspaceState, isWithinPathResolved, BROADSIDE_LENS_IDS, BROADSIDE_SKILL_NAME, BroadsideCancelledError, broadsideDirFor, collectResultText, estimateSubmitText, getLens, listBatchModels, listSkillNames, loadBroadsideConfig, modelsText, runBroadsideCollect, runBroadsideStatus, runBroadsideSubmit, statusText, loadCodecartoConfig, loadUsage, loadYamlFile, normalizeForComparison, packagedWorkspaceDir, pathExists, PACKAGE_VERSION, readBroadsideSkill, PhasePreflightError, PIPELINE_ALIASES, publishEntry, resolvePhase, resolvePipelineChoice, runPhasePreflight, seedOrchestratorFiles, stringifySimpleYaml, switchPipeline, validatePhaseOutput, writeLibraryConfig, } from "../../core/index.js";
|
|
12
|
+
import { applyAmendment, buildPhasePrompt, buildSkillPrompt, buildValidationSummary, canonicalPath, copyPackagedWorkspace, computePerPhaseTotals, computeTotals, ConfidentialityMismatchError, createEmptyStatus, DEFAULT_PIPELINE_PATH, describeScaffoldStaleness, deriveSlug, discoverLibrary, getNextEligiblePhase, getPipelineLabel, getWorkspaceState, isWithinPathResolved, BROADSIDE_LENS_IDS, BROADSIDE_SKILL_NAME, BroadsideCancelledError, broadsideDirFor, collectResultText, estimateSubmitText, getLens, listAmendmentNames, listBatchModels, listGuideTopics, listScaffoldRefreshFiles, listSkillNames, loadAmendmentFile, loadBroadsideConfig, modelsText, runBroadsideCollect, runBroadsideStatus, runBroadsideSubmit, statusText, loadCodecartoConfig, loadUsage, loadYamlFile, normalizeForComparison, packagedWorkspaceDir, pathExists, PACKAGE_VERSION, readBroadsideSkill, readGuide, refreshScaffold, PhasePreflightError, PIPELINE_ALIASES, publishEntry, resolvePhase, resolvePipelineChoice, resolvePublishSourceRepo, SourceRepoMismatchError, runPhasePreflight, SCAFFOLD_REFRESH_PROTECTED, seedOrchestratorFiles, stringifySimpleYaml, switchPipeline, validatePhaseOutput, writeLibraryConfig, } from "../../core/index.js";
|
|
13
13
|
import { initLibrary } from "../../core/library.js";
|
|
14
14
|
import { resolveUserConfigPath } from "../../core/orchestrator-config.js";
|
|
15
15
|
const STATUS_WIDGET_ID = "codecarto-widget";
|
|
@@ -137,11 +137,130 @@ function describeBroadsideEstimate(estimate) {
|
|
|
137
137
|
lines.push("", "The estimate is a pre-flight prediction from file sizes; OpenRouter bills actual usage.");
|
|
138
138
|
return lines.join("\n");
|
|
139
139
|
}
|
|
140
|
+
/**
|
|
141
|
+
* Resolve the argument of /codecarto-amend to the slug applyAmendment takes.
|
|
142
|
+
* Accepts the slug, `slug.yaml`, or a path to the file — but a path only when
|
|
143
|
+
* it lands inside .codecarto/scratch/amendments/, the one place an amendment
|
|
144
|
+
* is read from. The path names the file; the read always goes through the slug.
|
|
145
|
+
*/
|
|
146
|
+
function resolveAmendmentName(rawArg, cwd) {
|
|
147
|
+
const trimmed = rawArg.trim().replace(/^@/, "");
|
|
148
|
+
if (!trimmed)
|
|
149
|
+
return null;
|
|
150
|
+
if (!/[\\/]/.test(trimmed))
|
|
151
|
+
return trimmed;
|
|
152
|
+
const amendmentsDir = resolve(cwd, ".codecarto", "scratch", "amendments");
|
|
153
|
+
for (const candidate of [resolve(cwd, trimmed), resolve(cwd, ".codecarto", trimmed)]) {
|
|
154
|
+
if (dirname(candidate) === amendmentsDir)
|
|
155
|
+
return basename(candidate);
|
|
156
|
+
}
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
function clipDescription(text, max = 140) {
|
|
160
|
+
if (!text)
|
|
161
|
+
return "";
|
|
162
|
+
const oneLine = text.replace(/\s+/g, " ").trim();
|
|
163
|
+
return `: ${oneLine.length > max ? `${oneLine.slice(0, max - 1)}…` : oneLine}`;
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* What an amendment will do to canonical state, rendered for a human about to
|
|
167
|
+
* approve it: each closure resolved against status.yaml so an id that matches
|
|
168
|
+
* nothing is visible before the write, not only in the result.
|
|
169
|
+
*/
|
|
170
|
+
function describeAmendmentPreview(amendment, state) {
|
|
171
|
+
const openQuestions = new Map();
|
|
172
|
+
for (const [phaseId, phase] of Object.entries(state.status.phases)) {
|
|
173
|
+
for (const entry of phase.open_questions ?? []) {
|
|
174
|
+
if (!entry.id)
|
|
175
|
+
continue;
|
|
176
|
+
openQuestions.set(entry.id, [...(openQuestions.get(entry.id) ?? []), { phaseId, entry }]);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
const unmatched = " — matches nothing (already closed or unknown; reported, not fatal)";
|
|
180
|
+
const lines = [`Amendment file: .codecarto/scratch/amendments/${amendment.slug}.yaml`];
|
|
181
|
+
if (amendment.open_question_closures.length > 0) {
|
|
182
|
+
lines.push("", `Closes ${amendment.open_question_closures.length} open question(s):`);
|
|
183
|
+
for (const id of amendment.open_question_closures) {
|
|
184
|
+
const matches = openQuestions.get(id);
|
|
185
|
+
if (!matches) {
|
|
186
|
+
lines.push(` - ${id}${unmatched}`);
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
const { entry } = matches[0];
|
|
190
|
+
const where = [matches.map((match) => match.phaseId).join(", "), entry.kind].filter(Boolean).join(", ");
|
|
191
|
+
lines.push(` - ${id} (${where})${clipDescription(entry.description)}`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
if (amendment.post_pipeline_closures.length > 0) {
|
|
195
|
+
lines.push("", `Retires ${amendment.post_pipeline_closures.length} post-pipeline item(s):`);
|
|
196
|
+
for (const id of amendment.post_pipeline_closures) {
|
|
197
|
+
const entry = state.status.post_pipeline.find((item) => item.id === id);
|
|
198
|
+
if (!entry) {
|
|
199
|
+
lines.push(` - ${id}${unmatched}`);
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
const where = [entry.kind, entry.source_phase ? `from ${entry.source_phase}` : ""].filter(Boolean).join(", ");
|
|
203
|
+
lines.push(` - ${id}${where ? ` (${where})` : ""}${clipDescription(entry.description)}`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
if (amendment.notes.length > 0) {
|
|
207
|
+
lines.push("", `Records ${amendment.notes.length} note(s) in the closeout:`, ...amendment.notes.map((note) => ` - ${note}`));
|
|
208
|
+
}
|
|
209
|
+
const summary = amendment.closeout_summary.trim();
|
|
210
|
+
lines.push("", `Writes .codecarto/closeouts/<date>-amendment-${amendment.slug}.md, appends one THREAD_LOG entry${summary ? ` ("${summary}")` : ""}, updates workflow/status.yaml under the completion lock, and refreshes the dashboard.`);
|
|
211
|
+
return lines.join("\n");
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* What a scaffold refresh will overwrite, rendered for a human about to approve
|
|
215
|
+
* it. The file set is the one refreshScaffold writes; the protected set is the
|
|
216
|
+
* one it skips — both come from core, so the preview cannot drift from the write.
|
|
217
|
+
*/
|
|
218
|
+
function describeScaffoldRefreshPreview(files, scaffoldVersionBefore) {
|
|
219
|
+
const topLevel = [];
|
|
220
|
+
const byDir = new Map();
|
|
221
|
+
for (const file of files) {
|
|
222
|
+
const slash = file.indexOf("/");
|
|
223
|
+
if (slash === -1) {
|
|
224
|
+
topLevel.push(file);
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
const dir = file.slice(0, slash);
|
|
228
|
+
byDir.set(dir, [...(byDir.get(dir) ?? []), file.slice(slash + 1)]);
|
|
229
|
+
}
|
|
230
|
+
const from = scaffoldVersionBefore ?? "unversioned";
|
|
231
|
+
const lines = [
|
|
232
|
+
from === PACKAGE_VERSION
|
|
233
|
+
? `Scaffold version: ${from} (already current — the files are re-copied from the packaged template byte-for-byte).`
|
|
234
|
+
: `Scaffold version: ${from} → ${PACKAGE_VERSION}.`,
|
|
235
|
+
"",
|
|
236
|
+
`Overwrites ${files.length} framework-owned file(s) in .codecarto/ with the packaged template:`,
|
|
237
|
+
];
|
|
238
|
+
if (topLevel.length > 0)
|
|
239
|
+
lines.push(` ${topLevel.join(", ")}`);
|
|
240
|
+
for (const [dir, entries] of [...byDir.entries()].sort(([a], [b]) => a.localeCompare(b))) {
|
|
241
|
+
// workflow/ is where the pipelines live, and the version marker: name them.
|
|
242
|
+
lines.push(dir === "workflow" ? ` workflow/: ${entries.join(", ")}` : ` ${dir}/: ${entries.length} file(s)`);
|
|
243
|
+
}
|
|
244
|
+
if (byDir.has("findings")) {
|
|
245
|
+
lines.push(" (findings/ refreshes only the packaged SKILL.md, README.md, and pass files — the findings outputs beside them stay.)");
|
|
246
|
+
}
|
|
247
|
+
const protectedPaths = [
|
|
248
|
+
...SCAFFOLD_REFRESH_PROTECTED.workflowFiles.map((file) => `workflow/${file}`),
|
|
249
|
+
...SCAFFOLD_REFRESH_PROTECTED.topLevel,
|
|
250
|
+
...SCAFFOLD_REFRESH_PROTECTED.dirs.map((dir) => `${dir}/`),
|
|
251
|
+
];
|
|
252
|
+
lines.push("", `Never touched: ${protectedPaths.join(", ")}.`, "One THREAD_LOG entry records the refresh. Continue?");
|
|
253
|
+
return lines.join("\n");
|
|
254
|
+
}
|
|
140
255
|
export default function codeCartographerExtension(pi) {
|
|
141
256
|
phaseCompactionExtension(pi);
|
|
142
257
|
let lastFeedbackLines = [];
|
|
143
258
|
let codecartoModeActive = false;
|
|
259
|
+
// Argument completers receive only the prefix, so the session's cwd is
|
|
260
|
+
// remembered here for the completers that list files under .codecarto/.
|
|
261
|
+
let sessionCwd;
|
|
144
262
|
const readWorkspaceState = async (ctx, notifyOnError = true) => {
|
|
263
|
+
sessionCwd = ctx.cwd;
|
|
145
264
|
try {
|
|
146
265
|
return await getWorkspaceState(ctx.cwd);
|
|
147
266
|
}
|
|
@@ -185,6 +304,7 @@ export default function codeCartographerExtension(pi) {
|
|
|
185
304
|
pi.on("session_start", async (_event, ctx) => {
|
|
186
305
|
codecartoModeActive = false;
|
|
187
306
|
lastFeedbackLines = [];
|
|
307
|
+
sessionCwd = ctx.cwd;
|
|
188
308
|
setUiState(ctx, null);
|
|
189
309
|
});
|
|
190
310
|
pi.on("session_shutdown", async () => {
|
|
@@ -698,6 +818,76 @@ export default function codeCartographerExtension(pi) {
|
|
|
698
818
|
ctx.ui.notify(`Queued CodeCartographer skill: ${skillName}`, "info");
|
|
699
819
|
},
|
|
700
820
|
});
|
|
821
|
+
pi.registerCommand("codecarto-list-skills", {
|
|
822
|
+
description: "List the post-pipeline skills installed in .codecarto/skills/ (and the ungated Broad-Side reading guide)",
|
|
823
|
+
handler: async (_args, ctx) => {
|
|
824
|
+
// Same gate as /codecarto-skill: the listing reads the workspace's
|
|
825
|
+
// skills directory, so it needs a workspace — mirrors handleListSkills.
|
|
826
|
+
const state = await ensureWorkspaceState(ctx);
|
|
827
|
+
if (!state)
|
|
828
|
+
return;
|
|
829
|
+
const skills = await listSkillNames(state.workspaceDir);
|
|
830
|
+
const lines = skills.length > 0
|
|
831
|
+
? [`Available skills (${skills.length}):`, ...skills.map((name) => ` - ${name}`)]
|
|
832
|
+
: ["No skills installed."];
|
|
833
|
+
const nextPhase = getNextEligiblePhase(state);
|
|
834
|
+
if (skills.length > 0) {
|
|
835
|
+
lines.push(nextPhase
|
|
836
|
+
? `Post-pipeline skills unlock when the pipeline completes (next phase: ${nextPhase.id}).`
|
|
837
|
+
: "Run one with /codecarto-skill <name>.");
|
|
838
|
+
}
|
|
839
|
+
// Broad-Side is listed apart from the post-pipeline set because it
|
|
840
|
+
// answers to /codecarto-skill without the completion gate.
|
|
841
|
+
const broadsideAvailable = await readBroadsideSkill(ctx.cwd).then(() => true, () => false);
|
|
842
|
+
if (broadsideAvailable) {
|
|
843
|
+
lines.push("", `Also served by /codecarto-skill (not pipeline-gated): ${BROADSIDE_SKILL_NAME} — how to read a Broad-Side batch reconnaissance run.`);
|
|
844
|
+
}
|
|
845
|
+
lastFeedbackLines = lines;
|
|
846
|
+
setUiState(ctx, state, lastFeedbackLines);
|
|
847
|
+
ctx.ui.notify(skills.length > 0
|
|
848
|
+
? `${skills.length} post-pipeline skill${skills.length === 1 ? "" : "s"}: ${skills.join(", ")}`
|
|
849
|
+
: "No post-pipeline skills installed.", "info");
|
|
850
|
+
},
|
|
851
|
+
});
|
|
852
|
+
pi.registerCommand("codecarto-guide", {
|
|
853
|
+
description: "Read the packaged CodeCartographer agent guide into the session: /codecarto-guide [topic]",
|
|
854
|
+
getArgumentCompletions: async (prefix) => {
|
|
855
|
+
const topics = await listGuideTopics().catch(() => ["overview"]);
|
|
856
|
+
const items = topics
|
|
857
|
+
.filter((value) => value.startsWith(prefix))
|
|
858
|
+
.map((value) => ({ value, label: value }));
|
|
859
|
+
return items.length > 0 ? items : null;
|
|
860
|
+
},
|
|
861
|
+
handler: async (args, ctx) => {
|
|
862
|
+
// The guide is packaged with the extension, not copied into a
|
|
863
|
+
// workspace, so — like codecarto_guide — this needs no workspace.
|
|
864
|
+
let document;
|
|
865
|
+
let topics;
|
|
866
|
+
try {
|
|
867
|
+
topics = await listGuideTopics();
|
|
868
|
+
document = await readGuide(args.trim() || undefined);
|
|
869
|
+
}
|
|
870
|
+
catch (error) {
|
|
871
|
+
ctx.ui.notify(error instanceof Error ? error.message : String(error), "error");
|
|
872
|
+
return;
|
|
873
|
+
}
|
|
874
|
+
const other = topics.filter((name) => name !== document.topic);
|
|
875
|
+
const footer = other.length > 0
|
|
876
|
+
? `\n\n---\nOther guide topics: ${other.join(", ")} (run /codecarto-guide <topic>).`
|
|
877
|
+
: "";
|
|
878
|
+
const message = `${document.content}${footer}`;
|
|
879
|
+
if (ctx.isIdle()) {
|
|
880
|
+
pi.sendUserMessage(message);
|
|
881
|
+
}
|
|
882
|
+
else {
|
|
883
|
+
pi.sendUserMessage(message, { deliverAs: "followUp" });
|
|
884
|
+
}
|
|
885
|
+
lastFeedbackLines = [`Queued the CodeCartographer guide: ${document.topic}`];
|
|
886
|
+
if (codecartoModeActive)
|
|
887
|
+
void refreshWorkspaceUi(ctx, lastFeedbackLines);
|
|
888
|
+
ctx.ui.notify(`Queued the CodeCartographer guide (${document.topic})`, "info");
|
|
889
|
+
},
|
|
890
|
+
});
|
|
701
891
|
pi.registerCommand("codecarto-broadside", {
|
|
702
892
|
description: "Batch reconnaissance (Broad-Side): /codecarto-broadside [submit|collect|status|models] [lenses…] [flags]",
|
|
703
893
|
getArgumentCompletions: (prefix) => {
|
|
@@ -886,16 +1076,20 @@ export default function codeCartographerExtension(pi) {
|
|
|
886
1076
|
return;
|
|
887
1077
|
}
|
|
888
1078
|
const spec = await readFile(specPath, "utf8");
|
|
889
|
-
|
|
1079
|
+
// The git remote when there is one, the directory otherwise (#147).
|
|
1080
|
+
// Slug and source_repo derive from the same value so they agree.
|
|
1081
|
+
const source = await resolvePublishSourceRepo(ctx.cwd);
|
|
1082
|
+
const slug = deriveSlug(source.source_repo);
|
|
890
1083
|
const headline = derivePublishHeadline(spec, ctx.cwd);
|
|
891
1084
|
const namespace = marker.namespaced ? config.library.namespace ?? undefined : undefined;
|
|
892
1085
|
if (marker.namespaced && !namespace) {
|
|
893
1086
|
ctx.ui.notify("The configured library is namespaced; set library.namespace before publishing.", "error");
|
|
894
1087
|
return;
|
|
895
1088
|
}
|
|
1089
|
+
const label = `${namespace ? `${namespace}/` : ""}${slug}`;
|
|
896
1090
|
const preview = [
|
|
897
|
-
`Publish ${
|
|
898
|
-
`Source: ${
|
|
1091
|
+
`Publish ${label} to ${config.library.path}`,
|
|
1092
|
+
`Source: ${source.source_repo}${source.remote ? ` (git remote ${source.remote})` : ""}`,
|
|
899
1093
|
`Spec: .codecarto/${phase.primary_output}`,
|
|
900
1094
|
`Headline: ${headline}`,
|
|
901
1095
|
`Provenance: Pi / ${ctx.model?.provider ?? "unknown"} / ${ctx.model?.id ?? "unknown"}`,
|
|
@@ -905,7 +1099,7 @@ export default function codeCartographerExtension(pi) {
|
|
|
905
1099
|
const input = {
|
|
906
1100
|
slug,
|
|
907
1101
|
namespace,
|
|
908
|
-
source_repo:
|
|
1102
|
+
source_repo: source.source_repo,
|
|
909
1103
|
analyzed_at: new Date().toISOString(),
|
|
910
1104
|
pipeline: state.status.pipeline,
|
|
911
1105
|
codecarto_version: PACKAGE_VERSION,
|
|
@@ -915,22 +1109,44 @@ export default function codeCartographerExtension(pi) {
|
|
|
915
1109
|
generation: piGeneration(ctx),
|
|
916
1110
|
};
|
|
917
1111
|
try {
|
|
1112
|
+
// Both guards in publishEntry raise before anything is written, and
|
|
1113
|
+
// each asks a question only the user can answer, so the command has
|
|
1114
|
+
// no flags for them: a yes is the override. The options accumulate,
|
|
1115
|
+
// so a publish that trips both guards asks both questions in turn.
|
|
1116
|
+
const options = {};
|
|
918
1117
|
let result;
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
1118
|
+
while (!result) {
|
|
1119
|
+
try {
|
|
1120
|
+
result = await publishEntry(config.library.path, spec, input, options);
|
|
1121
|
+
}
|
|
1122
|
+
catch (error) {
|
|
1123
|
+
if (error instanceof SourceRepoMismatchError && !options.allowSourceRepoChange) {
|
|
1124
|
+
// The entry's history belongs to whatever the newest version
|
|
1125
|
+
// records. Appending is right only if that repository and this
|
|
1126
|
+
// one are the same project under a new address (#146) — which
|
|
1127
|
+
// includes the first publish after upgrading from a Pi that
|
|
1128
|
+
// recorded the directory to one that records the git remote.
|
|
1129
|
+
const moved = await ctx.ui.confirm("Source repository changed — did it move?", `Library entry ${label} records source_repo "${error.recorded}", but this publish carries "${error.incoming}". If the repository genuinely moved (rename, org transfer, host change — or this is the first publish since CodeCartographer began recording the git remote instead of the local directory), answer yes and this spec is appended as the entry's next version. If these are two different projects that share a directory name, answer no: nothing is written, and the second project needs a distinct slug (codecarto_publish on MCP accepts one). Did the repository move?`);
|
|
1130
|
+
if (!moved) {
|
|
1131
|
+
ctx.ui.notify("Publish cancelled. Nothing was written.", "info");
|
|
1132
|
+
return;
|
|
1133
|
+
}
|
|
1134
|
+
options.allowSourceRepoChange = true;
|
|
1135
|
+
}
|
|
1136
|
+
else if (error instanceof ConfidentialityMismatchError && !options.allowConfidentialityMismatch) {
|
|
1137
|
+
// Pi declares no confidentiality, so the entry sits at the internal
|
|
1138
|
+
// default; whether it may go into a wider library is the user's call.
|
|
1139
|
+
const publishAnyway = await ctx.ui.confirm("Confidentiality mismatch — publish anyway?", `This spec's confidentiality is "${error.entryConfidentiality}" (CodeCartographer's default; /codecarto-publish declares none), but the library "${marker.name}" has visibility "${error.libraryVisibility}". Publishing would expose it to everyone that library reaches. Publish anyway?`);
|
|
1140
|
+
if (!publishAnyway) {
|
|
1141
|
+
ctx.ui.notify("Publish cancelled. Nothing was written.", "info");
|
|
1142
|
+
return;
|
|
1143
|
+
}
|
|
1144
|
+
options.allowConfidentialityMismatch = true;
|
|
1145
|
+
}
|
|
1146
|
+
else {
|
|
1147
|
+
throw error;
|
|
1148
|
+
}
|
|
932
1149
|
}
|
|
933
|
-
result = await publishEntry(config.library.path, spec, input, { allowConfidentialityMismatch: true });
|
|
934
1150
|
}
|
|
935
1151
|
lastFeedbackLines = [`Published ${result.namespace ? `${result.namespace}/` : ""}${result.slug} v${result.version}`, result.isNewVersion ? "New content version." : "Metadata-only update (content unchanged)."];
|
|
936
1152
|
await writeDashboard(ctx.cwd, PACKAGE_VERSION);
|
|
@@ -1069,4 +1285,123 @@ export default function codeCartographerExtension(pi) {
|
|
|
1069
1285
|
ctx.ui.notify("Dashboard regenerated: .codecarto/dashboard.html", "info");
|
|
1070
1286
|
},
|
|
1071
1287
|
});
|
|
1288
|
+
pi.registerCommand("codecarto-refresh-scaffold", {
|
|
1289
|
+
description: "Refresh the framework-owned .codecarto/ files (GUIDE.md, templates/, workflow/ pipelines and VALIDATE.md) from the packaged template, after confirming; project state is untouched",
|
|
1290
|
+
handler: async (_args, ctx) => {
|
|
1291
|
+
const state = await ensureWorkspaceState(ctx);
|
|
1292
|
+
if (!state)
|
|
1293
|
+
return;
|
|
1294
|
+
// Pi can ask, so it shows the exact file set before overwriting
|
|
1295
|
+
// anything — MCP's codecarto_refresh_scaffold writes on call.
|
|
1296
|
+
let files;
|
|
1297
|
+
try {
|
|
1298
|
+
files = await listScaffoldRefreshFiles();
|
|
1299
|
+
}
|
|
1300
|
+
catch (error) {
|
|
1301
|
+
ctx.ui.notify(error instanceof Error ? error.message : String(error), "error");
|
|
1302
|
+
return;
|
|
1303
|
+
}
|
|
1304
|
+
const approved = await ctx.ui.confirm("Refresh the .codecarto/ scaffold from the packaged template?", describeScaffoldRefreshPreview(files, state.scaffoldVersion));
|
|
1305
|
+
if (!approved) {
|
|
1306
|
+
ctx.ui.notify("Scaffold refresh cancelled. Nothing was written.", "info");
|
|
1307
|
+
return;
|
|
1308
|
+
}
|
|
1309
|
+
try {
|
|
1310
|
+
const result = await refreshScaffold(ctx.cwd);
|
|
1311
|
+
const transition = `${result.scaffoldVersionBefore ?? "unversioned"} → ${result.scaffoldVersionAfter}`;
|
|
1312
|
+
lastFeedbackLines = [
|
|
1313
|
+
`Refreshed ${result.written.length} framework-owned file(s) from the packaged template (${transition}).`,
|
|
1314
|
+
"Project state, user config, findings outputs, scratch, closeouts, and orchestrator files were not touched.",
|
|
1315
|
+
"THREAD_LOG.md: one scaffold-refresh entry appended.",
|
|
1316
|
+
];
|
|
1317
|
+
// Re-read state so the widget's staleness line clears with the marker.
|
|
1318
|
+
await refreshWorkspaceUi(ctx, lastFeedbackLines);
|
|
1319
|
+
ctx.ui.notify(`Refreshed ${result.written.length} framework-owned file(s) (${transition}).`, "info");
|
|
1320
|
+
}
|
|
1321
|
+
catch (error) {
|
|
1322
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1323
|
+
lastFeedbackLines = [message];
|
|
1324
|
+
setUiState(ctx, state, lastFeedbackLines);
|
|
1325
|
+
ctx.ui.notify(`Scaffold refresh failed: ${message}`, "error");
|
|
1326
|
+
}
|
|
1327
|
+
},
|
|
1328
|
+
});
|
|
1329
|
+
pi.registerCommand("codecarto-amend", {
|
|
1330
|
+
description: "Apply a post-pipeline amendment from .codecarto/scratch/amendments/, after a preview: /codecarto-amend <name | scratch/amendments/name.yaml>",
|
|
1331
|
+
getArgumentCompletions: async (prefix) => {
|
|
1332
|
+
const names = await listAmendmentNames(join(sessionCwd ?? process.cwd(), ".codecarto"));
|
|
1333
|
+
const items = names
|
|
1334
|
+
.filter((value) => value.startsWith(prefix))
|
|
1335
|
+
.map((value) => ({ value, label: value }));
|
|
1336
|
+
return items.length > 0 ? items : null;
|
|
1337
|
+
},
|
|
1338
|
+
handler: async (args, ctx) => {
|
|
1339
|
+
const state = await ensureWorkspaceState(ctx);
|
|
1340
|
+
if (!state)
|
|
1341
|
+
return;
|
|
1342
|
+
if (!args.trim()) {
|
|
1343
|
+
const staged = await listAmendmentNames(state.workspaceDir);
|
|
1344
|
+
const hint = staged.length > 0
|
|
1345
|
+
? ` (staged: ${staged.join(", ")})`
|
|
1346
|
+
: " — write .codecarto/scratch/amendments/<name>.yaml first (see templates/amendment.yaml)";
|
|
1347
|
+
ctx.ui.notify(`Usage: /codecarto-amend <name>${hint}`, "warning");
|
|
1348
|
+
return;
|
|
1349
|
+
}
|
|
1350
|
+
const name = resolveAmendmentName(args, ctx.cwd);
|
|
1351
|
+
if (!name) {
|
|
1352
|
+
ctx.ui.notify(`Amendments are read from .codecarto/scratch/amendments/ only; pass the amendment name or a path inside that directory, not ${args.trim()}.`, "error");
|
|
1353
|
+
return;
|
|
1354
|
+
}
|
|
1355
|
+
// The same refusals codecarto_amend surfaces, raised before the
|
|
1356
|
+
// confirmation so nobody approves an amendment that cannot apply.
|
|
1357
|
+
let amendment;
|
|
1358
|
+
try {
|
|
1359
|
+
amendment = await loadAmendmentFile(name, state.workspaceDir);
|
|
1360
|
+
}
|
|
1361
|
+
catch (error) {
|
|
1362
|
+
ctx.ui.notify(error instanceof Error ? error.message : String(error), "error");
|
|
1363
|
+
return;
|
|
1364
|
+
}
|
|
1365
|
+
const nextPhase = getNextEligiblePhase(state);
|
|
1366
|
+
if (nextPhase) {
|
|
1367
|
+
ctx.ui.notify(`Cannot amend: the pipeline is not complete (next phase: ${nextPhase.id}). `
|
|
1368
|
+
+ "Resolve open questions and routed items through that phase's handoff (open_question_closures / carry_forward_closures) instead.", "error");
|
|
1369
|
+
return;
|
|
1370
|
+
}
|
|
1371
|
+
// Pi can ask, so the amendment is previewed against status.yaml
|
|
1372
|
+
// before anything is written — MCP's codecarto_amend applies on call.
|
|
1373
|
+
const approved = await ctx.ui.confirm(`Apply amendment "${amendment.slug}"?`, describeAmendmentPreview(amendment, state));
|
|
1374
|
+
if (!approved) {
|
|
1375
|
+
ctx.ui.notify(`Amendment ${amendment.slug} cancelled. Nothing was written.`, "info");
|
|
1376
|
+
return;
|
|
1377
|
+
}
|
|
1378
|
+
try {
|
|
1379
|
+
const { applied, closeoutNotice } = await applyAmendment(ctx.cwd, name);
|
|
1380
|
+
// An amendment exists precisely to change the numbers the dashboard
|
|
1381
|
+
// shows; refresh it, reporting only a render that actually landed.
|
|
1382
|
+
const dashboardWritten = await writeDashboard(ctx.cwd, PACKAGE_VERSION);
|
|
1383
|
+
const lines = [
|
|
1384
|
+
`Amendment applied: ${amendment.slug}`,
|
|
1385
|
+
`Open questions closed: ${applied.openQuestionsClosed.length > 0 ? applied.openQuestionsClosed.join(", ") : "none"}`,
|
|
1386
|
+
`Post-pipeline items closed: ${applied.postPipelineClosed.length > 0 ? applied.postPipelineClosed.join(", ") : "none"}`,
|
|
1387
|
+
];
|
|
1388
|
+
if (applied.unknownIds.length > 0)
|
|
1389
|
+
lines.push(`Ids that matched nothing (already closed or unknown): ${applied.unknownIds.join(", ")}`);
|
|
1390
|
+
lines.push(closeoutNotice);
|
|
1391
|
+
if (dashboardWritten)
|
|
1392
|
+
lines.push("Dashboard refreshed: .codecarto/dashboard.html");
|
|
1393
|
+
lastFeedbackLines = lines;
|
|
1394
|
+
await refreshWorkspaceUi(ctx, lastFeedbackLines);
|
|
1395
|
+
const closed = applied.openQuestionsClosed.length + applied.postPipelineClosed.length;
|
|
1396
|
+
ctx.ui.notify(`Amendment ${amendment.slug} applied: ${applied.openQuestionsClosed.length} open question(s) and ${applied.postPipelineClosed.length} post-pipeline item(s) closed`
|
|
1397
|
+
+ `${applied.unknownIds.length > 0 ? `; ${applied.unknownIds.length} id(s) matched nothing` : ""}.`, closed === 0 || applied.unknownIds.length > 0 ? "warning" : "info");
|
|
1398
|
+
}
|
|
1399
|
+
catch (error) {
|
|
1400
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1401
|
+
lastFeedbackLines = [message];
|
|
1402
|
+
setUiState(ctx, state, lastFeedbackLines);
|
|
1403
|
+
ctx.ui.notify(`Amendment failed: ${message}`, "error");
|
|
1404
|
+
}
|
|
1405
|
+
},
|
|
1406
|
+
});
|
|
1072
1407
|
}
|