codecartographer-pi 0.19.2 → 0.19.4
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/templates/phase-handoff.yaml +2 -1
- package/.codecarto/workflow/scaffold-version.yaml +1 -1
- package/dist/core/yaml.js +88 -19
- package/dist/extensions/codecarto/guide-framing.d.ts +11 -0
- package/dist/extensions/codecarto/guide-framing.js +76 -0
- package/dist/extensions/codecarto/index.js +29 -10
- package/package.json +1 -1
|
@@ -39,7 +39,8 @@ decisions: []
|
|
|
39
39
|
# evidence: <optional: where the pattern showed up>
|
|
40
40
|
proposed_conventions: []
|
|
41
41
|
closeout_summary: ""
|
|
42
|
-
# Optional full closeout Markdown.
|
|
42
|
+
# Optional full closeout Markdown. Block scalars are supported — literal (|, |-,
|
|
43
|
+
# |+) to keep line breaks, folded (>, >-, >+) to join wrapped prose into one line.
|
|
43
44
|
closeout_content: |-
|
|
44
45
|
# Closeout — <phase-id>
|
|
45
46
|
|
package/dist/core/yaml.js
CHANGED
|
@@ -105,6 +105,61 @@ export function parseYamlScalar(rawValue) {
|
|
|
105
105
|
}
|
|
106
106
|
return trimmed;
|
|
107
107
|
}
|
|
108
|
+
function parseBlockScalarHeader(rawValue) {
|
|
109
|
+
const match = /^([|>])([-+]?)$/.exec(rawValue);
|
|
110
|
+
if (!match)
|
|
111
|
+
return null;
|
|
112
|
+
return {
|
|
113
|
+
literal: match[1] === "|",
|
|
114
|
+
chomp: match[2] === "-" ? "strip" : match[2] === "+" ? "keep" : "clip",
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Fold a block scalar's lines per YAML's folding rules: a single line break
|
|
119
|
+
* between two content lines becomes a space, and a run of k blank lines becomes
|
|
120
|
+
* k newlines. Lines indented deeper than the block's own content indent are
|
|
121
|
+
* "more indented" and keep their breaks literally, which is what lets a folded
|
|
122
|
+
* block hold an indented snippet without it being flattened onto one line.
|
|
123
|
+
*/
|
|
124
|
+
function foldBlockLines(blockLines) {
|
|
125
|
+
let result = "";
|
|
126
|
+
let pendingBreaks = 0;
|
|
127
|
+
let started = false;
|
|
128
|
+
let previousMoreIndented = false;
|
|
129
|
+
for (const line of blockLines) {
|
|
130
|
+
if (line.trim() === "") {
|
|
131
|
+
pendingBreaks++;
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
const moreIndented = /^[ \t]/.test(line);
|
|
135
|
+
if (!started) {
|
|
136
|
+
result = line;
|
|
137
|
+
started = true;
|
|
138
|
+
previousMoreIndented = moreIndented;
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
if (pendingBreaks > 0) {
|
|
142
|
+
result += "\n".repeat(pendingBreaks);
|
|
143
|
+
pendingBreaks = 0;
|
|
144
|
+
}
|
|
145
|
+
else if (moreIndented || previousMoreIndented) {
|
|
146
|
+
result += "\n";
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
result += " ";
|
|
150
|
+
}
|
|
151
|
+
result += line;
|
|
152
|
+
previousMoreIndented = moreIndented;
|
|
153
|
+
}
|
|
154
|
+
return started ? result + "\n".repeat(pendingBreaks) : "";
|
|
155
|
+
}
|
|
156
|
+
function applyBlockScalar(blockLines, header) {
|
|
157
|
+
const content = header.literal ? blockLines.join("\n") : foldBlockLines(blockLines);
|
|
158
|
+
if (header.chomp === "keep")
|
|
159
|
+
return content;
|
|
160
|
+
const stripped = content.replace(/\n+$/, "");
|
|
161
|
+
return header.chomp === "strip" ? stripped : `${stripped}\n`;
|
|
162
|
+
}
|
|
108
163
|
export function parseSimpleYaml(raw) {
|
|
109
164
|
const lines = raw.split(/\r?\n/);
|
|
110
165
|
let index = 0;
|
|
@@ -112,6 +167,31 @@ export function parseSimpleYaml(raw) {
|
|
|
112
167
|
while (index < lines.length && isBlankOrComment(lines[index] ?? ""))
|
|
113
168
|
index++;
|
|
114
169
|
};
|
|
170
|
+
/**
|
|
171
|
+
* Read the body of a block scalar that opened on the line just consumed.
|
|
172
|
+
* Shared by mapping values (`key: >-`) and sequence items (`- >-`): when only
|
|
173
|
+
* the mapping path had it, a handoff whose `decisions:` list used `- >-`
|
|
174
|
+
* still failed with the indentation error that #211 was supposed to end.
|
|
175
|
+
*/
|
|
176
|
+
const collectBlockScalarLines = (baseIndent) => {
|
|
177
|
+
const blockLines = [];
|
|
178
|
+
let contentIndent = null;
|
|
179
|
+
while (index < lines.length) {
|
|
180
|
+
const blockLine = lines[index] ?? "";
|
|
181
|
+
if (blockLine.trim() === "") {
|
|
182
|
+
blockLines.push("");
|
|
183
|
+
index++;
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
const blockIndent = countIndent(blockLine);
|
|
187
|
+
if (blockIndent <= baseIndent)
|
|
188
|
+
break;
|
|
189
|
+
contentIndent ??= blockIndent;
|
|
190
|
+
blockLines.push(blockLine.slice(Math.min(contentIndent, blockIndent)));
|
|
191
|
+
index++;
|
|
192
|
+
}
|
|
193
|
+
return blockLines;
|
|
194
|
+
};
|
|
115
195
|
const parseBlock = (indent) => {
|
|
116
196
|
skipBlank();
|
|
117
197
|
if (index >= lines.length)
|
|
@@ -164,25 +244,9 @@ export function parseSimpleYaml(raw) {
|
|
|
164
244
|
throw new Error(`Duplicate YAML key: ${key} near line: ${line.trim()}`);
|
|
165
245
|
}
|
|
166
246
|
seen.add(key);
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
while (index < lines.length) {
|
|
171
|
-
const blockLine = lines[index] ?? "";
|
|
172
|
-
if (blockLine.trim() === "") {
|
|
173
|
-
blockLines.push("");
|
|
174
|
-
index++;
|
|
175
|
-
continue;
|
|
176
|
-
}
|
|
177
|
-
const blockIndent = countIndent(blockLine);
|
|
178
|
-
if (blockIndent <= indent)
|
|
179
|
-
break;
|
|
180
|
-
contentIndent ??= blockIndent;
|
|
181
|
-
blockLines.push(blockLine.slice(Math.min(contentIndent, blockIndent)));
|
|
182
|
-
index++;
|
|
183
|
-
}
|
|
184
|
-
const content = blockLines.join("\n").replace(/\n+$/, "");
|
|
185
|
-
assign(key, rawValue === "|" ? `${content}\n` : content);
|
|
247
|
+
const blockHeader = parseBlockScalarHeader(rawValue);
|
|
248
|
+
if (blockHeader) {
|
|
249
|
+
assign(key, applyBlockScalar(collectBlockScalarLines(indent), blockHeader));
|
|
186
250
|
continue;
|
|
187
251
|
}
|
|
188
252
|
if (rawValue !== "") {
|
|
@@ -230,6 +294,11 @@ export function parseSimpleYaml(raw) {
|
|
|
230
294
|
}
|
|
231
295
|
continue;
|
|
232
296
|
}
|
|
297
|
+
const itemBlockHeader = parseBlockScalarHeader(rawItem);
|
|
298
|
+
if (itemBlockHeader) {
|
|
299
|
+
result.push(applyBlockScalar(collectBlockScalarLines(indent), itemBlockHeader));
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
233
302
|
const separator = findKeySeparator(rawItem);
|
|
234
303
|
if (separator !== -1) {
|
|
235
304
|
const key = rawItem.slice(0, separator).trim();
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Tool names in the guide that have no Pi slash command. */
|
|
2
|
+
export declare const MCP_ONLY_TOOLS: readonly ["codecarto_library_list", "codecarto_library_reindex"];
|
|
3
|
+
export declare const GUIDE_PREAMBLE: string;
|
|
4
|
+
export declare const PI_SURFACE_ADDENDUM: string;
|
|
5
|
+
/**
|
|
6
|
+
* Assemble the message /codecarto-guide queues. The guide document is embedded
|
|
7
|
+
* whole and unmodified between the framing and the addendum — `guide.test.mjs`
|
|
8
|
+
* pins that documents are served entire rather than summarized, and that holds
|
|
9
|
+
* on this surface too.
|
|
10
|
+
*/
|
|
11
|
+
export declare function buildPiGuideMessage(documentContent: string, otherTopics: readonly string[]): string;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// Surface framing for the packaged agent guide when it is read into a Pi
|
|
2
|
+
// session.
|
|
3
|
+
//
|
|
4
|
+
// Two problems this solves, both invisible on the MCP surface:
|
|
5
|
+
//
|
|
6
|
+
// 1. The guide arrives as a user message. /codecarto-guide queues the document
|
|
7
|
+
// through pi.sendUserMessage, so ~200 lines of imperative instructions land
|
|
8
|
+
// as if the user had typed them, with no task attached. A model handed
|
|
9
|
+
// instructions and no task either starts driving immediately or stalls
|
|
10
|
+
// asking what to do; both are wrong for what is a reference lookup.
|
|
11
|
+
//
|
|
12
|
+
// 2. The guide is written for the MCP surface. It tells the agent to call
|
|
13
|
+
// codecarto_* tools — but the Pi extension registers no tools at all, only
|
|
14
|
+
// slash commands the *user* invokes. A model that tries to follow it finds
|
|
15
|
+
// nothing to call and reasonably concludes the server is missing. The drive
|
|
16
|
+
// loop differs too: on Pi, /codecarto-next runs the phase as an isolated
|
|
17
|
+
// sub-agent and then auto-validates and auto-completes it, so the guide's
|
|
18
|
+
// hand-written execute → handoff → validate → complete loop does not
|
|
19
|
+
// describe a Pi session.
|
|
20
|
+
//
|
|
21
|
+
// The guide text itself stays untouched — agent-skill/ is the single source and
|
|
22
|
+
// core/guide.ts serves it verbatim to every surface. Per-surface adaptation
|
|
23
|
+
// belongs in the wrapper, which is here.
|
|
24
|
+
/** Tool names in the guide that have no Pi slash command. */
|
|
25
|
+
export const MCP_ONLY_TOOLS = ["codecarto_library_list", "codecarto_library_reindex"];
|
|
26
|
+
export const GUIDE_PREAMBLE = [
|
|
27
|
+
"**CodeCartographer guide — reference material, not a task.**",
|
|
28
|
+
"",
|
|
29
|
+
"The user ran `/codecarto-guide`, which queues the guide below into this session so it is available when needed. Nothing is being asked of you yet.",
|
|
30
|
+
"",
|
|
31
|
+
"Do not start a workflow, do not begin a phase, and do not ask which repository or pipeline to use. Acknowledge in a sentence that you have read it, then wait. Answer from it when the user asks.",
|
|
32
|
+
].join("\n");
|
|
33
|
+
export const PI_SURFACE_ADDENDUM = [
|
|
34
|
+
"---",
|
|
35
|
+
"",
|
|
36
|
+
"## Reading this guide in a Pi session",
|
|
37
|
+
"",
|
|
38
|
+
"The guide above is written for the MCP surface, where an agent drives the workflow by calling `codecarto_*` tools. **This session is the Pi extension, which registers no tools.** There is nothing named `codecarto_*` for you to call, and their absence does not mean a server is missing or misconfigured.",
|
|
39
|
+
"",
|
|
40
|
+
`- **Every tool name maps to a slash command the user runs**, mechanically: \`codecarto_status\` → \`/codecarto-status\`, \`codecarto_next\` → \`/codecarto-next\`, and so on. Two have no Pi equivalent: ${MCP_ONLY_TOOLS.map((name) => `\`${name}\``).join(" and ")}.`,
|
|
41
|
+
"- **Ignore \"every tool takes an absolute `cwd`\".** Slash commands act on the session's own directory; there is no `cwd` argument to pass.",
|
|
42
|
+
"- **The drive loop is different.** `/codecarto-next` executes the phase itself, as an isolated sub-agent, and then auto-validates and auto-completes it. The guide's hand-written loop — take the prompt, execute it, write the handoff, then validate and complete yourself — describes the MCP surface. On Pi the user drives and the extension executes; your job is to explain what the framework is doing and answer questions about it, not to reproduce that loop by hand.",
|
|
43
|
+
"",
|
|
44
|
+
"### How to drive a run",
|
|
45
|
+
"",
|
|
46
|
+
"`/codecarto-next` takes flags that change how much runs and how each phase is seeded. They are independent: `--auto` decides *how many phases run*, `--llm-steer` decides *what prompt each one gets*.",
|
|
47
|
+
"",
|
|
48
|
+
"| Invocation | What it does |",
|
|
49
|
+
"| --- | --- |",
|
|
50
|
+
"| `/codecarto-next` | Runs the next eligible phase, once. Good for watching a single phase or retrying one that stopped. |",
|
|
51
|
+
"| `/codecarto-next --auto` | Runs every remaining phase back to back, validating and completing each before starting the next. Stops on a validation failure or a sub-agent error. |",
|
|
52
|
+
"| `/codecarto-next --auto --llm-steer` | The same, with each phase's prompt rewritten from the previous phase's closeout. **This is the usual choice for a full run** — it is what makes phase N+1 aware of what phase N found. |",
|
|
53
|
+
"| `/codecarto-next --auto --strict --llm-steer` | The same, but also stops on `PASS WITH GAPS` instead of advancing through it. Use when gaps should be reviewed rather than carried forward. |",
|
|
54
|
+
"",
|
|
55
|
+
"Notes worth passing on when the user asks:",
|
|
56
|
+
"",
|
|
57
|
+
"- **The first phase is never steered** — there is no previous closeout to steer from, so it reports `LLM rewriter skipped (no previous phase to steer from)` and uses the stock prompt. That message is normal, not a failure.",
|
|
58
|
+
"- **Steering costs an extra model call per phase**, on top of the phase sub-agent itself.",
|
|
59
|
+
"- `--strict` is only valid with `--auto`; on its own it is an error.",
|
|
60
|
+
"- `--no-llm-steer` forces steering off for one invocation when the workspace config has it on (`orchestrator.llm_steer_next_phase`, default off).",
|
|
61
|
+
"- **An auto run that stops says why in its summary block.** If a phase produced its artifact but the pipeline still shows it incomplete, read the `Auto pipeline stopped at …` message rather than assuming the phase failed — the phase usually succeeded and something after it did not.",
|
|
62
|
+
"",
|
|
63
|
+
"If the user has just initialized a workspace and has not said what they want, tell them the run command rather than waiting to be asked: `/codecarto-next --auto --llm-steer` for a full pass, or plain `/codecarto-next` to watch one phase first.",
|
|
64
|
+
].join("\n");
|
|
65
|
+
/**
|
|
66
|
+
* Assemble the message /codecarto-guide queues. The guide document is embedded
|
|
67
|
+
* whole and unmodified between the framing and the addendum — `guide.test.mjs`
|
|
68
|
+
* pins that documents are served entire rather than summarized, and that holds
|
|
69
|
+
* on this surface too.
|
|
70
|
+
*/
|
|
71
|
+
export function buildPiGuideMessage(documentContent, otherTopics) {
|
|
72
|
+
const footer = otherTopics.length > 0
|
|
73
|
+
? `\n\n---\nOther guide topics: ${otherTopics.join(", ")} (run /codecarto-guide <topic>).`
|
|
74
|
+
: "";
|
|
75
|
+
return `${GUIDE_PREAMBLE}\n\n---\n\n${documentContent}\n\n${PI_SURFACE_ADDENDUM}${footer}`;
|
|
76
|
+
}
|
|
@@ -8,6 +8,7 @@ import { narrateDashboard } from "./dashboard-narrator.js";
|
|
|
8
8
|
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
|
+
import { buildPiGuideMessage } from "./guide-framing.js";
|
|
11
12
|
import { phaseCompactionExtension } from "./phase-compaction.js";
|
|
12
13
|
import { applyAmendment, 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, listAmendmentNames, listBatchModels, listGuideTopics, listScaffoldRefreshFiles, listSkillNames, loadAmendmentFile, loadBroadsideConfig, modelsText, runBroadsideCollect, runBroadsideStatus, runBroadsideSubmit, statusText, loadCodecartoConfig, loadUsage, loadYamlFile, normalizeForComparison, packagedWorkspaceDir, pathExists, PACKAGE_VERSION, readBroadsideSkill, readGuide, refreshScaffold, PhasePreflightError, PIPELINE_ALIASES, publishEntry, resolvePhase, resolvePipelineChoice, resolvePublishSourceRepo, SourceRepoMismatchError, runPhasePreflight, SCAFFOLD_REFRESH_PROTECTED, seedOrchestratorFiles, stringifySimpleYaml, switchPipeline, validatePhaseOutput, writeLibraryConfig, } from "../../core/index.js";
|
|
13
14
|
import { initLibrary } from "../../core/library.js";
|
|
@@ -498,8 +499,14 @@ export default function codeCartographerExtension(pi) {
|
|
|
498
499
|
// duties maintain so they exist from the first phase.
|
|
499
500
|
await seedOrchestratorFiles(targetWorkspaceDir);
|
|
500
501
|
codecartoModeActive = true;
|
|
501
|
-
|
|
502
|
-
|
|
502
|
+
// Name the run command here: init is the moment someone needs it, and
|
|
503
|
+
// the flags that make a full run useful are not guessable from the
|
|
504
|
+
// command name alone.
|
|
505
|
+
lastFeedbackLines = [
|
|
506
|
+
`Initialized workspace with pipeline: ${getPipelineLabel(selectedPipelinePath)}`,
|
|
507
|
+
"Full run: `/codecarto-next --auto --llm-steer` — or `/codecarto-next` to watch one phase first.",
|
|
508
|
+
];
|
|
509
|
+
ctx.ui.notify(`Initialized CodeCartographer (${getPipelineLabel(selectedPipelinePath)}). Full run: /codecarto-next --auto --llm-steer`, "info");
|
|
503
510
|
// Render the initial dashboard (empty usage, all phases pending) so
|
|
504
511
|
// the user sees the file exist immediately after /codecarto-init.
|
|
505
512
|
void writeDashboard(ctx.cwd, PACKAGE_VERSION);
|
|
@@ -572,11 +579,23 @@ export default function codeCartographerExtension(pi) {
|
|
|
572
579
|
},
|
|
573
580
|
});
|
|
574
581
|
pi.registerCommand("codecarto-next", {
|
|
575
|
-
description: "Run the next
|
|
582
|
+
description: "Run the next phase as a sub-agent. Full run: --auto --llm-steer. Add --strict to stop on PASS WITH GAPS.",
|
|
576
583
|
getArgumentCompletions: (prefix) => {
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
584
|
+
// Descriptions, not bare flag names: the completion list is the only
|
|
585
|
+
// place most users will ever see what these do, and the useful
|
|
586
|
+
// combination (--auto --llm-steer) is not guessable from the names.
|
|
587
|
+
// --strict is offered only once --auto is present, because on its own
|
|
588
|
+
// it is rejected — suggesting it standalone invites the one error the
|
|
589
|
+
// parser has.
|
|
590
|
+
const autoAlreadyTyped = prefix.includes("--auto");
|
|
591
|
+
const items = [
|
|
592
|
+
{ value: "--auto", label: "--auto", description: "run every remaining phase back to back (recommended with --llm-steer)" },
|
|
593
|
+
{ value: "--llm-steer", label: "--llm-steer", description: "seed each phase from the previous phase's closeout; no effect on the first phase" },
|
|
594
|
+
{ value: "--no-llm-steer", label: "--no-llm-steer", description: "force steering off when the workspace config turns it on" },
|
|
595
|
+
...(autoAlreadyTyped
|
|
596
|
+
? [{ value: "--strict", label: "--strict", description: "with --auto: stop on PASS WITH GAPS instead of advancing" }]
|
|
597
|
+
: []),
|
|
598
|
+
].filter((item) => item.value.startsWith(prefix.split(/\s+/).pop() ?? prefix));
|
|
580
599
|
return items.length > 0 ? items : null;
|
|
581
600
|
},
|
|
582
601
|
handler: async (args, ctx) => {
|
|
@@ -937,10 +956,10 @@ export default function codeCartographerExtension(pi) {
|
|
|
937
956
|
return;
|
|
938
957
|
}
|
|
939
958
|
const other = topics.filter((name) => name !== document.topic);
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
const message =
|
|
959
|
+
// Framed, not bare: the guide is MCP-centric text arriving as a user
|
|
960
|
+
// message, so it needs both a "this is reference, not a task" header
|
|
961
|
+
// and a Pi-surface addendum. See guide-framing.ts.
|
|
962
|
+
const message = buildPiGuideMessage(document.content, other);
|
|
944
963
|
if (ctx.isIdle()) {
|
|
945
964
|
pi.sendUserMessage(message);
|
|
946
965
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codecartographer-pi",
|
|
3
|
-
"version": "0.19.
|
|
3
|
+
"version": "0.19.4",
|
|
4
4
|
"mcpName": "io.github.HuginnIndustries/codecartographer",
|
|
5
5
|
"description": "Turn an unfamiliar codebase into a validated reimplementation spec, then synthesize confirmed specs and a product vision into a traceable plan.",
|
|
6
6
|
"type": "module",
|