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.
Files changed (56) hide show
  1. package/.codecarto/GUIDE.md +1 -1
  2. package/.codecarto/findings/architecture/SKILL.md +1 -0
  3. package/.codecarto/findings/contracts/SKILL.md +1 -0
  4. package/.codecarto/findings/defect-scan/SKILL.md +15 -1
  5. package/.codecarto/findings/defect-scan/passes/01-logic-and-correctness.md +1 -1
  6. package/.codecarto/findings/defect-scan/passes/02-error-handling.md +1 -1
  7. package/.codecarto/findings/defect-scan/passes/03-concurrency-and-resources.md +1 -1
  8. package/.codecarto/findings/defect-scan/passes/04-security-and-trust.md +1 -1
  9. package/.codecarto/findings/defect-scan/passes/05-api-contract-violations.md +2 -1
  10. package/.codecarto/findings/defect-scan/passes/06-config-and-environment.md +1 -1
  11. package/.codecarto/findings/defect-scan-mechanical/SKILL.md +3 -2
  12. package/.codecarto/findings/defect-scan-semantic/SKILL.md +2 -0
  13. package/.codecarto/findings/porting/SKILL.md +2 -1
  14. package/.codecarto/findings/protocols/SKILL.md +1 -0
  15. package/.codecarto/findings/reimplementation-spec/SKILL.md +1 -1
  16. package/.codecarto/templates/architecture-map.md +1 -1
  17. package/.codecarto/templates/defect-report.md +23 -0
  18. package/.codecarto/templates/mechanical-defects.md +22 -0
  19. package/.codecarto/templates/reverse-engineering-bundle.md +2 -2
  20. package/.codecarto/templates/semantic-defects.md +26 -0
  21. package/.codecarto/workflow/VALIDATE.md +1 -1
  22. package/.codecarto/workflow/pipeline-defect-scan.yaml +2 -0
  23. package/.codecarto/workflow/pipeline-full-with-audit.yaml +3 -1
  24. package/.codecarto/workflow/pipeline-full-with-deep-audit.yaml +5 -1
  25. package/.codecarto/workflow/pipeline-scout-first.yaml +5 -1
  26. package/.codecarto/workflow/scaffold-version.yaml +1 -1
  27. package/README.md +4 -4
  28. package/agent-skill/codecartographer/references/deep-audit-synthesis.md +4 -1
  29. package/agent-skill/codecartographer/references/library.md +1 -1
  30. package/agent-skill/codecartographer/references/orchestration.md +1 -1
  31. package/dist/core/amendment.js +2 -2
  32. package/dist/core/completion.d.ts +5 -0
  33. package/dist/core/completion.js +18 -2
  34. package/dist/core/dashboard.js +5 -3
  35. package/dist/core/findings.d.ts +59 -0
  36. package/dist/core/findings.js +145 -0
  37. package/dist/core/index.d.ts +1 -0
  38. package/dist/core/index.js +1 -0
  39. package/dist/core/library.d.ts +66 -1
  40. package/dist/core/library.js +161 -8
  41. package/dist/core/pipeline.js +15 -0
  42. package/dist/core/prompts.js +1 -1
  43. package/dist/core/status.js +14 -6
  44. package/dist/core/types.d.ts +6 -0
  45. package/dist/core/utils.d.ts +14 -0
  46. package/dist/core/utils.js +30 -0
  47. package/dist/core/workspace.js +11 -18
  48. package/dist/core/yaml.js +19 -4
  49. package/dist/extensions/codecarto/auto-runner.d.ts +2 -0
  50. package/dist/extensions/codecarto/broadside-flags.d.ts +7 -2
  51. package/dist/extensions/codecarto/broadside-flags.js +22 -9
  52. package/dist/extensions/codecarto/dashboard-narrator.js +1 -1
  53. package/dist/extensions/codecarto/index.js +40 -16
  54. package/dist/extensions/codecarto/phase-compaction.js +4 -0
  55. package/dist/mcp-server/server.js +65 -7
  56. package/package.json +2 -2
@@ -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.`);
@@ -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)
@@ -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
- phase.open_questions.push(entry);
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
- phase.carry_forward.push(entry);
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)
@@ -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
@@ -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;
@@ -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
+ }
@@ -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
- await appendFile(join(state.workspaceDir, "THREAD_LOG.md"), `${entry}\n`, "utf8");
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, result.threadLogEntry, "utf8");
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[i - 1] !== "\\") {
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[i - 1] !== "\\") {
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
- if (trimmed.startsWith('"') && trimmed.endsWith('"')) {
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
- incremental: boolean;
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
- // --max-cost=N --no-triage
14
- // --wait=SECONDS --no-retry-truncated
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
- result.incremental = true;
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.error ??= `--incremental is only meaningful for submit (got action "${result.action}").`;
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
- incremental: flags.incremental || config.incremental,
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
- const result = await publishEntry(config.library.path, spec, {
900
- slug,
901
- namespace,
902
- source_repo: ctx.cwd,
903
- analyzed_at: new Date().toISOString(),
904
- pipeline: state.status.pipeline,
905
- codecarto_version: PACKAGE_VERSION,
906
- headline,
907
- tags: [],
908
- capabilities: [],
909
- generation: piGeneration(ctx),
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)