@gtrabanco/pi-agentic-workflow 0.14.0 → 0.15.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.
@@ -9,7 +9,7 @@ import { OPERATIONS, PATH_GLOB_MAX_LENGTH, PHASE_STATES, REQUIREMENTS } from "./
9
9
  * strong model ran when it did not. Only a *missing* file resolves to the
10
10
  * default, and that decision belongs to the loader.
11
11
  */
12
- const ROOT_KEYS = new Set(["default", "commands", "onUnavailableRoute", "onSettle", "pathProtection"]);
12
+ const ROOT_KEYS = new Set(["default", "commands", "onUnavailableRoute", "onSettle", "pathProtection", "advance"]);
13
13
  const ROUTE_KEYS = new Set(["model", "thinking"]);
14
14
  const COMMAND_NAME = /^[a-z0-9][a-z0-9._-]*$/u;
15
15
  function isRecord(value) {
@@ -41,6 +41,44 @@ function isUnavailableRoutePolicy(value) {
41
41
  function isSettlePolicy(value) {
42
42
  return SETTLE_POLICIES.includes(value);
43
43
  }
44
+ /** Strict validator for the optional `advance` conductor config (feature 62). */
45
+ const ADVANCE_KEYS = new Set(["iterationsCap", "sensitivePaths", "securityPaths", "runLogPath"]);
46
+ function checkAdvance(value, path, issues) {
47
+ if (!isRecord(value)) {
48
+ issues.push({ path, message: "must be an object" });
49
+ return undefined;
50
+ }
51
+ const out = {};
52
+ for (const key of Object.keys(value)) {
53
+ if (!ADVANCE_KEYS.has(key)) {
54
+ issues.push({ path: `${path}.${displayKey(key)}`, message: `unknown advance key "${key}" (allowed: iterationsCap, sensitivePaths, securityPaths, runLogPath)` });
55
+ continue;
56
+ }
57
+ const v = value[key];
58
+ if (key === "iterationsCap") {
59
+ if (typeof v !== "number" || !Number.isInteger(v) || v < 1) {
60
+ issues.push({ path: `${path}.iterationsCap`, message: "must be a positive integer" });
61
+ continue;
62
+ }
63
+ out.iterationsCap = v;
64
+ }
65
+ else if (key === "runLogPath") {
66
+ if (typeof v !== "string" || v.length === 0) {
67
+ issues.push({ path: `${path}.runLogPath`, message: "must be a non-empty string" });
68
+ continue;
69
+ }
70
+ out.runLogPath = v;
71
+ }
72
+ else {
73
+ if (!Array.isArray(v) || v.some((e) => typeof e !== "string")) {
74
+ issues.push({ path: `${path}.${key}`, message: "must be an array of strings" });
75
+ continue;
76
+ }
77
+ out[key] = v;
78
+ }
79
+ }
80
+ return out;
81
+ }
44
82
  /** Strict validator for the optional `pathProtection` override (feature 60, AC10). */
45
83
  function checkPathProtection(value, path, issues) {
46
84
  if (!isRecord(value)) {
@@ -176,7 +214,7 @@ function validateConfig(value) {
176
214
  if (!ROOT_KEYS.has(key)) {
177
215
  issues.push({
178
216
  path: `$.${displayKey(key)}`,
179
- message: `unknown config key "${key}" (allowed: default, commands, onUnavailableRoute, onSettle, pathProtection)`,
217
+ message: `unknown config key "${key}" (allowed: default, commands, onUnavailableRoute, onSettle, pathProtection, advance)`,
180
218
  });
181
219
  }
182
220
  }
@@ -225,6 +263,11 @@ function validateConfig(value) {
225
263
  config.onSettle = value.onSettle;
226
264
  }
227
265
  }
266
+ if (value.advance !== undefined) {
267
+ const advance = checkAdvance(value.advance, "$.advance", issues);
268
+ if (advance)
269
+ config.advance = advance;
270
+ }
228
271
  if (value.pathProtection !== undefined) {
229
272
  const override = checkPathProtection(value.pathProtection, "$.pathProtection", issues);
230
273
  if (override)
@@ -42,6 +42,8 @@ export interface ConfigFile {
42
42
  onSettle?: SettlePolicy;
43
43
  /** The tighten-only path-protection override (feature 60, AC10). */
44
44
  pathProtection?: PathProtectionOverride;
45
+ /** Conductor (advance) knobs (feature 62). */
46
+ advance?: AdvanceConfigFile;
45
47
  }
46
48
  export type UnavailableRoutePolicy = "stop" | "inherit";
47
49
  /**
@@ -65,6 +67,10 @@ export interface EffectiveConfig {
65
67
  onSettle: SettlePolicy;
66
68
  /** The effective path-protection policy: the shipped floor tightened by any override. */
67
69
  pathProtection: ResolvedPathPolicy;
70
+ /** The effective advance (conductor) knobs (feature 62). Resolved by
71
+ * mergeConfigs; the shipped DEFAULT_CONFIG predates it and leaves it
72
+ * absent — consumers fall back to DEFAULT_ADVANCE_CONFIG. */
73
+ advance?: AdvanceConfig;
68
74
  }
69
75
  export interface ConfigProblem {
70
76
  scope: "global" | "project";
@@ -76,3 +82,19 @@ export interface ConfigPaths {
76
82
  global: string;
77
83
  project: string;
78
84
  }
85
+ /** Per-iteration control knobs for the conductor loop. */
86
+ export interface AdvanceConfigFile {
87
+ iterationsCap?: number;
88
+ sensitivePaths?: string[];
89
+ securityPaths?: string[];
90
+ runLogPath?: string;
91
+ }
92
+ /** Effective advance configuration with all fields resolved. */
93
+ export interface AdvanceConfig {
94
+ iterationsCap: number;
95
+ sensitivePaths: string[];
96
+ securityPaths: string[];
97
+ runLogPath: string;
98
+ }
99
+ /** The shipped advance defaults (mirrors conductor DEFAULT_CONDUCTOR_CONFIG). */
100
+ export declare const DEFAULT_ADVANCE_CONFIG: Readonly<AdvanceConfig>;
@@ -13,3 +13,10 @@ export const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhig
13
13
  export const MAX_MODEL_CHAIN = 4;
14
14
  export const UNAVAILABLE_ROUTE_POLICIES = ["stop", "inherit"];
15
15
  export const SETTLE_POLICIES = ["keep", "restore"];
16
+ /** The shipped advance defaults (mirrors conductor DEFAULT_CONDUCTOR_CONFIG). */
17
+ export const DEFAULT_ADVANCE_CONFIG = {
18
+ iterationsCap: 12,
19
+ sensitivePaths: [],
20
+ securityPaths: [],
21
+ runLogPath: ".agentic-workflow/advance-run.log",
22
+ };
@@ -0,0 +1,23 @@
1
+ /**
2
+ * The native `advance` command wiring (feature 62).
3
+ *
4
+ * Bridges the pi-free conductor loop to a live session: real sensor spawn,
5
+ * real git probes, real run-log writes, real WIP-commit parking. One stage
6
+ * per invocation by default — the invoked skill must settle before the next
7
+ * sensor run can observe its effect — with `--continue` running the bounded
8
+ * full loop. `--fullauto`/`--unattended` switch the unattended adversarial
9
+ * floor on. The conductor never merges (feature 20 merge authority).
10
+ */
11
+ import type { LoadedConfig } from "../config/load.js";
12
+ import type { CommandRegistrar } from "./factory.js";
13
+ import type { InvocationContext, ModelRef } from "../routing/types.js";
14
+ /** Register the native `advance` command on the shared registrar. */
15
+ export declare function registerAdvanceCommand<M extends ModelRef = ModelRef>(registrar: CommandRegistrar<M>, deps: {
16
+ surface: (ctx: InvocationContext<M>) => {
17
+ sendUserMessage(content: string, options?: {
18
+ expandPromptTemplates?: boolean;
19
+ }): void;
20
+ };
21
+ /** The same bound config reader the router uses (factory's `read`). */
22
+ readConfig: (ctx: InvocationContext<M>) => LoadedConfig;
23
+ }, commandNames: Set<string>): void;
@@ -0,0 +1,93 @@
1
+ /**
2
+ * The native `advance` command wiring (feature 62).
3
+ *
4
+ * Bridges the pi-free conductor loop to a live session: real sensor spawn,
5
+ * real git probes, real run-log writes, real WIP-commit parking. One stage
6
+ * per invocation by default — the invoked skill must settle before the next
7
+ * sensor run can observe its effect — with `--continue` running the bounded
8
+ * full loop. `--fullauto`/`--unattended` switch the unattended adversarial
9
+ * floor on. The conductor never merges (feature 20 merge authority).
10
+ */
11
+ import { execFile } from "node:child_process";
12
+ import { appendFileSync, mkdirSync } from "node:fs";
13
+ import { dirname, resolve } from "node:path";
14
+ import { DEFAULT_ADVANCE_CONFIG } from "../config/types.js";
15
+ import { runtimeBin } from "../runtime.js";
16
+ import { runConductorLoop } from "../conductor/loop.js";
17
+ import { decideFromEnvelope } from "../conductor/decide.js";
18
+ import { invocationFromDecision } from "../conductor/invoke.js";
19
+ import { judgeUrgency } from "../conductor/urgency.js";
20
+ import { ADVANCE_COMMAND } from "../routing/types.js";
21
+ function git(args, cwd) {
22
+ return new Promise((res) => {
23
+ execFile("git", args, { cwd, timeout: 5_000 }, (err, stdout) => {
24
+ res({ ok: !err, stdout: typeof stdout === "string" ? stdout : "" });
25
+ });
26
+ });
27
+ }
28
+ async function gitProbe(cwd) {
29
+ const status = await git(["status", "--porcelain"], cwd);
30
+ const ahead = await git(["rev-list", "--count", "@{upstream}..HEAD"], cwd);
31
+ // No upstream (or probe failure) reads as unpushed work: fail closed to partial.
32
+ return { porcelain: status.stdout, ahead: ahead.ok ? Number.parseInt(ahead.stdout.trim(), 10) || 0 : 1 };
33
+ }
34
+ function appendRunLog(config, cwd, line) {
35
+ const path = resolve(cwd, config.runLogPath ?? ".agentic-workflow/advance-run.log");
36
+ try {
37
+ mkdirSync(dirname(path), { recursive: true });
38
+ appendFileSync(path, `${line}\n`, "utf8");
39
+ }
40
+ catch {
41
+ // A run log that cannot be written never blocks the stage itself.
42
+ }
43
+ }
44
+ async function parkInFlight(cwd) {
45
+ await git(["add", "-A"], cwd);
46
+ await git(["commit", "-m", "wip: parked by advance (urgent interrupt)"], cwd);
47
+ }
48
+ /** Register the native `advance` command on the shared registrar. */
49
+ export function registerAdvanceCommand(registrar, deps, commandNames) {
50
+ registrar.registerCommand(ADVANCE_COMMAND, {
51
+ description: "Deterministic conductor: sensor → decide → invoke the next stage; stops on needs_input/stop. Never merges.",
52
+ handler: async (_args, ctx) => {
53
+ const flags = _args.split(/\s+/).filter(Boolean);
54
+ const attended = !flags.includes("--fullauto") && !flags.includes("--unattended");
55
+ const continueLoop = flags.includes("--continue");
56
+ // The effective advance knobs: the merged config when it carries the
57
+ // key, the shipped defaults otherwise.
58
+ let config = { ...DEFAULT_ADVANCE_CONFIG };
59
+ try {
60
+ const loaded = deps.readConfig(ctx);
61
+ if (loaded.ok && loaded.config.advance)
62
+ config = loaded.config.advance;
63
+ }
64
+ catch {
65
+ // Config trouble falls back to the shipped advance defaults; the loop
66
+ // itself is fail-closed on the sensor side anyway.
67
+ }
68
+ const surface = deps.surface(ctx);
69
+ const result = await runConductorLoop({
70
+ cwd: ctx.cwd,
71
+ runtimeBin: runtimeBin(),
72
+ commandNames,
73
+ maxInvokes: continueLoop ? undefined : 1,
74
+ attended,
75
+ config,
76
+ runSensor: (cwd, bin) => import("../conductor/sensor.js").then(({ runSensor }) => runSensor(cwd, bin)),
77
+ decide: decideFromEnvelope,
78
+ invocation: invocationFromDecision,
79
+ judgeUrgency,
80
+ sendUserMessage: (invocation) => {
81
+ surface.sendUserMessage(invocation, { expandPromptTemplates: true });
82
+ return Promise.resolve({ ok: true });
83
+ },
84
+ gitProbe: () => gitProbe(ctx.cwd),
85
+ appendRunLog: (line) => appendRunLog(config, ctx.cwd, line),
86
+ parkInFlight: () => {
87
+ return parkInFlight(ctx.cwd);
88
+ },
89
+ });
90
+ ctx.notify(`${result.banner} — ${result.detail ?? ""} (iterations: ${result.iterations})`, "info");
91
+ },
92
+ });
93
+ }
@@ -2,7 +2,8 @@ import { loadConfig } from "../config/load.js";
2
2
  import { readCatalogue } from "../routing/catalogue.js";
3
3
  import { createRouter } from "../routing/dispatch.js";
4
4
  import { dirtyWorktreeWarning, receiptGuard } from "./receipt-guard.js";
5
- import { SETTINGS_COMMAND, SETTINGS_COMMAND_ALIAS } from "../routing/types.js";
5
+ import { ADVANCE_COMMAND, SETTINGS_COMMAND, SETTINGS_COMMAND_ALIAS } from "../routing/types.js";
6
+ import { registerAdvanceCommand } from "./conductor-command.js";
6
7
  export function createExtension(deps) {
7
8
  const { registrar, surface, skillsDir, agentDir, hint, settings } = deps;
8
9
  const catalogue = readCatalogue(skillsDir);
@@ -21,6 +22,7 @@ export function createExtension(deps) {
21
22
  };
22
23
  const knownCommands = new Set([
23
24
  ...catalogue.commands.map((entry) => entry.name),
25
+ ADVANCE_COMMAND,
24
26
  SETTINGS_COMMAND,
25
27
  SETTINGS_COMMAND_ALIAS,
26
28
  ]);
@@ -49,6 +51,9 @@ export function createExtension(deps) {
49
51
  ctx.notify(`Settings could not be opened: ${error.message}`, "error");
50
52
  }
51
53
  };
54
+ // The native conductor command (feature 62): registered in code, not derived
55
+ // from a skill dir, and able to invoke every catalogue command by name.
56
+ registerAdvanceCommand(registrar, { surface, readConfig: read }, knownCommands);
52
57
  registrar.registerCommand(SETTINGS_COMMAND, {
53
58
  description: "Show and configure per-command model routing",
54
59
  handler: settingsHandler,
@@ -90,6 +90,8 @@ export type DispatchOutcome = {
90
90
  message: string;
91
91
  };
92
92
  /** Slash name of the settings console (SPEC S4, AC3, AC10). */
93
+ /** The native conductor command (feature 62): sensor → decide → invoke loop. */
94
+ export declare const ADVANCE_COMMAND = "advance";
93
95
  export declare const SETTINGS_COMMAND = "agentic-workflow-settings";
94
96
  /** Shorthand alias for the same console — a pointer to the same handler, never a separate route (OB-5, AC6). */
95
97
  export declare const SETTINGS_COMMAND_ALIAS = "aw-settings";
@@ -1,5 +1,7 @@
1
1
  export const modelRefKey = (model) => `${model.provider}/${model.id}`;
2
2
  /** Slash name of the settings console (SPEC S4, AC3, AC10). */
3
+ /** The native conductor command (feature 62): sensor → decide → invoke loop. */
4
+ export const ADVANCE_COMMAND = "advance";
3
5
  export const SETTINGS_COMMAND = "agentic-workflow-settings";
4
6
  /** Shorthand alias for the same console — a pointer to the same handler, never a separate route (OB-5, AC6). */
5
7
  export const SETTINGS_COMMAND_ALIAS = "aw-settings";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gtrabanco/pi-agentic-workflow",
3
- "version": "0.14.0",
3
+ "version": "0.15.0",
4
4
  "description": "Pi package: canonical agentic-workflow skills, friendly slash commands, and per-command model routing.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -57,7 +57,8 @@
57
57
  },
58
58
  "dependencies": {
59
59
  "@earendil-works/pi-tui": "0.87.0",
60
- "@gtrabanco/agentic-workflow": "0.1.1"
60
+ "@gtrabanco/agentic-workflow": "file:../agentic-workflow",
61
+ "@gtrabanco/agentic-workflow-schema": "4.5.0"
61
62
  },
62
63
  "devDependencies": {
63
64
  "@types/node": "24.13.3",