codecartographer-pi 0.17.0 → 0.17.1
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 +1 -1
- 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/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/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 +4 -4
- package/agent-skill/codecartographer/references/deep-audit-synthesis.md +4 -1
- package/agent-skill/codecartographer/references/library.md +1 -1
- package/agent-skill/codecartographer/references/orchestration.md +1 -1
- package/dist/core/amendment.js +2 -2
- 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 +66 -1
- package/dist/core/library.js +161 -8
- package/dist/core/pipeline.js +15 -0
- package/dist/core/prompts.js +1 -1
- package/dist/core/status.js +14 -6
- 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.js +11 -18
- 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 +40 -16
- package/dist/extensions/codecarto/phase-compaction.js +4 -0
- package/dist/mcp-server/server.js +65 -7
- 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.js
CHANGED
|
@@ -274,36 +274,44 @@ export function applyHandoff(status, handoff) {
|
|
|
274
274
|
}
|
|
275
275
|
}
|
|
276
276
|
}
|
|
277
|
-
// Now merge into the current phase: overwrite by id or append new
|
|
277
|
+
// Now merge into the current phase: overwrite by id or append new. An entry
|
|
278
|
+
// with neither id nor description has no key to merge on; it is kept as-is
|
|
279
|
+
// rather than lost when the array is rebuilt from the map (#134).
|
|
278
280
|
const localOqMap = new Map();
|
|
281
|
+
const unkeyedOpenQuestions = [];
|
|
279
282
|
for (const entry of phase.open_questions) {
|
|
280
283
|
const key = entry.id || entry.description || "";
|
|
281
284
|
if (key)
|
|
282
285
|
localOqMap.set(key, entry);
|
|
286
|
+
else
|
|
287
|
+
unkeyedOpenQuestions.push(entry);
|
|
283
288
|
}
|
|
284
289
|
for (const entry of handoff.open_questions) {
|
|
285
290
|
const key = entry.id || entry.description || "";
|
|
286
291
|
if (key)
|
|
287
292
|
localOqMap.set(key, entry);
|
|
288
293
|
else
|
|
289
|
-
|
|
294
|
+
unkeyedOpenQuestions.push(entry);
|
|
290
295
|
}
|
|
291
|
-
phase.open_questions = [...localOqMap.values()];
|
|
292
|
-
// Merge carry_forward: overwrite by id or append new
|
|
296
|
+
phase.open_questions = [...localOqMap.values(), ...unkeyedOpenQuestions];
|
|
297
|
+
// Merge carry_forward: overwrite by id or append new, same unkeyed rule
|
|
293
298
|
const cfMap = new Map();
|
|
299
|
+
const unkeyedCarryForward = [];
|
|
294
300
|
for (const entry of phase.carry_forward) {
|
|
295
301
|
const key = entry.id || entry.description || "";
|
|
296
302
|
if (key)
|
|
297
303
|
cfMap.set(key, entry);
|
|
304
|
+
else
|
|
305
|
+
unkeyedCarryForward.push(entry);
|
|
298
306
|
}
|
|
299
307
|
for (const entry of handoff.carry_forward) {
|
|
300
308
|
const key = entry.id || entry.description || "";
|
|
301
309
|
if (key)
|
|
302
310
|
cfMap.set(key, entry);
|
|
303
311
|
else
|
|
304
|
-
|
|
312
|
+
unkeyedCarryForward.push(entry);
|
|
305
313
|
}
|
|
306
|
-
phase.carry_forward = [...cfMap.values()];
|
|
314
|
+
phase.carry_forward = [...cfMap.values(), ...unkeyedCarryForward];
|
|
307
315
|
// Apply closures: remove carry_forward entries from ALL phases by id
|
|
308
316
|
for (const closureId of handoff.carry_forward_closures) {
|
|
309
317
|
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.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)
|
|
@@ -245,7 +245,15 @@ export async function refreshScaffold(cwd) {
|
|
|
245
245
|
await copyFile(join(packagedWorkspaceDir, relativePath), target);
|
|
246
246
|
}
|
|
247
247
|
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
|
-
|
|
248
|
+
const threadLogPath = join(state.workspaceDir, "THREAD_LOG.md");
|
|
249
|
+
let currentLog = "";
|
|
250
|
+
try {
|
|
251
|
+
currentLog = await readFile(threadLogPath, "utf8");
|
|
252
|
+
}
|
|
253
|
+
catch {
|
|
254
|
+
// Created by the append when absent (pre-template scaffolds).
|
|
255
|
+
}
|
|
256
|
+
await appendFile(threadLogPath, `${newlineIfUnterminated(currentLog)}${entry}\n`, "utf8");
|
|
249
257
|
return {
|
|
250
258
|
written: files,
|
|
251
259
|
...(scaffoldVersionBefore !== undefined && { scaffoldVersionBefore }),
|
|
@@ -253,21 +261,6 @@ export async function refreshScaffold(cwd) {
|
|
|
253
261
|
};
|
|
254
262
|
}
|
|
255
263
|
// Numeric x.y.z comparison; null when either side is not a plain dotted triple.
|
|
256
|
-
function compareDottedVersions(a, b) {
|
|
257
|
-
const parse = (version) => {
|
|
258
|
-
const match = /^(\d+)\.(\d+)\.(\d+)$/.exec(version.trim());
|
|
259
|
-
return match ? [Number(match[1]), Number(match[2]), Number(match[3])] : null;
|
|
260
|
-
};
|
|
261
|
-
const left = parse(a);
|
|
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
|
-
}
|
|
271
264
|
/**
|
|
272
265
|
* Human-readable staleness notice for the workspace's .codecarto/ scaffold,
|
|
273
266
|
* or null when the scaffold matches the running framework. A missing marker
|
|
@@ -329,7 +322,7 @@ export async function updateStatusAtomically(cwd, updater) {
|
|
|
329
322
|
const normalizedEntry = result.threadLogEntry.trim();
|
|
330
323
|
const isDuplicate = logEntries.some((line) => line.trim() === normalizedEntry);
|
|
331
324
|
if (!isDuplicate) {
|
|
332
|
-
await appendFile(threadLogPath,
|
|
325
|
+
await appendFile(threadLogPath, `${newlineIfUnterminated(currentLog)}${normalizedEntry}\n`, "utf8");
|
|
333
326
|
}
|
|
334
327
|
}
|
|
335
328
|
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) {
|
|
@@ -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, 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 { 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";
|
|
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";
|
|
@@ -618,7 +618,7 @@ export default function codeCartographerExtension(pi) {
|
|
|
618
618
|
ctx.ui.notify(`Cannot complete ${validation.phaseId}: ${validation.overall}`, "error");
|
|
619
619
|
return;
|
|
620
620
|
}
|
|
621
|
-
const { updatedState, closeoutNotice } = await autoCompletePhase(ctx, validation);
|
|
621
|
+
const { updatedState, closeoutNotice, warnings } = await autoCompletePhase(ctx, validation);
|
|
622
622
|
lastFeedbackLines = [
|
|
623
623
|
`Completed phase: ${validation.phaseId}`,
|
|
624
624
|
`Validation: ${validation.overall}`,
|
|
@@ -626,10 +626,15 @@ export default function codeCartographerExtension(pi) {
|
|
|
626
626
|
];
|
|
627
627
|
if (closeoutNotice)
|
|
628
628
|
lastFeedbackLines.push(closeoutNotice);
|
|
629
|
+
const notes = [...(validation.warnings ?? []), ...warnings];
|
|
630
|
+
for (const note of notes)
|
|
631
|
+
lastFeedbackLines.push(`NOTE: ${note} Non-gating.`);
|
|
629
632
|
setUiState(ctx, updatedState, lastFeedbackLines);
|
|
630
|
-
ctx.ui.notify(`Marked ${validation.phaseId} complete`, validation.overall === "PASS WITH GAPS" ? "warning" : "info");
|
|
633
|
+
ctx.ui.notify(`Marked ${validation.phaseId} complete`, validation.overall === "PASS WITH GAPS" || notes.length > 0 ? "warning" : "info");
|
|
631
634
|
if (closeoutNotice)
|
|
632
635
|
ctx.ui.notify(closeoutNotice, "info");
|
|
636
|
+
for (const note of notes)
|
|
637
|
+
ctx.ui.notify(note, "warning");
|
|
633
638
|
},
|
|
634
639
|
});
|
|
635
640
|
pi.registerCommand("codecarto-skill", {
|
|
@@ -786,7 +791,9 @@ export default function codeCartographerExtension(pi) {
|
|
|
786
791
|
lenses,
|
|
787
792
|
model: config.model,
|
|
788
793
|
maxCost: flags.maxCost ?? config.maxCost,
|
|
789
|
-
|
|
794
|
+
// `??`, not `||`: --no-incremental parses to false and must beat a
|
|
795
|
+
// config-set true, exactly as MCP's `incremental: false` does (#163).
|
|
796
|
+
incremental: flags.incremental ?? config.incremental,
|
|
790
797
|
// Pi can ask, so it asks instead of refusing over max_cost the
|
|
791
798
|
// way MCP has to. An approval here IS the force flag.
|
|
792
799
|
confirm: (estimate) => ctx.ui.confirm(`Broad-Side will spend about $${estimate.totalCost.toFixed(4)}`, describeBroadsideEstimate(estimate)),
|
|
@@ -895,19 +902,36 @@ export default function codeCartographerExtension(pi) {
|
|
|
895
902
|
].join("\n");
|
|
896
903
|
if (config.library.publish_confirm && !(await ctx.ui.confirm("Publish reimplementation spec", preview)))
|
|
897
904
|
return;
|
|
905
|
+
const input = {
|
|
906
|
+
slug,
|
|
907
|
+
namespace,
|
|
908
|
+
source_repo: ctx.cwd,
|
|
909
|
+
analyzed_at: new Date().toISOString(),
|
|
910
|
+
pipeline: state.status.pipeline,
|
|
911
|
+
codecarto_version: PACKAGE_VERSION,
|
|
912
|
+
headline,
|
|
913
|
+
tags: [],
|
|
914
|
+
capabilities: [],
|
|
915
|
+
generation: piGeneration(ctx),
|
|
916
|
+
};
|
|
898
917
|
try {
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
918
|
+
let result;
|
|
919
|
+
try {
|
|
920
|
+
result = await publishEntry(config.library.path, spec, input);
|
|
921
|
+
}
|
|
922
|
+
catch (error) {
|
|
923
|
+
if (!(error instanceof ConfidentialityMismatchError))
|
|
924
|
+
throw error;
|
|
925
|
+
// Nothing was written. Pi declares no confidentiality, so the entry
|
|
926
|
+
// sits at the internal default; whether it may go into a wider
|
|
927
|
+
// library is the user's call, and a yes is the override.
|
|
928
|
+
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?`);
|
|
929
|
+
if (!publishAnyway) {
|
|
930
|
+
ctx.ui.notify("Publish cancelled. Nothing was written.", "info");
|
|
931
|
+
return;
|
|
932
|
+
}
|
|
933
|
+
result = await publishEntry(config.library.path, spec, input, { allowConfidentialityMismatch: true });
|
|
934
|
+
}
|
|
911
935
|
lastFeedbackLines = [`Published ${result.namespace ? `${result.namespace}/` : ""}${result.slug} v${result.version}`, result.isNewVersion ? "New content version." : "Metadata-only update (content unchanged)."];
|
|
912
936
|
await writeDashboard(ctx.cwd, PACKAGE_VERSION);
|
|
913
937
|
await refreshWorkspaceUi(ctx, lastFeedbackLines);
|
|
@@ -81,6 +81,10 @@ export function phaseCompactionExtension(pi) {
|
|
|
81
81
|
return undefined;
|
|
82
82
|
try {
|
|
83
83
|
const state = await getWorkspaceState(ctx.cwd);
|
|
84
|
+
// No workspace under this cwd: nothing phase-aware to add, so the host
|
|
85
|
+
// default applies — not a failure worth a warning (#134).
|
|
86
|
+
if (!state)
|
|
87
|
+
return undefined;
|
|
84
88
|
const phase = state.pipeline.phases.find((candidate) => candidate.id === phaseId);
|
|
85
89
|
const auth = await ctx.modelRegistry.getApiKeyAndHeaders(ctx.model);
|
|
86
90
|
if (!auth.ok || !auth.apiKey)
|