codecartographer-pi 0.20.0 → 0.22.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 +1 -1
- package/.codecarto/broadside/SKILL.md +15 -4
- package/.codecarto/broadside/config.yaml +26 -10
- package/.codecarto/findings/contracts/SKILL.md +4 -1
- package/.codecarto/findings/defect-scan/SKILL.md +10 -0
- package/.codecarto/findings/defect-scan-mechanical/SKILL.md +6 -0
- package/.codecarto/findings/defect-scan-semantic/SKILL.md +8 -1
- package/.codecarto/findings/porting/SKILL.md +4 -0
- package/.codecarto/findings/protocols/SKILL.md +4 -0
- package/.codecarto/templates/mechanical-defects.md +15 -0
- package/.codecarto/templates/reimplementation-spec.md +5 -3
- package/.codecarto/templates/reverse-engineering-bundle.md +10 -1
- package/.codecarto/templates/semantic-defects.md +15 -0
- package/.codecarto/workflow/VALIDATE.md +1 -1
- package/.codecarto/workflow/scaffold-version.yaml +1 -1
- package/README.md +5 -5
- package/agent-skill/codecartographer/references/broadside.md +10 -0
- package/dist/core/amendment.js +9 -4
- package/dist/core/broadside.d.ts +98 -0
- package/dist/core/broadside.js +330 -63
- package/dist/core/completion.js +7 -1
- package/dist/core/library.js +5 -3
- package/dist/core/pipeline.d.ts +39 -3
- package/dist/core/pipeline.js +61 -7
- package/dist/core/prompts.js +10 -3
- package/dist/core/status.d.ts +8 -0
- package/dist/core/status.js +39 -17
- package/dist/core/utils.d.ts +7 -0
- package/dist/core/utils.js +7 -0
- package/dist/core/workspace.js +2 -2
- package/dist/core/yaml.js +8 -1
- package/dist/extensions/codecarto/auto-runner.d.ts +1 -1
- package/dist/extensions/codecarto/auto-runner.js +18 -3
- package/dist/extensions/codecarto/broadside-flags.d.ts +7 -1
- package/dist/extensions/codecarto/broadside-flags.js +51 -0
- package/dist/extensions/codecarto/index.js +103 -35
- package/dist/extensions/codecarto/phase-compaction.js +5 -1
- package/dist/mcp-server/server.d.ts +5 -1
- package/dist/mcp-server/server.js +140 -32
- package/package.json +1 -1
package/dist/core/completion.js
CHANGED
|
@@ -110,7 +110,13 @@ async function appendDecisionLog(workspaceDir, phaseId, closeoutFile, decisions)
|
|
|
110
110
|
const number = String(nextNumber + index).padStart(3, "0");
|
|
111
111
|
return `D${number} | ${decision.trim()} | ${source} | closeouts/${closeoutFile} §Decisions Beyond Prompt (${phaseId})`;
|
|
112
112
|
});
|
|
113
|
-
|
|
113
|
+
// A row appended straight after the section's explanatory paragraph is
|
|
114
|
+
// rendered as part of that paragraph by most Markdown renderers; a blank
|
|
115
|
+
// line makes the rows their own block (self-audit F4). Rows already
|
|
116
|
+
// present stay contiguous with the new ones.
|
|
117
|
+
const trailing = content.replace(/\n+$/, "").split("\n").pop() ?? "";
|
|
118
|
+
const separator = /^D\d+\s*\|/.test(trailing) || trailing.trim() === "" ? "" : "\n";
|
|
119
|
+
content += `${content.endsWith("\n") ? "" : "\n"}${separator}${rows.join("\n")}\n`;
|
|
114
120
|
await writeFile(join(workspaceDir, "DECISIONS.md"), content, "utf8");
|
|
115
121
|
return fresh.length;
|
|
116
122
|
}
|
package/dist/core/library.js
CHANGED
|
@@ -33,7 +33,7 @@ import { spawn } from "node:child_process";
|
|
|
33
33
|
import { mkdir, readFile, readdir, rename, rm, writeFile } from "node:fs/promises";
|
|
34
34
|
import { basename, join, resolve } from "node:path";
|
|
35
35
|
import { acquireLock } from "./status.js";
|
|
36
|
-
import { atomicWriteFile, canonicalPath, isPlainObject, normalizeForComparison, pathExists, uniqueTempSuffix } from "./utils.js";
|
|
36
|
+
import { atomicWriteFile, canonicalPath, GIT_TIMEOUT_MS, isPlainObject, normalizeForComparison, pathExists, uniqueTempSuffix } from "./utils.js";
|
|
37
37
|
import { parseSimpleYaml, stringifySimpleYaml } from "./yaml.js";
|
|
38
38
|
// ─── Constants ──────────────────────────────────────────────────────────────
|
|
39
39
|
export const LIBRARY_MARKER_FILE = ".codecarto-library";
|
|
@@ -830,7 +830,7 @@ async function writeIndexMarkdown(libraryRoot, index, marker) {
|
|
|
830
830
|
const lines = [];
|
|
831
831
|
lines.push(`# ${escapeMd(marker.name)} — Library Index`);
|
|
832
832
|
lines.push("");
|
|
833
|
-
lines.push(`_Generated ${index.generated_at}. Do not edit by hand — regenerate with \`
|
|
833
|
+
lines.push(`_Generated ${index.generated_at}. Do not edit by hand — regenerate with the \`codecarto_library_reindex\` MCP tool._`);
|
|
834
834
|
lines.push("");
|
|
835
835
|
// A single-tenant library has no namespaces but is still one namespace's
|
|
836
836
|
// worth of entries; count once so the noun agrees with the number shown.
|
|
@@ -1041,7 +1041,9 @@ export async function commitPublish(libraryRoot, message, opts = {}) {
|
|
|
1041
1041
|
}
|
|
1042
1042
|
function runGit(cwd, args) {
|
|
1043
1043
|
return new Promise((resolvePromise) => {
|
|
1044
|
-
|
|
1044
|
+
// Bounded like every fetch: a credential helper waiting on a prompt
|
|
1045
|
+
// used to hang publish or source-repo resolution for good (sem 3.12).
|
|
1046
|
+
const child = spawn("git", args, { cwd, stdio: ["ignore", "pipe", "pipe"], timeout: GIT_TIMEOUT_MS });
|
|
1045
1047
|
let stdout = "";
|
|
1046
1048
|
let stderr = "";
|
|
1047
1049
|
child.stdout.on("data", (b) => {
|
package/dist/core/pipeline.d.ts
CHANGED
|
@@ -4,11 +4,47 @@ export declare const DEFAULT_PIPELINE_PATH = "workflow/pipeline-full-with-deep-a
|
|
|
4
4
|
export declare function getPhaseMap(pipeline: PipelineFile): Map<string, PipelinePhase>;
|
|
5
5
|
export declare function getPipelineLabel(pipelinePath: string): string;
|
|
6
6
|
export declare function getNextEligiblePhase(state: WorkspaceState): PipelinePhase | null;
|
|
7
|
+
/** One phase the pipeline cannot reach, and the dependencies keeping it there. */
|
|
8
|
+
export interface BlockedPhase {
|
|
9
|
+
phaseId: string;
|
|
10
|
+
/** Each unmet `depends_on` entry, with why it will not clear on its own. */
|
|
11
|
+
missing: Array<{
|
|
12
|
+
dependencyId: string;
|
|
13
|
+
reason: "not-in-pipeline" | "blocked";
|
|
14
|
+
}>;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* What the pipeline can do next. `getNextEligiblePhase` returned null both
|
|
18
|
+
* when every phase was complete and when the remaining phases waited on a
|
|
19
|
+
* dependency that would never clear, and every consumer read null as
|
|
20
|
+
* complete: a DAG with an unmet dependency reported 1/2 complete and unlocked
|
|
21
|
+
* the post-pipeline skills (#228). The third outcome is the difference.
|
|
22
|
+
*/
|
|
23
|
+
export type PipelineOutcome = {
|
|
24
|
+
kind: "eligible";
|
|
25
|
+
phase: PipelinePhase;
|
|
26
|
+
} | {
|
|
27
|
+
kind: "complete";
|
|
28
|
+
} | {
|
|
29
|
+
kind: "stuck";
|
|
30
|
+
blocked: BlockedPhase[];
|
|
31
|
+
};
|
|
32
|
+
export declare function resolvePipelineOutcome(state: WorkspaceState): PipelineOutcome;
|
|
33
|
+
/** True when every phase in the active pipeline is complete. */
|
|
34
|
+
export declare function isPipelineComplete(state: WorkspaceState): boolean;
|
|
35
|
+
/**
|
|
36
|
+
* One sentence both surfaces print for a stuck pipeline, naming each blocked
|
|
37
|
+
* phase and the dependency keeping it there. The pipeline file is the thing to
|
|
38
|
+
* fix — or switch away from — so the sentence says so.
|
|
39
|
+
*/
|
|
40
|
+
export declare function describeStuckPipeline(blocked: BlockedPhase[]): string;
|
|
7
41
|
/**
|
|
8
42
|
* Point `current_phase` and `next_actions` at whatever the engine finds
|
|
9
|
-
* eligible now,
|
|
10
|
-
*
|
|
11
|
-
*
|
|
43
|
+
* eligible now, at the terminal routing when every phase is complete, or at
|
|
44
|
+
* the first blocked phase with the stuck sentence when nothing can run.
|
|
45
|
+
* Completion and a pipeline switch both derive the cursor this way (#236), so
|
|
46
|
+
* status.yaml never disagrees with the phase records it sits beside. Returns
|
|
47
|
+
* the eligible phase, or null.
|
|
12
48
|
*/
|
|
13
49
|
export declare function recomputeCursor(state: WorkspaceState): PipelinePhase | null;
|
|
14
50
|
/**
|
package/dist/core/pipeline.js
CHANGED
|
@@ -40,17 +40,71 @@ export function getNextEligiblePhase(state) {
|
|
|
40
40
|
}
|
|
41
41
|
return null;
|
|
42
42
|
}
|
|
43
|
+
export function resolvePipelineOutcome(state) {
|
|
44
|
+
const phase = getNextEligiblePhase(state);
|
|
45
|
+
if (phase)
|
|
46
|
+
return { kind: "eligible", phase };
|
|
47
|
+
const phaseMap = getPhaseMap(state.pipeline);
|
|
48
|
+
const isComplete = (phaseId) => state.status.phases[phaseId]?.status === "complete";
|
|
49
|
+
const incomplete = state.pipeline.phase_order.filter((phaseId) => !isComplete(phaseId));
|
|
50
|
+
if (incomplete.length === 0)
|
|
51
|
+
return { kind: "complete" };
|
|
52
|
+
// Nothing is eligible and something is incomplete, so every incomplete
|
|
53
|
+
// phase has an unmet dependency. Each one is either a phase this pipeline
|
|
54
|
+
// does not declare, or one of the blocked phases themselves (a cycle, or a
|
|
55
|
+
// chain back to one).
|
|
56
|
+
const blocked = incomplete.map((phaseId) => ({
|
|
57
|
+
phaseId,
|
|
58
|
+
missing: (phaseMap.get(phaseId)?.depends_on ?? [])
|
|
59
|
+
.filter((dependencyId) => !isComplete(dependencyId))
|
|
60
|
+
.map((dependencyId) => ({
|
|
61
|
+
dependencyId,
|
|
62
|
+
reason: state.pipeline.phase_order.includes(dependencyId) ? "blocked" : "not-in-pipeline",
|
|
63
|
+
})),
|
|
64
|
+
}));
|
|
65
|
+
return { kind: "stuck", blocked };
|
|
66
|
+
}
|
|
67
|
+
/** True when every phase in the active pipeline is complete. */
|
|
68
|
+
export function isPipelineComplete(state) {
|
|
69
|
+
return resolvePipelineOutcome(state).kind === "complete";
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* One sentence both surfaces print for a stuck pipeline, naming each blocked
|
|
73
|
+
* phase and the dependency keeping it there. The pipeline file is the thing to
|
|
74
|
+
* fix — or switch away from — so the sentence says so.
|
|
75
|
+
*/
|
|
76
|
+
export function describeStuckPipeline(blocked) {
|
|
77
|
+
const parts = blocked.map((entry) => {
|
|
78
|
+
const deps = entry.missing.map((m) => m.reason === "not-in-pipeline" ? `${m.dependencyId}, which is not in this pipeline` : `${m.dependencyId}, which is itself blocked`);
|
|
79
|
+
return `${entry.phaseId} depends on ${deps.join(" and ") || "nothing it can reach"}`;
|
|
80
|
+
});
|
|
81
|
+
return `Pipeline is stuck: ${parts.join("; ")}. No phase can run until the pipeline file's depends_on is fixed (or switch pipelines with codecarto_switch_pipeline / /codecarto-switch-pipeline).`;
|
|
82
|
+
}
|
|
43
83
|
/**
|
|
44
84
|
* Point `current_phase` and `next_actions` at whatever the engine finds
|
|
45
|
-
* eligible now,
|
|
46
|
-
*
|
|
47
|
-
*
|
|
85
|
+
* eligible now, at the terminal routing when every phase is complete, or at
|
|
86
|
+
* the first blocked phase with the stuck sentence when nothing can run.
|
|
87
|
+
* Completion and a pipeline switch both derive the cursor this way (#236), so
|
|
88
|
+
* status.yaml never disagrees with the phase records it sits beside. Returns
|
|
89
|
+
* the eligible phase, or null.
|
|
48
90
|
*/
|
|
49
91
|
export function recomputeCursor(state) {
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
92
|
+
const outcome = resolvePipelineOutcome(state);
|
|
93
|
+
if (outcome.kind === "eligible") {
|
|
94
|
+
state.status.current_phase = outcome.phase.id;
|
|
95
|
+
state.status.next_actions = [beginPhaseAction(outcome.phase)];
|
|
96
|
+
return outcome.phase;
|
|
97
|
+
}
|
|
98
|
+
if (outcome.kind === "stuck") {
|
|
99
|
+
// The cursor stays on the first phase that cannot run; "complete" is
|
|
100
|
+
// reserved for the state where nothing is left (#228).
|
|
101
|
+
state.status.current_phase = outcome.blocked[0]?.phaseId ?? "complete";
|
|
102
|
+
state.status.next_actions = [describeStuckPipeline(outcome.blocked)];
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
state.status.current_phase = "complete";
|
|
106
|
+
state.status.next_actions = buildTerminalNextActions(state.status);
|
|
107
|
+
return null;
|
|
54
108
|
}
|
|
55
109
|
/**
|
|
56
110
|
* Phases status.yaml records as complete whose primary output is not on disk.
|
package/dist/core/prompts.js
CHANGED
|
@@ -120,6 +120,13 @@ export async function buildPhasePrompt(state, phase, forced, options = {}) {
|
|
|
120
120
|
const preflight = options.preflight ?? await runPhasePreflight(state, phase);
|
|
121
121
|
const synthesisWorkflow = state.pipeline.workflow_name === "evidence-backed-project-synthesis";
|
|
122
122
|
const handoffTemplateExists = await pathExists(join(state.workspaceDir, "templates", "phase-handoff.yaml"));
|
|
123
|
+
// The framework's own three files are the first reads on every phase, and
|
|
124
|
+
// after the first phase they are the same three files. Say so, so a host
|
|
125
|
+
// that carries context across phases spends it on the phase's inputs
|
|
126
|
+
// rather than re-reading the guide (self-audit F5). status.yaml is the
|
|
127
|
+
// exception: completion rewrote it, and it is the cursor.
|
|
128
|
+
const laterPhase = Object.values(state.status.phases).some((phaseState) => phaseState.status === "complete");
|
|
129
|
+
const unchangedNote = laterPhase ? " (framework-owned; unchanged since your last phase unless the scaffold was refreshed — skim rather than re-read if you still hold it)" : "";
|
|
123
130
|
const lines = [
|
|
124
131
|
`Read .codecarto/GUIDE.md and continue the CodeCartographer workflow for the phase \`${phase.id}\`.`,
|
|
125
132
|
synthesisWorkflow
|
|
@@ -127,11 +134,11 @@ export async function buildPhasePrompt(state, phase, forced, options = {}) {
|
|
|
127
134
|
: "Work on this phase only. The analyzed source code is the repository outside .codecarto/.",
|
|
128
135
|
"",
|
|
129
136
|
"Required reads before analysis:",
|
|
130
|
-
|
|
131
|
-
|
|
137
|
+
`- .codecarto/GUIDE.md${unchangedNote}`,
|
|
138
|
+
`- .codecarto/workflow/status.yaml${laterPhase ? " (rewritten by the last completion; read it)" : ""}`,
|
|
132
139
|
];
|
|
133
140
|
if (handoffTemplateExists) {
|
|
134
|
-
lines.push(
|
|
141
|
+
lines.push(`- .codecarto/templates/phase-handoff.yaml${unchangedNote}`);
|
|
135
142
|
}
|
|
136
143
|
const primaryOutput = phase.primary_output ? `.codecarto/${phase.primary_output}` : undefined;
|
|
137
144
|
if (primaryOutput) {
|
package/dist/core/status.d.ts
CHANGED
|
@@ -3,6 +3,14 @@ export declare const LOCK_RETRY_MS = 125;
|
|
|
3
3
|
export declare const LOCK_TIMEOUT_MS = 5000;
|
|
4
4
|
export declare const STALE_LOCK_MS = 60000;
|
|
5
5
|
export declare function assertSafePhaseId(phaseId: string): void;
|
|
6
|
+
/**
|
|
7
|
+
* A YAML scalar as text: strings as written, numbers and booleans spelled
|
|
8
|
+
* back out. Files written before #225 hold owner notes such as `2048` or
|
|
9
|
+
* `true` bare, which the reader returns as a number or a boolean; dropping
|
|
10
|
+
* or crashing on those would lose real state, so they are read as the text
|
|
11
|
+
* they were. Anything else (null, arrays, objects) has no text.
|
|
12
|
+
*/
|
|
13
|
+
export declare function textOf(value: unknown): string | null;
|
|
6
14
|
export declare function ensureArray(value: unknown): string[];
|
|
7
15
|
/**
|
|
8
16
|
* Normalize a handoff's `open_question_closures` (#122, #186). Accepts both
|
package/dist/core/status.js
CHANGED
|
@@ -13,8 +13,28 @@ export function assertSafePhaseId(phaseId) {
|
|
|
13
13
|
throw new Error(`Invalid phase id: ${phaseId}`);
|
|
14
14
|
}
|
|
15
15
|
}
|
|
16
|
+
/**
|
|
17
|
+
* A YAML scalar as text: strings as written, numbers and booleans spelled
|
|
18
|
+
* back out. Files written before #225 hold owner notes such as `2048` or
|
|
19
|
+
* `true` bare, which the reader returns as a number or a boolean; dropping
|
|
20
|
+
* or crashing on those would lose real state, so they are read as the text
|
|
21
|
+
* they were. Anything else (null, arrays, objects) has no text.
|
|
22
|
+
*/
|
|
23
|
+
export function textOf(value) {
|
|
24
|
+
if (typeof value === "string")
|
|
25
|
+
return value;
|
|
26
|
+
if (typeof value === "number" || typeof value === "boolean")
|
|
27
|
+
return String(value);
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
/** Like {@link textOf}, trimmed, and "" for a value that has no text. */
|
|
31
|
+
function trimmedText(value) {
|
|
32
|
+
return (textOf(value) ?? "").trim();
|
|
33
|
+
}
|
|
16
34
|
export function ensureArray(value) {
|
|
17
|
-
|
|
35
|
+
if (!Array.isArray(value))
|
|
36
|
+
return [];
|
|
37
|
+
return value.map(textOf).filter((entry) => entry !== null);
|
|
18
38
|
}
|
|
19
39
|
function coerceEntry(value, allowTargetPhase) {
|
|
20
40
|
if (typeof value === "string") {
|
|
@@ -27,22 +47,22 @@ function coerceEntry(value, allowTargetPhase) {
|
|
|
27
47
|
return null;
|
|
28
48
|
const raw = value;
|
|
29
49
|
const entry = {};
|
|
30
|
-
if (
|
|
31
|
-
entry.id = raw.id
|
|
32
|
-
if (
|
|
33
|
-
entry.kind = raw.kind
|
|
34
|
-
if (
|
|
35
|
-
entry.description = raw.description
|
|
36
|
-
if (
|
|
37
|
-
entry.deferred_reason = raw.deferred_reason
|
|
38
|
-
if (allowTargetPhase &&
|
|
39
|
-
entry.target_phase = raw.target_phase
|
|
50
|
+
if (trimmedText(raw.id))
|
|
51
|
+
entry.id = trimmedText(raw.id);
|
|
52
|
+
if (trimmedText(raw.kind))
|
|
53
|
+
entry.kind = trimmedText(raw.kind);
|
|
54
|
+
if (trimmedText(raw.description))
|
|
55
|
+
entry.description = trimmedText(raw.description);
|
|
56
|
+
if (trimmedText(raw.deferred_reason))
|
|
57
|
+
entry.deferred_reason = trimmedText(raw.deferred_reason);
|
|
58
|
+
if (allowTargetPhase && trimmedText(raw.target_phase))
|
|
59
|
+
entry.target_phase = trimmedText(raw.target_phase);
|
|
40
60
|
// derives_from rides the same flag as target_phase: it is a carry-forward
|
|
41
61
|
// concept only — the id of the open question this routed item answers one
|
|
42
62
|
// candidate of (#122, #186). An open_questions entry has nothing to derive
|
|
43
63
|
// from, so the field is dropped there rather than silently carried.
|
|
44
|
-
if (allowTargetPhase &&
|
|
45
|
-
entry.derives_from = raw.derives_from
|
|
64
|
+
if (allowTargetPhase && trimmedText(raw.derives_from))
|
|
65
|
+
entry.derives_from = trimmedText(raw.derives_from);
|
|
46
66
|
return Object.keys(entry).length > 0 ? entry : null;
|
|
47
67
|
}
|
|
48
68
|
/**
|
|
@@ -218,11 +238,13 @@ export function normalizeStatus(status, pipeline, pipelinePath, cwd) {
|
|
|
218
238
|
};
|
|
219
239
|
}
|
|
220
240
|
}
|
|
241
|
+
// Coerced, not assumed: a status.yaml written before #225 spells a
|
|
242
|
+
// digit-named project bare, and the reader returns a number for it.
|
|
221
243
|
return {
|
|
222
|
-
project_name: status.project_name
|
|
223
|
-
pipeline: status.pipeline
|
|
224
|
-
current_phase: status.current_phase
|
|
225
|
-
last_updated: status.last_updated
|
|
244
|
+
project_name: trimmedText(status.project_name) || basename(cwd),
|
|
245
|
+
pipeline: trimmedText(status.pipeline) || pipelinePath,
|
|
246
|
+
current_phase: trimmedText(status.current_phase) || pipeline.phase_order[0] || "complete",
|
|
247
|
+
last_updated: trimmedText(status.last_updated) || "",
|
|
226
248
|
schema_version: typeof status.schema_version === "number" ? status.schema_version : 1,
|
|
227
249
|
phases,
|
|
228
250
|
next_actions: ensureArray(status.next_actions),
|
package/dist/core/utils.d.ts
CHANGED
|
@@ -77,3 +77,10 @@ export declare function formatMillis(ms: number): string;
|
|
|
77
77
|
* hand-edited markers) so callers can fall back to string equality.
|
|
78
78
|
*/
|
|
79
79
|
export declare function compareDottedVersions(a: string, b: string): number | null;
|
|
80
|
+
/**
|
|
81
|
+
* Every git subprocess gets this. A hung git — a credential helper waiting
|
|
82
|
+
* on a prompt, a slow filesystem — used to hang a publish or a Broad-Side
|
|
83
|
+
* submit indefinitely while every fetch carried a 30 s timeout (self-audit
|
|
84
|
+
* sem 3.12).
|
|
85
|
+
*/
|
|
86
|
+
export declare const GIT_TIMEOUT_MS = 30000;
|
package/dist/core/utils.js
CHANGED
|
@@ -206,3 +206,10 @@ export function compareDottedVersions(a, b) {
|
|
|
206
206
|
}
|
|
207
207
|
return 0;
|
|
208
208
|
}
|
|
209
|
+
/**
|
|
210
|
+
* Every git subprocess gets this. A hung git — a credential helper waiting
|
|
211
|
+
* on a prompt, a slow filesystem — used to hang a publish or a Broad-Side
|
|
212
|
+
* submit indefinitely while every fetch carried a 30 s timeout (self-audit
|
|
213
|
+
* sem 3.12).
|
|
214
|
+
*/
|
|
215
|
+
export const GIT_TIMEOUT_MS = 30_000;
|
package/dist/core/workspace.js
CHANGED
|
@@ -7,7 +7,7 @@ import { appendFile, copyFile, cp, mkdir, readFile, readdir, rename } from "node
|
|
|
7
7
|
import { basename, dirname, join, relative } from "node:path";
|
|
8
8
|
import { fileURLToPath } from "node:url";
|
|
9
9
|
import { getPipelineLabel, recomputeCursor } from "./pipeline.js";
|
|
10
|
-
import { acquireLock, applyHandoff, autoAssignIds, createEmptyStatus, normalizeStatus, parseHandoff } from "./status.js";
|
|
10
|
+
import { acquireLock, applyHandoff, autoAssignIds, createEmptyStatus, normalizeStatus, parseHandoff, textOf } from "./status.js";
|
|
11
11
|
import { atomicWriteFile, compareDottedVersions, newlineIfUnterminated, pathExists } from "./utils.js";
|
|
12
12
|
import { loadYamlFile, stringifySimpleYaml } from "./yaml.js";
|
|
13
13
|
// Walk up from the current file to find the package root. Needed because the
|
|
@@ -68,7 +68,7 @@ export async function getWorkspaceState(cwd) {
|
|
|
68
68
|
if (!(await pathExists(statusPath)))
|
|
69
69
|
return null;
|
|
70
70
|
const rawStatus = await loadYamlFile(statusPath);
|
|
71
|
-
const pipelineRelativePath = rawStatus.pipeline
|
|
71
|
+
const pipelineRelativePath = (textOf(rawStatus.pipeline) ?? "").trim();
|
|
72
72
|
if (!pipelineRelativePath) {
|
|
73
73
|
throw new Error(`Missing pipeline in ${relative(cwd, statusPath) || statusPath}`);
|
|
74
74
|
}
|
package/dist/core/yaml.js
CHANGED
|
@@ -497,8 +497,15 @@ export function formatYamlScalar(value) {
|
|
|
497
497
|
const stringValue = String(value);
|
|
498
498
|
if (stringValue === "")
|
|
499
499
|
return '""';
|
|
500
|
-
|
|
500
|
+
// Bare only when the reader would hand the same string back. The charset
|
|
501
|
+
// keeps out spaces, quotes, `#` and `:`; the round-trip check keeps out the
|
|
502
|
+
// strings the reader coerces — "2048", "true", "null", "1.5" — which used
|
|
503
|
+
// to go out bare and come back as a number, a boolean, or nothing, until a
|
|
504
|
+
// repository named `2048` bricked its workspace on `project_name.trim`
|
|
505
|
+
// (#225). A lone `-` is a sequence marker inside a list, so it is quoted too.
|
|
506
|
+
if (/^[A-Za-z0-9_./-]+$/.test(stringValue) && stringValue !== "-" && parseYamlScalar(stringValue) === stringValue) {
|
|
501
507
|
return stringValue;
|
|
508
|
+
}
|
|
502
509
|
return JSON.stringify(stringValue);
|
|
503
510
|
}
|
|
504
511
|
export function stringifySimpleYaml(value, indent = 0) {
|
|
@@ -45,7 +45,7 @@ export interface AutoCompleteResult {
|
|
|
45
45
|
warnings: string[];
|
|
46
46
|
}
|
|
47
47
|
export declare function autoCompletePhase(cwd: string, validation: ValidationResult): Promise<AutoCompleteResult>;
|
|
48
|
-
export type AutoOutcome = "complete" | "stopped" | "aborted";
|
|
48
|
+
export type AutoOutcome = "complete" | "stopped" | "aborted" | "stuck";
|
|
49
49
|
export interface AutoRunOptions {
|
|
50
50
|
strict: boolean;
|
|
51
51
|
llmSteerOverride?: boolean;
|
|
@@ -17,7 +17,7 @@ import { clearPhase, finishPhase, getPhaseActivity, startPhase } from "./agent-s
|
|
|
17
17
|
import { buildSteeringMessage, rewritePhasePrompt } from "./agent-rewriter.js";
|
|
18
18
|
import { buildPhaseSummary } from "./agent-summary.js";
|
|
19
19
|
import { getAgentsWidget } from "./agent-widget.js";
|
|
20
|
-
import { appendUsageRun, buildPhasePrompt, buildValidationSummary, completeValidatedPhase, formatMillis, formatTokenCount,
|
|
20
|
+
import { appendUsageRun, buildPhasePrompt, buildValidationSummary, completeValidatedPhase, formatMillis, formatTokenCount, describeStuckPipeline, resolvePipelineOutcome, getWorkspaceState, describeConfigProblems, loadCodecartoConfig, PACKAGE_VERSION, PhasePreflightError, runPhasePreflight, validatePhaseOutput, writeDashboard, } from "../../core/index.js";
|
|
21
21
|
/**
|
|
22
22
|
* Run one phase end to end: optional LLM-steered rewrite, spawn the sub-agent,
|
|
23
23
|
* wait for it, then emit the side effects the historical /codecarto-next chain
|
|
@@ -203,13 +203,22 @@ export async function runAuto(ctx, pi, initialState, options) {
|
|
|
203
203
|
reason: "User aborted the auto run.",
|
|
204
204
|
});
|
|
205
205
|
}
|
|
206
|
-
const
|
|
207
|
-
if (
|
|
206
|
+
const outcome = resolvePipelineOutcome(state);
|
|
207
|
+
if (outcome.kind === "stuck") {
|
|
208
|
+
// Not "complete": the loop ending because nothing can run is the
|
|
209
|
+
// case that used to read as success (#228).
|
|
210
|
+
return finish({
|
|
211
|
+
outcome: "stuck",
|
|
212
|
+
reason: describeStuckPipeline(outcome.blocked),
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
if (outcome.kind === "complete") {
|
|
208
216
|
return finish({
|
|
209
217
|
outcome: "complete",
|
|
210
218
|
reason: "Pipeline complete.",
|
|
211
219
|
});
|
|
212
220
|
}
|
|
221
|
+
const phase = outcome.phase;
|
|
213
222
|
let preflight;
|
|
214
223
|
try {
|
|
215
224
|
preflight = await runPhasePreflight(state, phase);
|
|
@@ -308,6 +317,8 @@ export function buildAutoSummary(result, availableSkills = []) {
|
|
|
308
317
|
return `**Auto pipeline stopped at \`${result.stoppedAt?.phaseId ?? "?"}\`.**`;
|
|
309
318
|
case "aborted":
|
|
310
319
|
return `**Auto pipeline aborted${result.stoppedAt?.phaseId ? ` during \`${result.stoppedAt.phaseId}\`` : ""}.**`;
|
|
320
|
+
case "stuck":
|
|
321
|
+
return `**Auto pipeline stuck: no phase can run.**`;
|
|
311
322
|
}
|
|
312
323
|
})();
|
|
313
324
|
const statsLine = `_⟳ ${ranOf} · ${tokensStr} tokens · ${wallTime}_`;
|
|
@@ -321,6 +332,10 @@ export function buildAutoSummary(result, availableSkills = []) {
|
|
|
321
332
|
lines.push("", result.reason);
|
|
322
333
|
lines.push("", recoveryHint(result));
|
|
323
334
|
}
|
|
335
|
+
if (result.outcome === "stuck") {
|
|
336
|
+
// The reason is the stuck sentence, which already says what to fix.
|
|
337
|
+
lines.push("", result.reason);
|
|
338
|
+
}
|
|
324
339
|
if (result.outcome === "complete") {
|
|
325
340
|
lines.push("", "Dashboard: `.codecarto/dashboard.html`");
|
|
326
341
|
if (availableSkills.length > 0) {
|
|
@@ -16,11 +16,17 @@ export interface BroadsideFlags {
|
|
|
16
16
|
/** Undefined means "use the repository's config default". */
|
|
17
17
|
maxCost?: number;
|
|
18
18
|
waitSeconds?: number;
|
|
19
|
+
/** For collect: the run to collect instead of the most recent (#268). */
|
|
20
|
+
runId?: string;
|
|
21
|
+
/** For submit: the run's batch model, replacing config.yaml's (#141). */
|
|
22
|
+
model?: string;
|
|
23
|
+
/** For submit: per-lens model overrides, layered over config.yaml's (#141). */
|
|
24
|
+
lensModels?: Partial<Record<BroadsideLensId, string>>;
|
|
19
25
|
benchmarks: boolean;
|
|
20
26
|
unknown: string[];
|
|
21
27
|
/** Set on an invalid combination. The caller surfaces it as an error. */
|
|
22
28
|
error?: string;
|
|
23
29
|
}
|
|
24
30
|
/** Every token the completer offers, in the order it offers them. */
|
|
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"];
|
|
31
|
+
export declare const KNOWN_BROADSIDE_TOKENS: readonly ["submit", "collect", "status", "models", "architecture", "api", "security", "defect", "conventions", "porting", "--incremental", "--no-incremental", "--max-cost=", "--wait=", "--run=", "--model=", "--lens-model=", "--no-synthesis", "--no-triage", "--no-retry-truncated", "--benchmarks"];
|
|
26
32
|
export declare function parseBroadsideFlags(args: string): BroadsideFlags;
|
|
@@ -13,6 +13,12 @@
|
|
|
13
13
|
// --no-incremental --no-triage
|
|
14
14
|
// --max-cost=N --no-retry-truncated
|
|
15
15
|
// --wait=SECONDS --benchmarks (models only)
|
|
16
|
+
// --run=ID (collect only: an older run, as listed by status)
|
|
17
|
+
// --model=ID (submit only: the run's batch model, as listed by models)
|
|
18
|
+
// --lens-model=LENS:ID (submit only, repeatable: one lens on its own model)
|
|
19
|
+
//
|
|
20
|
+
// A model id itself contains a colon (`vendor/name:batch`), so --lens-model
|
|
21
|
+
// splits on the first colon only: `security:deepseek/deepseek-v4-pro:batch`.
|
|
16
22
|
//
|
|
17
23
|
// --incremental has a spelled-out negative because the value is tri-state:
|
|
18
24
|
// absent defers to config.yaml, so a repository that set `incremental: true`
|
|
@@ -34,6 +40,9 @@ export const KNOWN_BROADSIDE_TOKENS = [
|
|
|
34
40
|
"--no-incremental",
|
|
35
41
|
"--max-cost=",
|
|
36
42
|
"--wait=",
|
|
43
|
+
"--run=",
|
|
44
|
+
"--model=",
|
|
45
|
+
"--lens-model=",
|
|
37
46
|
"--no-synthesis",
|
|
38
47
|
"--no-triage",
|
|
39
48
|
"--no-retry-truncated",
|
|
@@ -106,6 +115,39 @@ export function parseBroadsideFlags(args) {
|
|
|
106
115
|
result.waitSeconds = parseNumeric(token, "--wait", result);
|
|
107
116
|
continue;
|
|
108
117
|
}
|
|
118
|
+
if (token.startsWith("--run=")) {
|
|
119
|
+
const value = token.slice("--run=".length).trim();
|
|
120
|
+
if (!value)
|
|
121
|
+
result.error ??= "--run= needs a run id (see /codecarto-broadside status).";
|
|
122
|
+
result.runId = value || undefined;
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (token.startsWith("--model=")) {
|
|
126
|
+
const value = token.slice("--model=".length).trim();
|
|
127
|
+
// An empty value is a mistyped selection, not "use the default":
|
|
128
|
+
// the command is about to spend money on whichever model wins.
|
|
129
|
+
if (!value)
|
|
130
|
+
result.error ??= "--model= needs an OpenRouter batch model id (see /codecarto-broadside models).";
|
|
131
|
+
result.model = value || undefined;
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
if (token.startsWith("--lens-model=")) {
|
|
135
|
+
const value = token.slice("--lens-model=".length).trim();
|
|
136
|
+
const colon = value.indexOf(":");
|
|
137
|
+
const lensId = colon > 0 ? value.slice(0, colon).trim() : "";
|
|
138
|
+
const modelId = colon > 0 ? value.slice(colon + 1).trim() : "";
|
|
139
|
+
if (!lensId || !modelId) {
|
|
140
|
+
result.error ??= `--lens-model needs LENS:MODEL, e.g. --lens-model=security:vendor/name:batch (got "${value}").`;
|
|
141
|
+
}
|
|
142
|
+
else if (!BROADSIDE_LENS_IDS.includes(lensId)) {
|
|
143
|
+
result.error ??= `--lens-model: unknown lens "${lensId}". Lenses: ${BROADSIDE_LENS_IDS.join(", ")}.`;
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
result.lensModels ??= {};
|
|
147
|
+
result.lensModels[lensId] = modelId;
|
|
148
|
+
}
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
109
151
|
result.unknown.push(token);
|
|
110
152
|
}
|
|
111
153
|
// Flags that only mean something for one action are refused rather than
|
|
@@ -125,5 +167,14 @@ export function parseBroadsideFlags(args) {
|
|
|
125
167
|
if (result.action === "status" && result.waitSeconds !== undefined) {
|
|
126
168
|
result.error ??= "--wait is only meaningful for submit and collect; status reads recorded state.";
|
|
127
169
|
}
|
|
170
|
+
if (result.runId !== undefined && result.action !== "collect") {
|
|
171
|
+
result.error ??= `--run is only meaningful for collect (got action "${result.action}").`;
|
|
172
|
+
}
|
|
173
|
+
if (result.model !== undefined && result.action !== "submit") {
|
|
174
|
+
result.error ??= `--model is only meaningful for submit (got action "${result.action}").`;
|
|
175
|
+
}
|
|
176
|
+
if (result.lensModels !== undefined && result.action !== "submit") {
|
|
177
|
+
result.error ??= `--lens-model is only meaningful for submit (got action "${result.action}").`;
|
|
178
|
+
}
|
|
128
179
|
return result;
|
|
129
180
|
}
|