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.
@@ -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. Lives at
2
- // `.codecarto/workflow/config.yaml`. Missing file or missing keys fall back
3
- // to defaults, so existing workspaces created before this file existed
4
- // keep working unchanged. Schema is intentionally narrow — one surface
5
- // per feature, easy to grow.
6
- import { join } from "node:path";
7
- import { pathExists } from "./utils.js";
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 configPath = join(workspaceDir, CONFIG_RELATIVE_PATH);
17
- if (!(await pathExists(configPath)))
18
- return cloneDefault();
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
- const raw = await loadYamlFile(configPath);
21
- return mergeConfig(raw);
60
+ return await loadYamlFile(path);
22
61
  }
23
62
  catch {
24
- // Malformed YAML: fall back to defaults rather than failing the
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
- const merged = cloneDefault();
78
+ return applyRaw(cloneDefault(), raw);
79
+ }
80
+ function applyRaw(base, raw) {
31
81
  if (!raw || typeof raw !== "object")
32
- return merged;
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
- merged.orchestrator.llm_steer_next_phase = o.llm_steer_next_phase;
90
+ out.orchestrator.llm_steer_next_phase = o.llm_steer_next_phase;
37
91
  }
38
92
  }
39
- return merged;
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
  }
@@ -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 getPhaseMap(state.pipeline).get(trimmed) ?? null;
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 getNextEligiblePhase(state);
56
+ return null;
45
57
  }
46
58
  export function resolvePipelineChoice(input) {
47
59
  const trimmed = input.trim();
@@ -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 declare function buildPhasePrompt(state: WorkspaceState, phase: PipelinePhase, forced: boolean): Promise<string>;
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>;
@@ -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
- lines.push("Strategic Alignment Hook (run BEFORE producing the spec):");
70
- lines.push("- Confirm with the user whether this spec should be language-agnostic or opinionated:");
71
- lines.push(" - language-agnostic use templates/reimplementation-spec.md (default).");
72
- lines.push(" - opinionated (target stack locked) use templates/reimplementation-spec-opinionated.md.");
73
- lines.push("- Record the chosen variant in the spec front-matter and in your validation block.");
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/.");
@@ -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
- typeof r.status === "string");
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
  }
@@ -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;
@@ -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 { normalize, resolve } from "node:path";
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
+ }
@@ -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;
@@ -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;