codecartographer-pi 0.8.0 → 0.9.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 +2 -0
- package/README.md +36 -16
- package/assets/logo.svg +42 -0
- package/dist/core/dashboard.d.ts +6 -0
- package/dist/core/dashboard.js +396 -285
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.js +1 -0
- package/dist/core/library.d.ts +157 -0
- package/dist/core/library.js +675 -0
- package/dist/core/orchestrator-config.d.ts +38 -0
- package/dist/core/orchestrator-config.js +86 -19
- package/dist/core/pipeline.js +15 -3
- package/dist/core/prompts.d.ts +12 -1
- package/dist/core/prompts.js +15 -6
- package/dist/core/usage.js +15 -1
- package/dist/core/utils.d.ts +8 -0
- package/dist/core/utils.js +17 -1
- package/dist/extensions/codecarto/auto-runner.d.ts +16 -0
- package/dist/extensions/codecarto/auto-runner.js +7 -3
- package/dist/extensions/codecarto/dashboard-writer.js +26 -1
- package/dist/extensions/codecarto/index.js +23 -2
- package/dist/mcp-server/server.d.ts +21 -0
- package/dist/mcp-server/server.js +305 -3
- package/package.json +3 -2
|
@@ -6,15 +6,53 @@ export interface OrchestratorConfig {
|
|
|
6
6
|
* tokens, opt-in. */
|
|
7
7
|
llm_steer_next_phase: boolean;
|
|
8
8
|
}
|
|
9
|
+
export interface LibraryConfig {
|
|
10
|
+
/** Absolute, tilde-expanded path to the CodeCartographer library
|
|
11
|
+
* this workspace publishes to and reads from. Null if unconfigured —
|
|
12
|
+
* callers should prompt the user on first use. */
|
|
13
|
+
path: string | null;
|
|
14
|
+
/** Default namespace for entries published by this workspace.
|
|
15
|
+
* Null if unconfigured (single-tenant libraries should use null). */
|
|
16
|
+
namespace: string | null;
|
|
17
|
+
/** Whether `codecarto publish` should display a confirmation prompt
|
|
18
|
+
* with slug + source + library path before writing. Default true. */
|
|
19
|
+
publish_confirm: boolean;
|
|
20
|
+
}
|
|
9
21
|
export interface CodecartoConfig {
|
|
10
22
|
orchestrator: OrchestratorConfig;
|
|
23
|
+
library: LibraryConfig;
|
|
11
24
|
}
|
|
12
25
|
export declare const CONFIG_RELATIVE_PATH = "workflow/config.yaml";
|
|
26
|
+
export declare const USER_CONFIG_DIR: string;
|
|
27
|
+
export declare const USER_CONFIG_PATH: string;
|
|
28
|
+
/**
|
|
29
|
+
* Tests and tooling can override the user-global config path by setting
|
|
30
|
+
* `CODECARTO_USER_CONFIG_PATH`. The exported constant above is the default
|
|
31
|
+
* for documentation and onboarding flows. Internal load functions go
|
|
32
|
+
* through `resolveUserConfigPath()` so the override takes effect.
|
|
33
|
+
*/
|
|
34
|
+
export declare function resolveUserConfigPath(): string;
|
|
13
35
|
type RawConfig = {
|
|
14
36
|
orchestrator?: Partial<{
|
|
15
37
|
llm_steer_next_phase: unknown;
|
|
16
38
|
}>;
|
|
39
|
+
library?: Partial<{
|
|
40
|
+
path: unknown;
|
|
41
|
+
namespace: unknown;
|
|
42
|
+
publish_confirm: unknown;
|
|
43
|
+
}>;
|
|
17
44
|
};
|
|
18
45
|
export declare function loadCodecartoConfig(workspaceDir: PathLike): Promise<CodecartoConfig>;
|
|
46
|
+
/**
|
|
47
|
+
* Read the user-global config directly. Exposed so wrappers can show
|
|
48
|
+
* "your library is at <path>" in onboarding flows without having to
|
|
49
|
+
* load a workspace first.
|
|
50
|
+
*/
|
|
51
|
+
export declare function loadUserConfig(): Promise<CodecartoConfig>;
|
|
52
|
+
/**
|
|
53
|
+
* Apply one raw config layer over an existing config. Public so tests can
|
|
54
|
+
* exercise layering without filesystem fixtures, and so wrappers can mock
|
|
55
|
+
* a layer in memory (e.g. "what if library_path were X").
|
|
56
|
+
*/
|
|
19
57
|
export declare function mergeConfig(raw: RawConfig | null | undefined): CodecartoConfig;
|
|
20
58
|
export {};
|
|
@@ -1,45 +1,112 @@
|
|
|
1
|
-
// Workspace-level orchestrator configuration.
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
|
|
7
|
-
|
|
1
|
+
// Workspace-level orchestrator configuration. Two layers:
|
|
2
|
+
//
|
|
3
|
+
// 1. User-global — `~/.codecarto/config.yaml`. Default location for
|
|
4
|
+
// `library.path`, `library.namespace`, `library.publish_confirm`, and
|
|
5
|
+
// the orchestrator toggles. Shared across all workspaces on this
|
|
6
|
+
// machine.
|
|
7
|
+
// 2. Per-workspace — `.codecarto/workflow/config.yaml` inside the
|
|
8
|
+
// workspace. Overrides individual keys from the user-global layer.
|
|
9
|
+
//
|
|
10
|
+
// Resolution order (top wins): per-workspace > user-global > defaults.
|
|
11
|
+
// Missing files at either layer fall back to defaults; malformed YAML
|
|
12
|
+
// at either layer is non-fatal (drops the layer, logs nothing).
|
|
13
|
+
//
|
|
14
|
+
// `library.path` is returned tilde-expanded and absolute so consumers
|
|
15
|
+
// don't have to expand themselves.
|
|
16
|
+
import { homedir } from "node:os";
|
|
17
|
+
import { join, resolve } from "node:path";
|
|
18
|
+
import { expandTilde, pathExists } from "./utils.js";
|
|
8
19
|
import { loadYamlFile } from "./yaml.js";
|
|
9
20
|
export const CONFIG_RELATIVE_PATH = "workflow/config.yaml";
|
|
21
|
+
export const USER_CONFIG_DIR = join(homedir(), ".codecarto");
|
|
22
|
+
export const USER_CONFIG_PATH = join(USER_CONFIG_DIR, "config.yaml");
|
|
23
|
+
/**
|
|
24
|
+
* Tests and tooling can override the user-global config path by setting
|
|
25
|
+
* `CODECARTO_USER_CONFIG_PATH`. The exported constant above is the default
|
|
26
|
+
* for documentation and onboarding flows. Internal load functions go
|
|
27
|
+
* through `resolveUserConfigPath()` so the override takes effect.
|
|
28
|
+
*/
|
|
29
|
+
export function resolveUserConfigPath() {
|
|
30
|
+
return process.env.CODECARTO_USER_CONFIG_PATH ?? USER_CONFIG_PATH;
|
|
31
|
+
}
|
|
10
32
|
const DEFAULT_CONFIG = {
|
|
11
33
|
orchestrator: {
|
|
12
34
|
llm_steer_next_phase: false,
|
|
13
35
|
},
|
|
36
|
+
library: {
|
|
37
|
+
path: null,
|
|
38
|
+
namespace: null,
|
|
39
|
+
publish_confirm: true,
|
|
40
|
+
},
|
|
14
41
|
};
|
|
15
42
|
export async function loadCodecartoConfig(workspaceDir) {
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
43
|
+
const userRaw = await loadRawIfExists(resolveUserConfigPath());
|
|
44
|
+
const workspaceRaw = await loadRawIfExists(join(workspaceDir, CONFIG_RELATIVE_PATH));
|
|
45
|
+
return mergeLayered([userRaw, workspaceRaw]);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Read the user-global config directly. Exposed so wrappers can show
|
|
49
|
+
* "your library is at <path>" in onboarding flows without having to
|
|
50
|
+
* load a workspace first.
|
|
51
|
+
*/
|
|
52
|
+
export async function loadUserConfig() {
|
|
53
|
+
const userRaw = await loadRawIfExists(resolveUserConfigPath());
|
|
54
|
+
return mergeLayered([userRaw]);
|
|
55
|
+
}
|
|
56
|
+
async function loadRawIfExists(path) {
|
|
57
|
+
if (!(await pathExists(path)))
|
|
58
|
+
return null;
|
|
19
59
|
try {
|
|
20
|
-
|
|
21
|
-
return mergeConfig(raw);
|
|
60
|
+
return await loadYamlFile(path);
|
|
22
61
|
}
|
|
23
62
|
catch {
|
|
24
|
-
|
|
25
|
-
// command. The user can fix it; a broken config shouldn't block work.
|
|
26
|
-
return cloneDefault();
|
|
63
|
+
return null;
|
|
27
64
|
}
|
|
28
65
|
}
|
|
66
|
+
function mergeLayered(layers) {
|
|
67
|
+
let merged = cloneDefault();
|
|
68
|
+
for (const layer of layers)
|
|
69
|
+
merged = applyRaw(merged, layer);
|
|
70
|
+
return merged;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Apply one raw config layer over an existing config. Public so tests can
|
|
74
|
+
* exercise layering without filesystem fixtures, and so wrappers can mock
|
|
75
|
+
* a layer in memory (e.g. "what if library_path were X").
|
|
76
|
+
*/
|
|
29
77
|
export function mergeConfig(raw) {
|
|
30
|
-
|
|
78
|
+
return applyRaw(cloneDefault(), raw);
|
|
79
|
+
}
|
|
80
|
+
function applyRaw(base, raw) {
|
|
31
81
|
if (!raw || typeof raw !== "object")
|
|
32
|
-
return
|
|
82
|
+
return base;
|
|
83
|
+
const out = {
|
|
84
|
+
orchestrator: { ...base.orchestrator },
|
|
85
|
+
library: { ...base.library },
|
|
86
|
+
};
|
|
33
87
|
const o = raw.orchestrator;
|
|
34
88
|
if (o && typeof o === "object") {
|
|
35
89
|
if (typeof o.llm_steer_next_phase === "boolean") {
|
|
36
|
-
|
|
90
|
+
out.orchestrator.llm_steer_next_phase = o.llm_steer_next_phase;
|
|
37
91
|
}
|
|
38
92
|
}
|
|
39
|
-
|
|
93
|
+
const l = raw.library;
|
|
94
|
+
if (l && typeof l === "object") {
|
|
95
|
+
if (typeof l.path === "string" && l.path.trim() !== "") {
|
|
96
|
+
out.library.path = resolve(expandTilde(l.path.trim()));
|
|
97
|
+
}
|
|
98
|
+
if (typeof l.namespace === "string" && l.namespace.trim() !== "") {
|
|
99
|
+
out.library.namespace = l.namespace.trim();
|
|
100
|
+
}
|
|
101
|
+
if (typeof l.publish_confirm === "boolean") {
|
|
102
|
+
out.library.publish_confirm = l.publish_confirm;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return out;
|
|
40
106
|
}
|
|
41
107
|
function cloneDefault() {
|
|
42
108
|
return {
|
|
43
109
|
orchestrator: { ...DEFAULT_CONFIG.orchestrator },
|
|
110
|
+
library: { ...DEFAULT_CONFIG.library },
|
|
44
111
|
};
|
|
45
112
|
}
|
package/dist/core/pipeline.js
CHANGED
|
@@ -38,10 +38,22 @@ export function getNextEligiblePhase(state) {
|
|
|
38
38
|
}
|
|
39
39
|
export function resolvePhase(state, phaseId) {
|
|
40
40
|
const trimmed = phaseId?.trim();
|
|
41
|
-
if (trimmed)
|
|
42
|
-
return
|
|
41
|
+
if (!trimmed)
|
|
42
|
+
return getNextEligiblePhase(state);
|
|
43
|
+
const exact = getPhaseMap(state.pipeline).get(trimmed);
|
|
44
|
+
if (exact)
|
|
45
|
+
return exact;
|
|
46
|
+
// Fall back to matching the primary_output filename. Validation errors
|
|
47
|
+
// surface that path (e.g. "Missing primary output: .codecarto/findings/
|
|
48
|
+
// protocols/protocols-and-state.md"), so users naturally paste it back as
|
|
49
|
+
// the phase argument. Accept the basename with or without the .md suffix.
|
|
50
|
+
const wanted = basename(trimmed, ".md");
|
|
51
|
+
for (const phase of state.pipeline.phases) {
|
|
52
|
+
if (phase.primary_output && basename(phase.primary_output, ".md") === wanted) {
|
|
53
|
+
return phase;
|
|
54
|
+
}
|
|
43
55
|
}
|
|
44
|
-
return
|
|
56
|
+
return null;
|
|
45
57
|
}
|
|
46
58
|
export function resolvePipelineChoice(input) {
|
|
47
59
|
const trimmed = input.trim();
|
package/dist/core/prompts.d.ts
CHANGED
|
@@ -1,7 +1,18 @@
|
|
|
1
1
|
import type { CarryForwardEntry, OpenQuestionEntry, PipelinePhase, ValidationResult, WorkspaceState } from "./types.ts";
|
|
2
2
|
export declare function describeEntry(entry: OpenQuestionEntry | CarryForwardEntry): string;
|
|
3
3
|
export declare function collectRoutedCarryForward(state: WorkspaceState, targetPhaseId: string): CarryForwardEntry[];
|
|
4
|
-
export
|
|
4
|
+
export interface BuildPhasePromptOptions {
|
|
5
|
+
/**
|
|
6
|
+
* Set when the phase is being run inside `/codecarto-next --auto` (or any
|
|
7
|
+
* other non-interactive driver). Suppresses interactive hooks that would
|
|
8
|
+
* otherwise pause the sub-agent to ask the user a question — those hooks
|
|
9
|
+
* are the dominant cause of the auto loop wedging at `reimplementation-spec`
|
|
10
|
+
* with a `MISSING` primary output. Each suppressed hook documents the
|
|
11
|
+
* default it falls back to.
|
|
12
|
+
*/
|
|
13
|
+
auto?: boolean;
|
|
14
|
+
}
|
|
15
|
+
export declare function buildPhasePrompt(state: WorkspaceState, phase: PipelinePhase, forced: boolean, options?: BuildPhasePromptOptions): Promise<string>;
|
|
5
16
|
export declare function closeoutFileName(date: string, phaseOrModule: string): string;
|
|
6
17
|
export declare function buildThreadLogEntry(phaseOrModule: string, validation: ValidationResult, timestamp: string): string;
|
|
7
18
|
export declare function ensureCloseoutStub(workspaceDir: string, phaseOrModule: string, timestamp: string): Promise<string | null>;
|
package/dist/core/prompts.js
CHANGED
|
@@ -26,7 +26,7 @@ export function collectRoutedCarryForward(state, targetPhaseId) {
|
|
|
26
26
|
}
|
|
27
27
|
return routed;
|
|
28
28
|
}
|
|
29
|
-
export async function buildPhasePrompt(state, phase, forced) {
|
|
29
|
+
export async function buildPhasePrompt(state, phase, forced, options = {}) {
|
|
30
30
|
const lines = [
|
|
31
31
|
`Read .codecarto/GUIDE.md and continue the CodeCartographer workflow for the phase \`${phase.id}\`.`,
|
|
32
32
|
`Work on this phase only. The analyzed source code is the repository outside .codecarto/.`,
|
|
@@ -66,11 +66,20 @@ export async function buildPhasePrompt(state, phase, forced) {
|
|
|
66
66
|
}
|
|
67
67
|
if (phase.id === "reimplementation-spec") {
|
|
68
68
|
lines.push("");
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
69
|
+
if (options.auto) {
|
|
70
|
+
lines.push("Strategic Alignment Hook (auto run — DO NOT ask the user):");
|
|
71
|
+
lines.push("- This phase is running inside `/codecarto-next --auto`. The user is not in the loop.");
|
|
72
|
+
lines.push("- Default the spec to LANGUAGE-AGNOSTIC: use templates/reimplementation-spec.md.");
|
|
73
|
+
lines.push("- Record `variant: language-agnostic` and `selection: auto-default` in the spec front-matter and in your validation block, so a later opinionated re-run is traceable.");
|
|
74
|
+
lines.push("- Do NOT block on the user. If you would otherwise pause to ask about target stack, project name, or scope cuts, instead produce the language-agnostic spec and capture each unresolved choice as an `open_questions` entry (`kind: needs-maintainer-decision`) for follow-up.");
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
lines.push("Strategic Alignment Hook (run BEFORE producing the spec):");
|
|
78
|
+
lines.push("- Confirm with the user whether this spec should be language-agnostic or opinionated:");
|
|
79
|
+
lines.push(" - language-agnostic → use templates/reimplementation-spec.md (default).");
|
|
80
|
+
lines.push(" - opinionated (target stack locked) → use templates/reimplementation-spec-opinionated.md.");
|
|
81
|
+
lines.push("- Record the chosen variant in the spec front-matter and in your validation block.");
|
|
82
|
+
}
|
|
74
83
|
}
|
|
75
84
|
lines.push("", "Rules:");
|
|
76
85
|
lines.push("- Do not modify source files outside .codecarto/.");
|
package/dist/core/usage.js
CHANGED
|
@@ -90,5 +90,19 @@ function isUsageRun(x) {
|
|
|
90
90
|
const r = x;
|
|
91
91
|
return (typeof r.timestamp === "string" &&
|
|
92
92
|
typeof r.phase === "string" &&
|
|
93
|
-
|
|
93
|
+
(r.status === "completed" || r.status === "aborted" || r.status === "error") &&
|
|
94
|
+
isFiniteNumber(r.turn_count) &&
|
|
95
|
+
isFiniteNumber(r.tool_uses) &&
|
|
96
|
+
isFiniteNumber(r.duration_ms) &&
|
|
97
|
+
isUsageTokens(r.tokens) &&
|
|
98
|
+
(r.session_file === undefined || typeof r.session_file === "string"));
|
|
99
|
+
}
|
|
100
|
+
function isUsageTokens(x) {
|
|
101
|
+
if (!x || typeof x !== "object")
|
|
102
|
+
return false;
|
|
103
|
+
const t = x;
|
|
104
|
+
return isFiniteNumber(t.input) && isFiniteNumber(t.output) && isFiniteNumber(t.cache_write);
|
|
105
|
+
}
|
|
106
|
+
function isFiniteNumber(x) {
|
|
107
|
+
return typeof x === "number" && Number.isFinite(x);
|
|
94
108
|
}
|
package/dist/core/utils.d.ts
CHANGED
|
@@ -6,6 +6,14 @@ export declare function isWithinPath(path: string, root: string): boolean;
|
|
|
6
6
|
export declare function isPlainObject(value: unknown): value is Record<string, unknown>;
|
|
7
7
|
export declare function uniqueStrings(items: string[]): string[];
|
|
8
8
|
export declare function dateOnly(timestamp: string): string;
|
|
9
|
+
/**
|
|
10
|
+
* Expand a leading `~` or `~/` to the user's home directory. Node's `path`
|
|
11
|
+
* module deliberately doesn't do this (it's a shell convention, not a path
|
|
12
|
+
* primitive), so callers that accept user-typed paths (config files,
|
|
13
|
+
* `library.path`) need to expand explicitly before passing to `resolve`.
|
|
14
|
+
* Paths without a leading tilde are returned unchanged.
|
|
15
|
+
*/
|
|
16
|
+
export declare function expandTilde(path: string): string;
|
|
9
17
|
/**
|
|
10
18
|
* Format an integer as `2.50M` / `2.3k` / `500`. Used by the HTML dashboard
|
|
11
19
|
* for compact numeric cells. The widget and notify paths have their own
|
package/dist/core/utils.js
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
// path-boundary enforcement (Pi tool interception, MCP cwd validation).
|
|
3
3
|
import { access } from "node:fs/promises";
|
|
4
4
|
import { constants } from "node:fs";
|
|
5
|
-
import {
|
|
5
|
+
import { homedir } from "node:os";
|
|
6
|
+
import { join, normalize, resolve } from "node:path";
|
|
6
7
|
import { realpath } from "node:fs/promises";
|
|
7
8
|
export function sleep(ms) {
|
|
8
9
|
return new Promise((resolvePromise) => setTimeout(resolvePromise, ms));
|
|
@@ -44,6 +45,21 @@ export function uniqueStrings(items) {
|
|
|
44
45
|
export function dateOnly(timestamp) {
|
|
45
46
|
return timestamp.slice(0, 10);
|
|
46
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* Expand a leading `~` or `~/` to the user's home directory. Node's `path`
|
|
50
|
+
* module deliberately doesn't do this (it's a shell convention, not a path
|
|
51
|
+
* primitive), so callers that accept user-typed paths (config files,
|
|
52
|
+
* `library.path`) need to expand explicitly before passing to `resolve`.
|
|
53
|
+
* Paths without a leading tilde are returned unchanged.
|
|
54
|
+
*/
|
|
55
|
+
export function expandTilde(path) {
|
|
56
|
+
if (path === "~")
|
|
57
|
+
return homedir();
|
|
58
|
+
if (path.startsWith("~/") || path.startsWith("~\\")) {
|
|
59
|
+
return join(homedir(), path.slice(2));
|
|
60
|
+
}
|
|
61
|
+
return path;
|
|
62
|
+
}
|
|
47
63
|
/**
|
|
48
64
|
* Format an integer as `2.50M` / `2.3k` / `500`. Used by the HTML dashboard
|
|
49
65
|
* for compact numeric cells. The widget and notify paths have their own
|
|
@@ -4,6 +4,14 @@ import { type PipelinePhase, type ValidationOverall, type ValidationResult, type
|
|
|
4
4
|
export interface RunSinglePhaseOptions {
|
|
5
5
|
llmSteerEnabled: boolean;
|
|
6
6
|
signal?: AbortSignal;
|
|
7
|
+
/**
|
|
8
|
+
* True when this phase is being driven by `/codecarto-next --auto`. The flag
|
|
9
|
+
* propagates into `buildPhasePrompt` so interactive hooks (notably the
|
|
10
|
+
* `reimplementation-spec` Strategic Alignment Hook) suppress their user-
|
|
11
|
+
* facing question and fall back to a documented default. The one-shot
|
|
12
|
+
* `/codecarto-next` path leaves this unset and the prompt is unchanged.
|
|
13
|
+
*/
|
|
14
|
+
auto?: boolean;
|
|
7
15
|
}
|
|
8
16
|
export interface SinglePhaseResult {
|
|
9
17
|
status: "completed" | "aborted" | "error";
|
|
@@ -38,6 +46,14 @@ export interface AutoRunOptions {
|
|
|
38
46
|
strict: boolean;
|
|
39
47
|
llmSteerOverride?: boolean;
|
|
40
48
|
signal?: AbortSignal;
|
|
49
|
+
/**
|
|
50
|
+
* Fired after each phase is auto-completed and `state` has advanced to the
|
|
51
|
+
* next eligible phase. Lets the caller refresh UI that reflects pipeline
|
|
52
|
+
* progress (the status widget, session name) mid-run instead of only after
|
|
53
|
+
* the whole loop returns — otherwise the readout stays frozen at the
|
|
54
|
+
* initial phase/progress until the auto run finishes.
|
|
55
|
+
*/
|
|
56
|
+
onPhaseAdvanced?: (state: WorkspaceState) => void;
|
|
41
57
|
}
|
|
42
58
|
export interface AutoRunResult {
|
|
43
59
|
outcome: AutoOutcome;
|
|
@@ -29,7 +29,7 @@ import { appendUsageRun, buildPhasePrompt, buildThreadLogEntry, buildValidationS
|
|
|
29
29
|
* getPhaseActivity) and attached the agents widget if it wanted live progress.
|
|
30
30
|
*/
|
|
31
31
|
export async function runSinglePhase(ctx, pi, state, phase, options) {
|
|
32
|
-
let prompt = await buildPhasePrompt(state, phase, false);
|
|
32
|
+
let prompt = await buildPhasePrompt(state, phase, false, { auto: options.auto === true });
|
|
33
33
|
if (options.llmSteerEnabled) {
|
|
34
34
|
if (ctx.hasUI)
|
|
35
35
|
ctx.ui.notify(`Customizing ${phase.id} prompt via LLM rewriter…`, "info");
|
|
@@ -87,10 +87,11 @@ export async function runSinglePhase(ctx, pi, state, phase, options) {
|
|
|
87
87
|
tokens: activity.lifetimeUsage,
|
|
88
88
|
durationMs: (activity.completedAt ?? Date.now()) - activity.startedAt,
|
|
89
89
|
responseText: result.responseText,
|
|
90
|
+
sessionFile: result.sessionFile,
|
|
90
91
|
}),
|
|
91
92
|
display: true,
|
|
92
93
|
});
|
|
93
|
-
void recordUsage(state.workspaceDir, phase.id, status, activity);
|
|
94
|
+
void recordUsage(state.workspaceDir, phase.id, status, activity, result.sessionFile);
|
|
94
95
|
void writeDashboard(ctx.cwd, PACKAGE_VERSION);
|
|
95
96
|
return {
|
|
96
97
|
status: result.aborted ? "aborted" : "completed",
|
|
@@ -264,6 +265,7 @@ export async function runAuto(ctx, pi, initialState, options) {
|
|
|
264
265
|
const phaseResult = await runSinglePhase(ctx, pi, state, phase, {
|
|
265
266
|
llmSteerEnabled,
|
|
266
267
|
signal: options.signal,
|
|
268
|
+
auto: true,
|
|
267
269
|
});
|
|
268
270
|
// Accumulate tokens whether the phase succeeded, was aborted, or errored.
|
|
269
271
|
totalTokens.input += phaseResult.activity.lifetimeUsage.input;
|
|
@@ -300,6 +302,7 @@ export async function runAuto(ctx, pi, initialState, options) {
|
|
|
300
302
|
const { updatedState } = await autoCompletePhase(ctx, validation);
|
|
301
303
|
state = updatedState;
|
|
302
304
|
phasesRun.push(phase.id);
|
|
305
|
+
options.onPhaseAdvanced?.(state);
|
|
303
306
|
}
|
|
304
307
|
catch (err) {
|
|
305
308
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -377,7 +380,7 @@ function recoveryHint(result) {
|
|
|
377
380
|
// ----------------------------------------------------------------------------
|
|
378
381
|
// Helpers
|
|
379
382
|
// ----------------------------------------------------------------------------
|
|
380
|
-
async function recordUsage(workspaceDir, phaseId, status, activity) {
|
|
383
|
+
async function recordUsage(workspaceDir, phaseId, status, activity, sessionFile) {
|
|
381
384
|
try {
|
|
382
385
|
await appendUsageRun(workspaceDir, {
|
|
383
386
|
timestamp: new Date().toISOString(),
|
|
@@ -391,6 +394,7 @@ async function recordUsage(workspaceDir, phaseId, status, activity) {
|
|
|
391
394
|
output: activity.lifetimeUsage.output,
|
|
392
395
|
cache_write: activity.lifetimeUsage.cacheWrite,
|
|
393
396
|
},
|
|
397
|
+
...(sessionFile ? { session_file: sessionFile } : {}),
|
|
394
398
|
});
|
|
395
399
|
}
|
|
396
400
|
catch {
|
|
@@ -56,10 +56,35 @@ async function listCloseouts(workspaceDir) {
|
|
|
56
56
|
const m = CLOSEOUT_FILENAME_RE.exec(name);
|
|
57
57
|
if (!m)
|
|
58
58
|
continue;
|
|
59
|
-
out.push({ date: m[1], phaseOrModule: m[2], fileName: name });
|
|
59
|
+
out.push({ date: m[1], phaseOrModule: m[2], fileName: name, summary: await readCloseoutSummary(join(dir, name)) });
|
|
60
60
|
}
|
|
61
61
|
return out;
|
|
62
62
|
}
|
|
63
|
+
async function readCloseoutSummary(path) {
|
|
64
|
+
try {
|
|
65
|
+
const raw = await readFile(path, "utf8");
|
|
66
|
+
const lines = raw.split(/\r?\n/);
|
|
67
|
+
const summaryStart = lines.findIndex((line) => /^##\s+Summary\s*$/i.test(line.trim()));
|
|
68
|
+
if (summaryStart === -1)
|
|
69
|
+
return undefined;
|
|
70
|
+
const body = [];
|
|
71
|
+
for (const line of lines.slice(summaryStart + 1)) {
|
|
72
|
+
if (/^##\s+/.test(line.trim()))
|
|
73
|
+
break;
|
|
74
|
+
const trimmed = line.trim();
|
|
75
|
+
if (!trimmed || trimmed === "-")
|
|
76
|
+
continue;
|
|
77
|
+
body.push(trimmed.replace(/^[-*]\s+/, ""));
|
|
78
|
+
if (body.join(" ").length > 280)
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
const summary = body.join(" ").trim();
|
|
82
|
+
return summary ? `${summary.slice(0, 280)}${summary.length > 280 ? "…" : ""}` : undefined;
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
63
88
|
async function buildOutputsPresent(workspaceDir, pipeline) {
|
|
64
89
|
const out = new Map();
|
|
65
90
|
for (const phaseId of pipeline.phase_order) {
|
|
@@ -227,6 +227,15 @@ export default function codeCartographerExtension(pi) {
|
|
|
227
227
|
strict: flags.strict,
|
|
228
228
|
llmSteerOverride: flags.llmSteerOverride,
|
|
229
229
|
signal: ctx.signal,
|
|
230
|
+
onPhaseAdvanced: (advancedState) => {
|
|
231
|
+
// Refresh the status widget + session name between phases so
|
|
232
|
+
// the readout tracks progress live instead of staying frozen
|
|
233
|
+
// at the initial phase until the whole auto run finishes.
|
|
234
|
+
setUiState(ctx, advancedState, [`Auto pipeline${flags.strict ? " (strict)" : ""} running…`]);
|
|
235
|
+
const phaseId = getNextEligiblePhase(advancedState)?.id ?? advancedState.status.current_phase;
|
|
236
|
+
if (phaseId)
|
|
237
|
+
pi.setSessionName(`CodeCartographer: ${phaseId}`);
|
|
238
|
+
},
|
|
230
239
|
});
|
|
231
240
|
const availableSkills = await listSkillNames(state.workspaceDir).catch(() => []);
|
|
232
241
|
pi.sendMessage({
|
|
@@ -302,7 +311,13 @@ export default function codeCartographerExtension(pi) {
|
|
|
302
311
|
const state = await ensureWorkspaceState(ctx);
|
|
303
312
|
if (!state)
|
|
304
313
|
return;
|
|
305
|
-
const validation = await validatePhaseOutput(state, args.trim() || undefined);
|
|
314
|
+
const validation = await validatePhaseOutput(state, args.trim() || undefined).catch((error) => error instanceof Error ? error : new Error(String(error)));
|
|
315
|
+
if (validation instanceof Error) {
|
|
316
|
+
lastFeedbackLines = [validation.message];
|
|
317
|
+
setUiState(ctx, state, lastFeedbackLines);
|
|
318
|
+
ctx.ui.notify(validation.message, "error");
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
306
321
|
lastFeedbackLines = buildValidationSummary(validation);
|
|
307
322
|
setUiState(ctx, state, lastFeedbackLines);
|
|
308
323
|
const level = validation.overall === "FAIL" || validation.overall === "MISSING" ? "error" : validation.overall === "PASS WITH GAPS" ? "warning" : "info";
|
|
@@ -315,7 +330,13 @@ export default function codeCartographerExtension(pi) {
|
|
|
315
330
|
const currentState = await ensureWorkspaceState(ctx);
|
|
316
331
|
if (!currentState)
|
|
317
332
|
return;
|
|
318
|
-
const validation = await validatePhaseOutput(currentState, args.trim() || undefined);
|
|
333
|
+
const validation = await validatePhaseOutput(currentState, args.trim() || undefined).catch((error) => error instanceof Error ? error : new Error(String(error)));
|
|
334
|
+
if (validation instanceof Error) {
|
|
335
|
+
lastFeedbackLines = [validation.message];
|
|
336
|
+
setUiState(ctx, currentState, lastFeedbackLines);
|
|
337
|
+
ctx.ui.notify(validation.message, "error");
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
319
340
|
if (validation.overall === "FAIL" || validation.overall === "MISSING") {
|
|
320
341
|
lastFeedbackLines = buildValidationSummary(validation);
|
|
321
342
|
setUiState(ctx, currentState, lastFeedbackLines);
|
|
@@ -68,6 +68,27 @@ export declare function handleSkill(args: {
|
|
|
68
68
|
}>;
|
|
69
69
|
structuredContent?: Record<string, unknown>;
|
|
70
70
|
}>;
|
|
71
|
+
export declare function handlePublish(args: Record<string, unknown>): Promise<{
|
|
72
|
+
content: Array<{
|
|
73
|
+
type: "text";
|
|
74
|
+
text: string;
|
|
75
|
+
}>;
|
|
76
|
+
structuredContent?: Record<string, unknown>;
|
|
77
|
+
}>;
|
|
78
|
+
export declare function handleLibraryList(args: Record<string, unknown>): Promise<{
|
|
79
|
+
content: Array<{
|
|
80
|
+
type: "text";
|
|
81
|
+
text: string;
|
|
82
|
+
}>;
|
|
83
|
+
structuredContent?: Record<string, unknown>;
|
|
84
|
+
}>;
|
|
85
|
+
export declare function handleLibraryReindex(args: Record<string, unknown>): Promise<{
|
|
86
|
+
content: Array<{
|
|
87
|
+
type: "text";
|
|
88
|
+
text: string;
|
|
89
|
+
}>;
|
|
90
|
+
structuredContent?: Record<string, unknown>;
|
|
91
|
+
}>;
|
|
71
92
|
export declare function buildServer(): Server<{
|
|
72
93
|
method: string;
|
|
73
94
|
params?: {
|