codecartographer-pi 0.17.1 → 0.19.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 +9 -2
- package/.codecarto/skills/spec-delta-application/SKILL.md +1 -1
- package/.codecarto/templates/amendment.yaml +3 -3
- package/.codecarto/templates/phase-handoff.yaml +20 -1
- 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/handoff-contract.md +30 -2
- 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/completion.d.ts +15 -0
- package/dist/core/completion.js +80 -3
- package/dist/core/coverage.d.ts +45 -0
- package/dist/core/coverage.js +131 -0
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.js +1 -0
- 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/prompts.js +19 -1
- package/dist/core/status.d.ts +10 -2
- package/dist/core/status.js +56 -7
- package/dist/core/types.d.ts +24 -1
- 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
|
@@ -36,8 +36,42 @@ function coerceEntry(value, allowTargetPhase) {
|
|
|
36
36
|
entry.deferred_reason = raw.deferred_reason.trim();
|
|
37
37
|
if (allowTargetPhase && typeof raw.target_phase === "string" && raw.target_phase.trim())
|
|
38
38
|
entry.target_phase = raw.target_phase.trim();
|
|
39
|
+
// derives_from rides the same flag as target_phase: it is a carry-forward
|
|
40
|
+
// concept only — the id of the open question this routed item answers one
|
|
41
|
+
// candidate of (#122, #186). An open_questions entry has nothing to derive
|
|
42
|
+
// from, so the field is dropped there rather than silently carried.
|
|
43
|
+
if (allowTargetPhase && typeof raw.derives_from === "string" && raw.derives_from.trim())
|
|
44
|
+
entry.derives_from = raw.derives_from.trim();
|
|
39
45
|
return Object.keys(entry).length > 0 ? entry : null;
|
|
40
46
|
}
|
|
47
|
+
/**
|
|
48
|
+
* Normalize a handoff's `open_question_closures` (#122, #186). Accepts both
|
|
49
|
+
* the original bare-string shape and `{ id, evidence }`; a string becomes
|
|
50
|
+
* `{ id }`, and an entry with no usable id is dropped rather than resolving
|
|
51
|
+
* nothing under the lock. Values are trimmed.
|
|
52
|
+
*/
|
|
53
|
+
export function ensureClosureArray(value) {
|
|
54
|
+
if (!Array.isArray(value))
|
|
55
|
+
return [];
|
|
56
|
+
const result = [];
|
|
57
|
+
for (const item of value) {
|
|
58
|
+
if (typeof item === "string") {
|
|
59
|
+
const id = item.trim();
|
|
60
|
+
if (id)
|
|
61
|
+
result.push({ id });
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (!item || typeof item !== "object" || Array.isArray(item))
|
|
65
|
+
continue;
|
|
66
|
+
const raw = item;
|
|
67
|
+
const id = typeof raw.id === "string" ? raw.id.trim() : "";
|
|
68
|
+
if (!id)
|
|
69
|
+
continue;
|
|
70
|
+
const evidence = typeof raw.evidence === "string" && raw.evidence.trim() ? raw.evidence.trim() : undefined;
|
|
71
|
+
result.push({ id, ...(evidence !== undefined && { evidence }) });
|
|
72
|
+
}
|
|
73
|
+
return result;
|
|
74
|
+
}
|
|
41
75
|
export function ensureEntryArray(value, allowTargetPhase = false) {
|
|
42
76
|
if (!Array.isArray(value))
|
|
43
77
|
return [];
|
|
@@ -126,26 +160,40 @@ export function createEmptyStatus(projectName, pipelinePath, pipeline) {
|
|
|
126
160
|
post_pipeline: [],
|
|
127
161
|
};
|
|
128
162
|
}
|
|
163
|
+
/**
|
|
164
|
+
* Spell a tool for both executable surfaces — the shape the scaffold
|
|
165
|
+
* staleness notice adopted (#177). next_actions is canonical state rendered
|
|
166
|
+
* by codecarto_status on MCP and as the Pi widget's "Next:" line alike, and a
|
|
167
|
+
* Pi user handed only the MCP tool name has nothing to run. Several tools
|
|
168
|
+
* join with "then" so a sequence reads as one per surface. No backticks:
|
|
169
|
+
* these lines render in a plain TUI line and in the HTML dashboard.
|
|
170
|
+
*/
|
|
171
|
+
function onBothSurfaces(...tools) {
|
|
172
|
+
const mcp = tools.map((tool) => `codecarto_${tool}`).join(" then ");
|
|
173
|
+
const pi = tools.map((tool) => `/codecarto-${tool.replace(/_/g, "-")}`).join(" then ");
|
|
174
|
+
return `${mcp} on MCP, ${pi} on Pi`;
|
|
175
|
+
}
|
|
129
176
|
/**
|
|
130
177
|
* Route the terminal boundary to the post-pipeline surfaces (issue #114). The
|
|
131
178
|
* moment every phase completes is exactly when skills, amendments, publishing,
|
|
132
179
|
* and the dashboard apply; the prior static sentence left them undiscovered —
|
|
133
180
|
* 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.
|
|
181
|
+
* Amendment recomputes this list so closure counts never go stale. Every tool
|
|
182
|
+
* named here is spelled for both surfaces (see onBothSurfaces).
|
|
135
183
|
*/
|
|
136
184
|
export function buildTerminalNextActions(status) {
|
|
137
185
|
const openQuestions = Object.values(status.phases).reduce((sum, phase) => sum + (phase.open_questions?.length ?? 0), 0);
|
|
138
186
|
const postPipeline = status.post_pipeline.length;
|
|
139
187
|
const actions = [
|
|
140
|
-
|
|
188
|
+
`All phases complete. Review findings; post-pipeline skills: ${onBothSurfaces("list_skills", "skill")}.`,
|
|
141
189
|
];
|
|
142
190
|
if (openQuestions > 0 || postPipeline > 0) {
|
|
143
|
-
actions.push(`${openQuestions} open question(s) and ${postPipeline} post-pipeline item(s) remain — apply resolutions with
|
|
191
|
+
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
192
|
}
|
|
145
193
|
if ("reimplementation-spec" in status.phases) {
|
|
146
|
-
actions.push(
|
|
194
|
+
actions.push(`Publish the finished spec to a library: ${onBothSurfaces("publish")} (create one with ${onBothSurfaces("library_init")}; see the library guide topic).`);
|
|
147
195
|
}
|
|
148
|
-
actions.push(
|
|
196
|
+
actions.push(`Dashboard: .codecarto/dashboard.html (refreshed on completion and amendment; re-render on demand with ${onBothSurfaces("dashboard")}). Usage totals: ${onBothSurfaces("usage")}.`);
|
|
149
197
|
return actions;
|
|
150
198
|
}
|
|
151
199
|
export function normalizeStatus(status, pipeline, pipelinePath, cwd) {
|
|
@@ -204,7 +252,7 @@ export function parseHandoff(value) {
|
|
|
204
252
|
open_questions: openQuestions,
|
|
205
253
|
carry_forward: carryForward,
|
|
206
254
|
carry_forward_closures: ensureArray(raw.carry_forward_closures),
|
|
207
|
-
open_question_closures:
|
|
255
|
+
open_question_closures: ensureClosureArray(raw.open_question_closures),
|
|
208
256
|
post_pipeline: ensurePostPipelineArray(raw.post_pipeline),
|
|
209
257
|
decisions: ensureArray(raw.decisions),
|
|
210
258
|
proposed_conventions: ensureProposedConventionArray(raw.proposed_conventions),
|
|
@@ -321,7 +369,8 @@ export function applyHandoff(status, handoff) {
|
|
|
321
369
|
}
|
|
322
370
|
}
|
|
323
371
|
// Apply open_question_closures: remove resolved questions from ALL phases by id
|
|
324
|
-
for (const
|
|
372
|
+
for (const closure of handoff.open_question_closures) {
|
|
373
|
+
const closureId = closure?.id;
|
|
325
374
|
if (!closureId)
|
|
326
375
|
continue;
|
|
327
376
|
for (const ph of Object.values(status.phases)) {
|
package/dist/core/types.d.ts
CHANGED
|
@@ -9,6 +9,16 @@ export type OpenQuestionEntry = {
|
|
|
9
9
|
};
|
|
10
10
|
export type CarryForwardEntry = OpenQuestionEntry & {
|
|
11
11
|
target_phase?: string;
|
|
12
|
+
/**
|
|
13
|
+
* Optional id of the `open_questions` entry this routed item is one
|
|
14
|
+
* candidate answer to (#122, #186). The upstream phase usually registers
|
|
15
|
+
* the question and routes the candidate onward in the same handoff, which
|
|
16
|
+
* is the cheapest moment to record the link. Completion refuses a closure
|
|
17
|
+
* of this entry while that question is still open and is not closed by the
|
|
18
|
+
* same handoff: closing a routed item does not settle the question it came
|
|
19
|
+
* from. Omitted on every entry that predates the field.
|
|
20
|
+
*/
|
|
21
|
+
derives_from?: string;
|
|
12
22
|
};
|
|
13
23
|
export type PostPipelineEntry = OpenQuestionEntry & {
|
|
14
24
|
source_phase?: string;
|
|
@@ -114,6 +124,18 @@ export type ProposedConventionEntry = {
|
|
|
114
124
|
/** Optional: where the pattern showed up (file, phase, incident). */
|
|
115
125
|
evidence?: string;
|
|
116
126
|
};
|
|
127
|
+
/**
|
|
128
|
+
* One claimed closure in a handoff's `open_question_closures` (#122, #186).
|
|
129
|
+
* A bare string — the only shape before this field grew — normalizes to
|
|
130
|
+
* `{ id }`; the object form adds the evidence that settles the question.
|
|
131
|
+
* Completion requires non-empty `evidence` when the closed question's `kind`
|
|
132
|
+
* is `needs-runtime-test`, because such a question closes on runtime evidence
|
|
133
|
+
* rather than on another source read.
|
|
134
|
+
*/
|
|
135
|
+
export type ClosureEntry = {
|
|
136
|
+
id: string;
|
|
137
|
+
evidence?: string;
|
|
138
|
+
};
|
|
117
139
|
export type PhaseHandoff = {
|
|
118
140
|
phase_id: string;
|
|
119
141
|
/**
|
|
@@ -126,7 +148,8 @@ export type PhaseHandoff = {
|
|
|
126
148
|
open_questions: OpenQuestionEntry[];
|
|
127
149
|
carry_forward: CarryForwardEntry[];
|
|
128
150
|
carry_forward_closures: string[];
|
|
129
|
-
|
|
151
|
+
/** ids to resolve and remove from all phases; a bare string parses to `{ id }`. */
|
|
152
|
+
open_question_closures: ClosureEntry[];
|
|
130
153
|
post_pipeline: PostPipelineEntry[];
|
|
131
154
|
decisions: string[];
|
|
132
155
|
/** Conventions proposed for promotion; completion stages them in CONVENTIONS.md. Omitted defaults to empty. */
|
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
|
}
|