codecartographer-pi 0.6.1 → 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 +275 -219
- package/assets/logo.svg +42 -0
- package/dist/core/dashboard.d.ts +42 -0
- package/dist/core/dashboard.js +637 -0
- package/dist/core/index.d.ts +2 -0
- package/dist/core/index.js +2 -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 +21 -0
- package/dist/core/utils.js +44 -1
- package/dist/core/workspace.d.ts +1 -0
- package/dist/core/workspace.js +13 -1
- package/dist/extensions/codecarto/auto-runner.d.ts +96 -0
- package/dist/extensions/codecarto/auto-runner.js +403 -0
- package/dist/extensions/codecarto/dashboard-flags.d.ts +6 -0
- package/dist/extensions/codecarto/dashboard-flags.js +17 -0
- package/dist/extensions/codecarto/dashboard-narrator.d.ts +8 -0
- package/dist/extensions/codecarto/dashboard-narrator.js +182 -0
- package/dist/extensions/codecarto/dashboard-writer.d.ts +1 -0
- package/dist/extensions/codecarto/dashboard-writer.js +148 -0
- package/dist/extensions/codecarto/index.js +98 -208
- package/dist/extensions/codecarto/next-flags.d.ts +4 -0
- package/dist/extensions/codecarto/next-flags.js +19 -6
- 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,3 +6,24 @@ 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;
|
|
17
|
+
/**
|
|
18
|
+
* Format an integer as `2.50M` / `2.3k` / `500`. Used by the HTML dashboard
|
|
19
|
+
* for compact numeric cells. The widget and notify paths have their own
|
|
20
|
+
* formatters that include " tokens" / unit suffixes inline; this helper is
|
|
21
|
+
* deliberately suffix-free so callers attach units in surrounding markup.
|
|
22
|
+
*/
|
|
23
|
+
export declare function formatTokenCount(count: number): string;
|
|
24
|
+
/**
|
|
25
|
+
* Format a millisecond duration as `2m30s` / `1.5s` / `500ms`. Matches the
|
|
26
|
+
* extension widget's `formatDuration` shape; promoted to `core/` so the
|
|
27
|
+
* dashboard renderer can reuse without crossing the core/extensions boundary.
|
|
28
|
+
*/
|
|
29
|
+
export declare function formatMillis(ms: number): string;
|
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,3 +45,45 @@ 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
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Format an integer as `2.50M` / `2.3k` / `500`. Used by the HTML dashboard
|
|
65
|
+
* for compact numeric cells. The widget and notify paths have their own
|
|
66
|
+
* formatters that include " tokens" / unit suffixes inline; this helper is
|
|
67
|
+
* deliberately suffix-free so callers attach units in surrounding markup.
|
|
68
|
+
*/
|
|
69
|
+
export function formatTokenCount(count) {
|
|
70
|
+
if (count >= 1_000_000)
|
|
71
|
+
return `${(count / 1_000_000).toFixed(2)}M`;
|
|
72
|
+
if (count >= 1_000)
|
|
73
|
+
return `${(count / 1_000).toFixed(1)}k`;
|
|
74
|
+
return `${count}`;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Format a millisecond duration as `2m30s` / `1.5s` / `500ms`. Matches the
|
|
78
|
+
* extension widget's `formatDuration` shape; promoted to `core/` so the
|
|
79
|
+
* dashboard renderer can reuse without crossing the core/extensions boundary.
|
|
80
|
+
*/
|
|
81
|
+
export function formatMillis(ms) {
|
|
82
|
+
if (ms < 1000)
|
|
83
|
+
return `${ms}ms`;
|
|
84
|
+
if (ms < 60_000)
|
|
85
|
+
return `${(ms / 1000).toFixed(1)}s`;
|
|
86
|
+
const minutes = Math.floor(ms / 60_000);
|
|
87
|
+
const seconds = Math.floor((ms % 60_000) / 1000);
|
|
88
|
+
return `${minutes}m${seconds.toString().padStart(2, "0")}s`;
|
|
89
|
+
}
|
package/dist/core/workspace.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { WorkspaceState } from "./types.ts";
|
|
2
2
|
export declare const packagedWorkspaceDir: string;
|
|
3
|
+
export declare const PACKAGE_VERSION: string;
|
|
3
4
|
export declare function getWorkspaceState(cwd: string): Promise<WorkspaceState | null>;
|
|
4
5
|
export declare function updateStatusAtomically(cwd: string, updater: (state: WorkspaceState) => Promise<{
|
|
5
6
|
state: WorkspaceState;
|
package/dist/core/workspace.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// (so the MCP server and Pi can both copy from it on /codecarto-init), loads
|
|
3
3
|
// + normalizes the per-project workspace state from disk, and provides the
|
|
4
4
|
// atomic status-update primitive used by /codecarto-complete.
|
|
5
|
-
import { existsSync } from "node:fs";
|
|
5
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
6
6
|
import { appendFile, rename, writeFile } from "node:fs/promises";
|
|
7
7
|
import { dirname, join, relative } from "node:path";
|
|
8
8
|
import { fileURLToPath } from "node:url";
|
|
@@ -30,6 +30,18 @@ const packageRoot = findPackageRoot(coreDir);
|
|
|
30
30
|
// Path to the packaged framework template directory. Wrappers copy this on
|
|
31
31
|
// /codecarto-init.
|
|
32
32
|
export const packagedWorkspaceDir = join(packageRoot, ".codecarto");
|
|
33
|
+
// Resolved at module-load time from the same package.json that findPackageRoot
|
|
34
|
+
// located. Used by the HTML dashboard renderer for the footer; cheap to read
|
|
35
|
+
// once since startup is already paying for findPackageRoot.
|
|
36
|
+
export const PACKAGE_VERSION = (() => {
|
|
37
|
+
try {
|
|
38
|
+
const pkg = JSON.parse(readFileSync(join(packageRoot, "package.json"), "utf8"));
|
|
39
|
+
return typeof pkg.version === "string" ? pkg.version : "0.0.0";
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return "0.0.0";
|
|
43
|
+
}
|
|
44
|
+
})();
|
|
33
45
|
export async function getWorkspaceState(cwd) {
|
|
34
46
|
const workspaceDir = join(cwd, ".codecarto");
|
|
35
47
|
const statusPath = join(workspaceDir, "workflow", "status.yaml");
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { type PhaseActivity } from "./agent-state.ts";
|
|
3
|
+
import { type PipelinePhase, type ValidationOverall, type ValidationResult, type WorkspaceState } from "../../core/index.ts";
|
|
4
|
+
export interface RunSinglePhaseOptions {
|
|
5
|
+
llmSteerEnabled: boolean;
|
|
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;
|
|
15
|
+
}
|
|
16
|
+
export interface SinglePhaseResult {
|
|
17
|
+
status: "completed" | "aborted" | "error";
|
|
18
|
+
activity: PhaseActivity;
|
|
19
|
+
error?: string;
|
|
20
|
+
responseText?: string;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Run one phase end to end: optional LLM-steered rewrite, spawn the sub-agent,
|
|
24
|
+
* wait for it, then emit the side effects the historical /codecarto-next chain
|
|
25
|
+
* fired (notify, phase-summary sendMessage, recordUsage, writeDashboard). The
|
|
26
|
+
* .finally linger-timeout for clearPhase fires here so both callers (one-shot
|
|
27
|
+
* + auto loop) get the same lifecycle.
|
|
28
|
+
*
|
|
29
|
+
* Pre-conditions: caller verified the phase isn't already running (via
|
|
30
|
+
* getPhaseActivity) and attached the agents widget if it wanted live progress.
|
|
31
|
+
*/
|
|
32
|
+
export declare function runSinglePhase(ctx: ExtensionContext, pi: ExtensionAPI, state: WorkspaceState, phase: PipelinePhase, options: RunSinglePhaseOptions): Promise<SinglePhaseResult>;
|
|
33
|
+
/**
|
|
34
|
+
* Re-entry guard: returns true if the phase is already running from a prior
|
|
35
|
+
* spawn (manual or auto). Callers (both /codecarto-next paths) should reject
|
|
36
|
+
* before invoking runSinglePhase.
|
|
37
|
+
*/
|
|
38
|
+
export declare function isPhaseRunning(phaseId: string): boolean;
|
|
39
|
+
export interface AutoCompleteResult {
|
|
40
|
+
updatedState: WorkspaceState;
|
|
41
|
+
closeoutNotice?: string;
|
|
42
|
+
}
|
|
43
|
+
export declare function autoCompletePhase(ctx: ExtensionContext, validation: ValidationResult): Promise<AutoCompleteResult>;
|
|
44
|
+
export type AutoOutcome = "complete" | "stopped" | "aborted";
|
|
45
|
+
export interface AutoRunOptions {
|
|
46
|
+
strict: boolean;
|
|
47
|
+
llmSteerOverride?: boolean;
|
|
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;
|
|
57
|
+
}
|
|
58
|
+
export interface AutoRunResult {
|
|
59
|
+
outcome: AutoOutcome;
|
|
60
|
+
reason: string;
|
|
61
|
+
phasesRun: string[];
|
|
62
|
+
totalPhases: number;
|
|
63
|
+
startedAt: number;
|
|
64
|
+
endedAt: number;
|
|
65
|
+
totalTokens: {
|
|
66
|
+
input: number;
|
|
67
|
+
output: number;
|
|
68
|
+
cacheWrite: number;
|
|
69
|
+
};
|
|
70
|
+
stoppedAt?: {
|
|
71
|
+
phaseId: string;
|
|
72
|
+
validation?: ValidationOverall;
|
|
73
|
+
error?: string;
|
|
74
|
+
};
|
|
75
|
+
validationSummary?: string[];
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Per-iteration decision: given the outcome of a sub-agent run and (if it
|
|
79
|
+
* completed) its validation result, what should the auto loop do next?
|
|
80
|
+
* Pure function — no I/O, no module state — so the decision matrix is
|
|
81
|
+
* unit-testable without mocking the SDK.
|
|
82
|
+
*/
|
|
83
|
+
export type AutoDecision = {
|
|
84
|
+
action: "continue";
|
|
85
|
+
} | {
|
|
86
|
+
action: "stop";
|
|
87
|
+
reason: string;
|
|
88
|
+
validation?: ValidationOverall;
|
|
89
|
+
error?: string;
|
|
90
|
+
validationSummary?: string[];
|
|
91
|
+
} | {
|
|
92
|
+
action: "aborted";
|
|
93
|
+
};
|
|
94
|
+
export declare function decideAfterPhase(phaseStatus: SinglePhaseResult["status"], phaseError: string | undefined, validation: ValidationResult | null, strict: boolean): AutoDecision;
|
|
95
|
+
export declare function runAuto(ctx: ExtensionContext, pi: ExtensionAPI, initialState: WorkspaceState, options: AutoRunOptions): Promise<AutoRunResult>;
|
|
96
|
+
export declare function buildAutoSummary(result: AutoRunResult, availableSkills?: string[]): string;
|