codecartographer-pi 0.17.0 → 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 +3 -3
- package/.codecarto/findings/architecture/SKILL.md +1 -0
- package/.codecarto/findings/contracts/SKILL.md +1 -0
- package/.codecarto/findings/defect-scan/SKILL.md +15 -1
- package/.codecarto/findings/defect-scan/passes/01-logic-and-correctness.md +1 -1
- package/.codecarto/findings/defect-scan/passes/02-error-handling.md +1 -1
- package/.codecarto/findings/defect-scan/passes/03-concurrency-and-resources.md +1 -1
- package/.codecarto/findings/defect-scan/passes/04-security-and-trust.md +1 -1
- package/.codecarto/findings/defect-scan/passes/05-api-contract-violations.md +2 -1
- package/.codecarto/findings/defect-scan/passes/06-config-and-environment.md +1 -1
- package/.codecarto/findings/defect-scan-mechanical/SKILL.md +3 -2
- package/.codecarto/findings/defect-scan-semantic/SKILL.md +2 -0
- package/.codecarto/findings/porting/SKILL.md +2 -1
- package/.codecarto/findings/protocols/SKILL.md +1 -0
- package/.codecarto/findings/reimplementation-spec/SKILL.md +1 -1
- package/.codecarto/skills/spec-delta-application/SKILL.md +1 -1
- package/.codecarto/templates/amendment.yaml +3 -3
- package/.codecarto/templates/architecture-map.md +1 -1
- package/.codecarto/templates/defect-report.md +23 -0
- package/.codecarto/templates/mechanical-defects.md +22 -0
- package/.codecarto/templates/reverse-engineering-bundle.md +2 -2
- package/.codecarto/templates/semantic-defects.md +26 -0
- package/.codecarto/templates/spike-report.md +2 -2
- package/.codecarto/workflow/VALIDATE.md +1 -1
- package/.codecarto/workflow/pipeline-defect-scan.yaml +2 -0
- package/.codecarto/workflow/pipeline-full-with-audit.yaml +3 -1
- package/.codecarto/workflow/pipeline-full-with-deep-audit.yaml +5 -1
- package/.codecarto/workflow/pipeline-scout-first.yaml +5 -1
- package/.codecarto/workflow/scaffold-version.yaml +1 -1
- package/README.md +14 -10
- package/agent-skill/codecartographer/SKILL.md +1 -1
- package/agent-skill/codecartographer/references/deep-audit-synthesis.md +4 -1
- package/agent-skill/codecartographer/references/library.md +3 -3
- package/agent-skill/codecartographer/references/orchestration.md +1 -1
- package/agent-skill/codecartographer/references/phase-recovery.md +1 -1
- package/dist/core/amendment.d.ts +5 -0
- package/dist/core/amendment.js +23 -4
- package/dist/core/completion.d.ts +5 -0
- package/dist/core/completion.js +18 -2
- package/dist/core/dashboard.js +5 -3
- package/dist/core/findings.d.ts +59 -0
- package/dist/core/findings.js +145 -0
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.js +1 -0
- package/dist/core/library.d.ts +139 -1
- package/dist/core/library.js +291 -40
- package/dist/core/orchestrator-config.d.ts +6 -0
- package/dist/core/orchestrator-config.js +2 -0
- package/dist/core/pipeline.js +15 -0
- package/dist/core/prompts.js +1 -1
- package/dist/core/status.d.ts +2 -1
- package/dist/core/status.js +33 -11
- package/dist/core/types.d.ts +6 -0
- package/dist/core/utils.d.ts +14 -0
- package/dist/core/utils.js +30 -0
- package/dist/core/workspace.d.ts +16 -0
- package/dist/core/workspace.js +42 -22
- package/dist/core/yaml.js +19 -4
- package/dist/extensions/codecarto/auto-runner.d.ts +2 -0
- package/dist/extensions/codecarto/broadside-flags.d.ts +7 -2
- package/dist/extensions/codecarto/broadside-flags.js +22 -9
- package/dist/extensions/codecarto/dashboard-narrator.js +1 -1
- package/dist/extensions/codecarto/index.js +379 -20
- package/dist/extensions/codecarto/phase-compaction.js +4 -0
- package/dist/mcp-server/server.js +127 -10
- package/package.json +2 -2
package/dist/core/pipeline.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { readFile } from "node:fs/promises";
|
|
3
3
|
import { basename, join } from "node:path";
|
|
4
4
|
import { pathExists } from "./utils.js";
|
|
5
|
+
import { crossCheckFindings, findingsPairingGateActive } from "./findings.js";
|
|
5
6
|
export const PIPELINE_ALIASES = {
|
|
6
7
|
"full-with-audit": "workflow/pipeline-full-with-audit.yaml",
|
|
7
8
|
"full-with-deep-audit": "workflow/pipeline-full-with-deep-audit.yaml",
|
|
@@ -155,6 +156,16 @@ export async function validatePhaseOutput(state, phaseId) {
|
|
|
155
156
|
errors.push("One or more validation criteria are marked FAIL.");
|
|
156
157
|
overall = "FAIL";
|
|
157
158
|
}
|
|
159
|
+
// Findings cross-checks (#122): the validation table says whether criteria
|
|
160
|
+
// were met; these read what the findings' own evidence and action cells
|
|
161
|
+
// say. Deterministic on two cells the model wrote, so the pairing rule can
|
|
162
|
+
// gate — on a scaffold that offers `verify at runtime`. Older scaffolds warn.
|
|
163
|
+
const crossCheck = crossCheckFindings(content, { gate: findingsPairingGateActive(state.scaffoldVersion) });
|
|
164
|
+
if (crossCheck.errors.length > 0) {
|
|
165
|
+
errors.push(...crossCheck.errors);
|
|
166
|
+
overall = "FAIL";
|
|
167
|
+
}
|
|
168
|
+
const warnings = crossCheck.warnings;
|
|
158
169
|
if (overall === "FAIL" && errors.length === 0) {
|
|
159
170
|
errors.push("Validation overall result is FAIL.");
|
|
160
171
|
}
|
|
@@ -169,6 +180,7 @@ export async function validatePhaseOutput(state, phaseId) {
|
|
|
169
180
|
gaps,
|
|
170
181
|
errors,
|
|
171
182
|
secondaryOutputs,
|
|
183
|
+
...(warnings.length > 0 && { warnings }),
|
|
172
184
|
};
|
|
173
185
|
}
|
|
174
186
|
export function buildValidationSummary(validation) {
|
|
@@ -184,6 +196,9 @@ export function buildValidationSummary(validation) {
|
|
|
184
196
|
if (validation.errors.length > 0) {
|
|
185
197
|
lines.push(...validation.errors.slice(0, 3));
|
|
186
198
|
}
|
|
199
|
+
for (const warning of validation.warnings ?? []) {
|
|
200
|
+
lines.push(`NOTE: ${warning} Non-gating.`);
|
|
201
|
+
}
|
|
187
202
|
const missingSecondary = (validation.secondaryOutputs ?? []).filter((output) => !output.exists);
|
|
188
203
|
if (missingSecondary.length > 0) {
|
|
189
204
|
lines.push(`NOTE: ${missingSecondary.length} declared secondary output(s) not written: ${missingSecondary.map((output) => `.codecarto/${output.path}`).join(", ")} — write each, or account for it in Coverage and limits / a routed handoff entry. Non-gating.`);
|
package/dist/core/prompts.js
CHANGED
|
@@ -33,7 +33,7 @@ async function buildOrchestratorDuties(state, phase, auto) {
|
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
35
|
if (retriage.length > 0) {
|
|
36
|
-
lines.push("- Re-triage these open questions' kind labels — a label is a claim needing its own evidence; re-test whether each is now answerable by reading before accepting it:");
|
|
36
|
+
lines.push("- Re-triage these open questions' kind labels — a label is a claim needing its own evidence; re-test whether each is now answerable by reading before accepting it. If one still needs a runtime test, no finding in this phase may assert one of its candidate answers with a settled action (fix before porting / fix now): the finding inherits the question's uncertainty as `verify at runtime` until runtime evidence closes the question:");
|
|
37
37
|
for (const label of retriage.slice(0, RETRIAGE_LIST_LIMIT))
|
|
38
38
|
lines.push(` - ${label}`);
|
|
39
39
|
if (retriage.length > RETRIAGE_LIST_LIMIT)
|
package/dist/core/status.d.ts
CHANGED
|
@@ -14,7 +14,8 @@ export declare function createEmptyStatus(projectName: string, pipelinePath: str
|
|
|
14
14
|
* moment every phase completes is exactly when skills, amendments, publishing,
|
|
15
15
|
* and the dashboard apply; the prior static sentence left them undiscovered —
|
|
16
16
|
* the 0.15.0 field test finished two full runs with every one of them unused.
|
|
17
|
-
* Amendment recomputes this list so closure counts never go stale.
|
|
17
|
+
* Amendment recomputes this list so closure counts never go stale. Every tool
|
|
18
|
+
* named here is spelled for both surfaces (see onBothSurfaces).
|
|
18
19
|
*/
|
|
19
20
|
export declare function buildTerminalNextActions(status: NormalizedStatus): string[];
|
|
20
21
|
export declare function normalizeStatus(status: StatusFile, pipeline: PipelineFile, pipelinePath: string, cwd: string): NormalizedStatus;
|
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) {
|
|
@@ -274,36 +288,44 @@ export function applyHandoff(status, handoff) {
|
|
|
274
288
|
}
|
|
275
289
|
}
|
|
276
290
|
}
|
|
277
|
-
// Now merge into the current phase: overwrite by id or append new
|
|
291
|
+
// Now merge into the current phase: overwrite by id or append new. An entry
|
|
292
|
+
// with neither id nor description has no key to merge on; it is kept as-is
|
|
293
|
+
// rather than lost when the array is rebuilt from the map (#134).
|
|
278
294
|
const localOqMap = new Map();
|
|
295
|
+
const unkeyedOpenQuestions = [];
|
|
279
296
|
for (const entry of phase.open_questions) {
|
|
280
297
|
const key = entry.id || entry.description || "";
|
|
281
298
|
if (key)
|
|
282
299
|
localOqMap.set(key, entry);
|
|
300
|
+
else
|
|
301
|
+
unkeyedOpenQuestions.push(entry);
|
|
283
302
|
}
|
|
284
303
|
for (const entry of handoff.open_questions) {
|
|
285
304
|
const key = entry.id || entry.description || "";
|
|
286
305
|
if (key)
|
|
287
306
|
localOqMap.set(key, entry);
|
|
288
307
|
else
|
|
289
|
-
|
|
308
|
+
unkeyedOpenQuestions.push(entry);
|
|
290
309
|
}
|
|
291
|
-
phase.open_questions = [...localOqMap.values()];
|
|
292
|
-
// Merge carry_forward: overwrite by id or append new
|
|
310
|
+
phase.open_questions = [...localOqMap.values(), ...unkeyedOpenQuestions];
|
|
311
|
+
// Merge carry_forward: overwrite by id or append new, same unkeyed rule
|
|
293
312
|
const cfMap = new Map();
|
|
313
|
+
const unkeyedCarryForward = [];
|
|
294
314
|
for (const entry of phase.carry_forward) {
|
|
295
315
|
const key = entry.id || entry.description || "";
|
|
296
316
|
if (key)
|
|
297
317
|
cfMap.set(key, entry);
|
|
318
|
+
else
|
|
319
|
+
unkeyedCarryForward.push(entry);
|
|
298
320
|
}
|
|
299
321
|
for (const entry of handoff.carry_forward) {
|
|
300
322
|
const key = entry.id || entry.description || "";
|
|
301
323
|
if (key)
|
|
302
324
|
cfMap.set(key, entry);
|
|
303
325
|
else
|
|
304
|
-
|
|
326
|
+
unkeyedCarryForward.push(entry);
|
|
305
327
|
}
|
|
306
|
-
phase.carry_forward = [...cfMap.values()];
|
|
328
|
+
phase.carry_forward = [...cfMap.values(), ...unkeyedCarryForward];
|
|
307
329
|
// Apply closures: remove carry_forward entries from ALL phases by id
|
|
308
330
|
for (const closureId of handoff.carry_forward_closures) {
|
|
309
331
|
if (!closureId)
|
package/dist/core/types.d.ts
CHANGED
|
@@ -94,6 +94,12 @@ export type ValidationResult = {
|
|
|
94
94
|
path: string;
|
|
95
95
|
exists: boolean;
|
|
96
96
|
}>;
|
|
97
|
+
/**
|
|
98
|
+
* Non-gating observations from the findings cross-checks (issue #122):
|
|
99
|
+
* contradictions worth the reader's attention that must not stop an
|
|
100
|
+
* --auto run. Rendered as NOTE lines by buildValidationSummary.
|
|
101
|
+
*/
|
|
102
|
+
warnings?: string[];
|
|
97
103
|
};
|
|
98
104
|
/**
|
|
99
105
|
* One convention a phase proposes for promotion. Completion stages these in
|
package/dist/core/utils.d.ts
CHANGED
|
@@ -15,6 +15,14 @@ export declare function isWithinPathResolved(path: string, root: string): Promis
|
|
|
15
15
|
export declare function isPlainObject(value: unknown): value is Record<string, unknown>;
|
|
16
16
|
export declare function uniqueStrings(items: string[]): string[];
|
|
17
17
|
export declare function dateOnly(timestamp: string): string;
|
|
18
|
+
/**
|
|
19
|
+
* The separator to put before a line appended to file content `current` so
|
|
20
|
+
* the line starts at column 0. A file whose last line lacks a trailing newline
|
|
21
|
+
* (a hand-edited THREAD_LOG.md, an editor that strips final newlines) would
|
|
22
|
+
* otherwise have the appended entry glued onto that line (#134). Empty or
|
|
23
|
+
* absent content needs no separator.
|
|
24
|
+
*/
|
|
25
|
+
export declare function newlineIfUnterminated(current: string): string;
|
|
18
26
|
/**
|
|
19
27
|
* Expand a leading `~` or `~/` to the user's home directory. Node's `path`
|
|
20
28
|
* module deliberately doesn't do this (it's a shell convention, not a path
|
|
@@ -36,3 +44,9 @@ export declare function formatTokenCount(count: number): string;
|
|
|
36
44
|
* dashboard renderer can reuse without crossing the core/extensions boundary.
|
|
37
45
|
*/
|
|
38
46
|
export declare function formatMillis(ms: number): string;
|
|
47
|
+
/**
|
|
48
|
+
* Compare two dotted `major.minor.patch` versions. Returns -1, 0, or 1, or
|
|
49
|
+
* null when either side is not a plain three-part version (pre-release tags,
|
|
50
|
+
* hand-edited markers) so callers can fall back to string equality.
|
|
51
|
+
*/
|
|
52
|
+
export declare function compareDottedVersions(a: string, b: string): number | null;
|
package/dist/core/utils.js
CHANGED
|
@@ -72,6 +72,16 @@ export function uniqueStrings(items) {
|
|
|
72
72
|
export function dateOnly(timestamp) {
|
|
73
73
|
return timestamp.slice(0, 10);
|
|
74
74
|
}
|
|
75
|
+
/**
|
|
76
|
+
* The separator to put before a line appended to file content `current` so
|
|
77
|
+
* the line starts at column 0. A file whose last line lacks a trailing newline
|
|
78
|
+
* (a hand-edited THREAD_LOG.md, an editor that strips final newlines) would
|
|
79
|
+
* otherwise have the appended entry glued onto that line (#134). Empty or
|
|
80
|
+
* absent content needs no separator.
|
|
81
|
+
*/
|
|
82
|
+
export function newlineIfUnterminated(current) {
|
|
83
|
+
return current === "" || current.endsWith("\n") ? "" : "\n";
|
|
84
|
+
}
|
|
75
85
|
/**
|
|
76
86
|
* Expand a leading `~` or `~/` to the user's home directory. Node's `path`
|
|
77
87
|
* module deliberately doesn't do this (it's a shell convention, not a path
|
|
@@ -114,3 +124,23 @@ export function formatMillis(ms) {
|
|
|
114
124
|
const seconds = Math.floor((ms % 60_000) / 1000);
|
|
115
125
|
return `${minutes}m${seconds.toString().padStart(2, "0")}s`;
|
|
116
126
|
}
|
|
127
|
+
/**
|
|
128
|
+
* Compare two dotted `major.minor.patch` versions. Returns -1, 0, or 1, or
|
|
129
|
+
* null when either side is not a plain three-part version (pre-release tags,
|
|
130
|
+
* hand-edited markers) so callers can fall back to string equality.
|
|
131
|
+
*/
|
|
132
|
+
export function compareDottedVersions(a, b) {
|
|
133
|
+
const parse = (version) => {
|
|
134
|
+
const match = /^(\d+)\.(\d+)\.(\d+)$/.exec(version.trim());
|
|
135
|
+
return match ? [Number(match[1]), Number(match[2]), Number(match[3])] : null;
|
|
136
|
+
};
|
|
137
|
+
const left = parse(a);
|
|
138
|
+
const right = parse(b);
|
|
139
|
+
if (!left || !right)
|
|
140
|
+
return null;
|
|
141
|
+
for (let i = 0; i < 3; i++) {
|
|
142
|
+
if (left[i] !== right[i])
|
|
143
|
+
return left[i] < right[i] ? -1 : 1;
|
|
144
|
+
}
|
|
145
|
+
return 0;
|
|
146
|
+
}
|
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
|
@@ -7,7 +7,7 @@ import { appendFile, copyFile, cp, mkdir, readFile, readdir, rename, writeFile }
|
|
|
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";
|
|
10
|
-
import { pathExists } from "./utils.js";
|
|
10
|
+
import { compareDottedVersions, newlineIfUnterminated, pathExists } from "./utils.js";
|
|
11
11
|
import { loadYamlFile, stringifySimpleYaml } from "./yaml.js";
|
|
12
12
|
// Walk up from the current file to find the package root. Needed because the
|
|
13
13
|
// source lives at <root>/core/workspace.ts (one level below the package root)
|
|
@@ -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,14 +259,22 @@ 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 });
|
|
245
266
|
await copyFile(join(packagedWorkspaceDir, relativePath), target);
|
|
246
267
|
}
|
|
247
268
|
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.`;
|
|
248
|
-
|
|
269
|
+
const threadLogPath = join(state.workspaceDir, "THREAD_LOG.md");
|
|
270
|
+
let currentLog = "";
|
|
271
|
+
try {
|
|
272
|
+
currentLog = await readFile(threadLogPath, "utf8");
|
|
273
|
+
}
|
|
274
|
+
catch {
|
|
275
|
+
// Created by the append when absent (pre-template scaffolds).
|
|
276
|
+
}
|
|
277
|
+
await appendFile(threadLogPath, `${newlineIfUnterminated(currentLog)}${entry}\n`, "utf8");
|
|
249
278
|
return {
|
|
250
279
|
written: files,
|
|
251
280
|
...(scaffoldVersionBefore !== undefined && { scaffoldVersionBefore }),
|
|
@@ -253,21 +282,12 @@ export async function refreshScaffold(cwd) {
|
|
|
253
282
|
};
|
|
254
283
|
}
|
|
255
284
|
// Numeric x.y.z comparison; null when either side is not a plain dotted triple.
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
const right = parse(b);
|
|
263
|
-
if (!left || !right)
|
|
264
|
-
return null;
|
|
265
|
-
for (let i = 0; i < 3; i++) {
|
|
266
|
-
if (left[i] !== right[i])
|
|
267
|
-
return left[i] < right[i] ? -1 : 1;
|
|
268
|
-
}
|
|
269
|
-
return 0;
|
|
270
|
-
}
|
|
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";
|
|
271
291
|
/**
|
|
272
292
|
* Human-readable staleness notice for the workspace's .codecarto/ scaffold,
|
|
273
293
|
* or null when the scaffold matches the running framework. A missing marker
|
|
@@ -279,7 +299,7 @@ function compareDottedVersions(a, b) {
|
|
|
279
299
|
export function describeScaffoldStaleness(state) {
|
|
280
300
|
const scaffold = state.scaffoldVersion;
|
|
281
301
|
if (!scaffold) {
|
|
282
|
-
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.`;
|
|
283
303
|
}
|
|
284
304
|
const comparison = compareDottedVersions(scaffold, PACKAGE_VERSION);
|
|
285
305
|
if (comparison === 0)
|
|
@@ -287,10 +307,10 @@ export function describeScaffoldStaleness(state) {
|
|
|
287
307
|
if (comparison === null) {
|
|
288
308
|
return scaffold === PACKAGE_VERSION
|
|
289
309
|
? null
|
|
290
|
-
: `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.`;
|
|
291
311
|
}
|
|
292
312
|
if (comparison < 0) {
|
|
293
|
-
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.`;
|
|
294
314
|
}
|
|
295
315
|
return `This workspace's scaffold (v${scaffold}) is newer than the running framework (v${PACKAGE_VERSION}). Upgrade CodeCartographer to at least v${scaffold}.`;
|
|
296
316
|
}
|
|
@@ -329,7 +349,7 @@ export async function updateStatusAtomically(cwd, updater) {
|
|
|
329
349
|
const normalizedEntry = result.threadLogEntry.trim();
|
|
330
350
|
const isDuplicate = logEntries.some((line) => line.trim() === normalizedEntry);
|
|
331
351
|
if (!isDuplicate) {
|
|
332
|
-
await appendFile(threadLogPath,
|
|
352
|
+
await appendFile(threadLogPath, `${newlineIfUnterminated(currentLog)}${normalizedEntry}\n`, "utf8");
|
|
333
353
|
}
|
|
334
354
|
}
|
|
335
355
|
return nextState;
|
package/dist/core/yaml.js
CHANGED
|
@@ -3,6 +3,18 @@
|
|
|
3
3
|
// Round-trips structured carry_forward/open_questions entries.
|
|
4
4
|
import { readFile } from "node:fs/promises";
|
|
5
5
|
import { isPlainObject } from "./utils.js";
|
|
6
|
+
/**
|
|
7
|
+
* True when the double quote at `index` is escaped, i.e. preceded by an odd
|
|
8
|
+
* run of backslashes. Checking only the single previous character read `\\"`
|
|
9
|
+
* (an escaped backslash, then the closing quote) as an escaped quote, so the
|
|
10
|
+
* scalar never closed and a trailing ` # comment` leaked into the value (#134).
|
|
11
|
+
*/
|
|
12
|
+
function isEscapedQuote(text, index) {
|
|
13
|
+
let backslashes = 0;
|
|
14
|
+
for (let i = index - 1; i >= 0 && text[i] === "\\"; i--)
|
|
15
|
+
backslashes++;
|
|
16
|
+
return backslashes % 2 === 1;
|
|
17
|
+
}
|
|
6
18
|
export function stripYamlComment(value) {
|
|
7
19
|
let inSingle = false;
|
|
8
20
|
let inDouble = false;
|
|
@@ -12,7 +24,7 @@ export function stripYamlComment(value) {
|
|
|
12
24
|
inSingle = !inSingle;
|
|
13
25
|
continue;
|
|
14
26
|
}
|
|
15
|
-
if (char === '"' && !inSingle && value
|
|
27
|
+
if (char === '"' && !inSingle && !isEscapedQuote(value, i)) {
|
|
16
28
|
inDouble = !inDouble;
|
|
17
29
|
continue;
|
|
18
30
|
}
|
|
@@ -49,7 +61,7 @@ function findKeySeparator(text) {
|
|
|
49
61
|
inSingle = !inSingle;
|
|
50
62
|
continue;
|
|
51
63
|
}
|
|
52
|
-
if (char === '"' && !inSingle && text
|
|
64
|
+
if (char === '"' && !inSingle && !isEscapedQuote(text, i)) {
|
|
53
65
|
inDouble = !inDouble;
|
|
54
66
|
continue;
|
|
55
67
|
}
|
|
@@ -77,7 +89,10 @@ export function parseYamlScalar(rawValue) {
|
|
|
77
89
|
return Number.parseInt(trimmed, 10);
|
|
78
90
|
if (/^-?\d+\.\d+$/.test(trimmed))
|
|
79
91
|
return Number.parseFloat(trimmed);
|
|
80
|
-
|
|
92
|
+
// A quoted scalar needs at least its two quotes: a lone quote character
|
|
93
|
+
// satisfied startsWith and endsWith at once and was sliced to "" (#134).
|
|
94
|
+
const canBeQuoted = trimmed.length >= 2;
|
|
95
|
+
if (canBeQuoted && trimmed.startsWith('"') && trimmed.endsWith('"')) {
|
|
81
96
|
try {
|
|
82
97
|
return JSON.parse(trimmed);
|
|
83
98
|
}
|
|
@@ -85,7 +100,7 @@ export function parseYamlScalar(rawValue) {
|
|
|
85
100
|
return trimmed.slice(1, -1);
|
|
86
101
|
}
|
|
87
102
|
}
|
|
88
|
-
if (trimmed.startsWith("'") && trimmed.endsWith("'")) {
|
|
103
|
+
if (canBeQuoted && trimmed.startsWith("'") && trimmed.endsWith("'")) {
|
|
89
104
|
return trimmed.slice(1, -1).replace(/''/g, "'");
|
|
90
105
|
}
|
|
91
106
|
return trimmed;
|
|
@@ -41,6 +41,8 @@ export declare function isPhaseRunning(phaseId: string): boolean;
|
|
|
41
41
|
export interface AutoCompleteResult {
|
|
42
42
|
updatedState: WorkspaceState;
|
|
43
43
|
closeoutNotice?: string;
|
|
44
|
+
/** Non-gating closure-integrity notes from completion (#122). */
|
|
45
|
+
warnings: string[];
|
|
44
46
|
}
|
|
45
47
|
export declare function autoCompletePhase(ctx: ExtensionContext, validation: ValidationResult): Promise<AutoCompleteResult>;
|
|
46
48
|
export type AutoOutcome = "complete" | "stopped" | "aborted";
|
|
@@ -4,7 +4,12 @@ export interface BroadsideFlags {
|
|
|
4
4
|
action: BroadsideAction;
|
|
5
5
|
/** Empty means "the repository's default lens set". */
|
|
6
6
|
lenses: BroadsideLensId[];
|
|
7
|
-
|
|
7
|
+
/**
|
|
8
|
+
* Undefined means "use the repository's config default". --incremental sets
|
|
9
|
+
* true and --no-incremental sets false, so a config-set `incremental: true`
|
|
10
|
+
* can be overridden back to a full scan for one run (#163).
|
|
11
|
+
*/
|
|
12
|
+
incremental?: boolean;
|
|
8
13
|
includeSynthesis?: boolean;
|
|
9
14
|
includeTriage?: boolean;
|
|
10
15
|
retryTruncated?: boolean;
|
|
@@ -17,5 +22,5 @@ export interface BroadsideFlags {
|
|
|
17
22
|
error?: string;
|
|
18
23
|
}
|
|
19
24
|
/** Every token the completer offers, in the order it offers them. */
|
|
20
|
-
export declare const KNOWN_BROADSIDE_TOKENS: readonly ["submit", "collect", "status", "models", "architecture", "api", "security", "defect", "conventions", "porting", "--incremental", "--max-cost=", "--wait=", "--no-synthesis", "--no-triage", "--no-retry-truncated", "--benchmarks"];
|
|
25
|
+
export declare const KNOWN_BROADSIDE_TOKENS: readonly ["submit", "collect", "status", "models", "architecture", "api", "security", "defect", "conventions", "porting", "--incremental", "--no-incremental", "--max-cost=", "--wait=", "--no-synthesis", "--no-triage", "--no-retry-truncated", "--benchmarks"];
|
|
21
26
|
export declare function parseBroadsideFlags(args: string): BroadsideFlags;
|
|
@@ -10,9 +10,14 @@
|
|
|
10
10
|
// Flags mirror the codecarto_broadside tool parameters, with the negative
|
|
11
11
|
// forms spelled out because a slash command has no place to pass `false`:
|
|
12
12
|
// --incremental --no-synthesis
|
|
13
|
-
// --
|
|
14
|
-
// --
|
|
15
|
-
// --benchmarks (models only)
|
|
13
|
+
// --no-incremental --no-triage
|
|
14
|
+
// --max-cost=N --no-retry-truncated
|
|
15
|
+
// --wait=SECONDS --benchmarks (models only)
|
|
16
|
+
//
|
|
17
|
+
// --incremental has a spelled-out negative because the value is tri-state:
|
|
18
|
+
// absent defers to config.yaml, so a repository that set `incremental: true`
|
|
19
|
+
// can still ask for a one-off full scan (#163), exactly as an MCP caller can
|
|
20
|
+
// with `incremental: false`.
|
|
16
21
|
//
|
|
17
22
|
// The parser never throws. index.ts decides how to surface unknown tokens and
|
|
18
23
|
// invalid combinations, matching parseNextFlags.
|
|
@@ -26,6 +31,7 @@ export const KNOWN_BROADSIDE_TOKENS = [
|
|
|
26
31
|
"models",
|
|
27
32
|
...BROADSIDE_LENS_IDS,
|
|
28
33
|
"--incremental",
|
|
34
|
+
"--no-incremental",
|
|
29
35
|
"--max-cost=",
|
|
30
36
|
"--wait=",
|
|
31
37
|
"--no-synthesis",
|
|
@@ -50,7 +56,6 @@ export function parseBroadsideFlags(args) {
|
|
|
50
56
|
const result = {
|
|
51
57
|
action: "submit",
|
|
52
58
|
lenses: [],
|
|
53
|
-
incremental: false,
|
|
54
59
|
benchmarks: false,
|
|
55
60
|
unknown: [],
|
|
56
61
|
};
|
|
@@ -67,8 +72,14 @@ export function parseBroadsideFlags(args) {
|
|
|
67
72
|
result.lenses.push(token);
|
|
68
73
|
continue;
|
|
69
74
|
}
|
|
70
|
-
if (token === "--incremental") {
|
|
71
|
-
|
|
75
|
+
if (token === "--incremental" || token === "--no-incremental") {
|
|
76
|
+
const value = token === "--incremental";
|
|
77
|
+
// Last-one-wins would be a silent tiebreak on a command that spends
|
|
78
|
+
// money; a contradiction is an error.
|
|
79
|
+
if (result.incremental !== undefined && result.incremental !== value) {
|
|
80
|
+
result.error ??= "--incremental and --no-incremental contradict each other; pass one or neither.";
|
|
81
|
+
}
|
|
82
|
+
result.incremental = value;
|
|
72
83
|
continue;
|
|
73
84
|
}
|
|
74
85
|
if (token === "--no-synthesis") {
|
|
@@ -99,12 +110,14 @@ export function parseBroadsideFlags(args) {
|
|
|
99
110
|
}
|
|
100
111
|
// Flags that only mean something for one action are refused rather than
|
|
101
112
|
// ignored: silently dropping --incremental on a collect would read as
|
|
102
|
-
// "collected incrementally", which is not a thing
|
|
113
|
+
// "collected incrementally", which is not a thing (and --no-incremental
|
|
114
|
+
// would read as a full re-collect, which is not one either).
|
|
103
115
|
if (result.lenses.length > 0 && result.action !== "submit") {
|
|
104
116
|
result.error ??= `Lens names are only meaningful for submit (got action "${result.action}").`;
|
|
105
117
|
}
|
|
106
|
-
if (result.incremental && result.action !== "submit") {
|
|
107
|
-
result.
|
|
118
|
+
if (result.incremental !== undefined && result.action !== "submit") {
|
|
119
|
+
const flag = result.incremental ? "--incremental" : "--no-incremental";
|
|
120
|
+
result.error ??= `${flag} is only meaningful for submit (got action "${result.action}").`;
|
|
108
121
|
}
|
|
109
122
|
if (result.benchmarks && result.action !== "models") {
|
|
110
123
|
result.error ??= `--benchmarks is only meaningful for models (got action "${result.action}").`;
|
|
@@ -66,7 +66,7 @@ async function readRecentCloseouts(workspaceDir) {
|
|
|
66
66
|
return m ? { date: m[1], phaseOrModule: m[2], fileName: name } : null;
|
|
67
67
|
})
|
|
68
68
|
.filter((x) => x !== null)
|
|
69
|
-
.sort((a, b) => (a.date < b.date ? 1 : -1))
|
|
69
|
+
.sort((a, b) => (a.date < b.date ? 1 : a.date > b.date ? -1 : 0))
|
|
70
70
|
.slice(0, MAX_CLOSEOUTS);
|
|
71
71
|
const out = [];
|
|
72
72
|
for (const entry of matched) {
|