@gtrabanco/pi-agentic-workflow 0.15.2 → 0.16.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/README.md +49 -0
- package/dist/conductor/loop.js +23 -1
- package/dist/conductor/types.d.ts +6 -0
- package/dist/config/defaults.js +11 -0
- package/dist/config/merge.js +93 -1
- package/dist/config/profiles.d.ts +36 -0
- package/dist/config/profiles.js +102 -0
- package/dist/config/recommended.d.ts +14 -0
- package/dist/config/recommended.js +28 -0
- package/dist/config/schema.js +159 -3
- package/dist/config/types.d.ts +52 -0
- package/dist/config/types.js +10 -0
- package/dist/extension/conductor-command.d.ts +2 -5
- package/dist/extension/conductor-command.js +18 -6
- package/dist/extension/factory.d.ts +2 -0
- package/dist/extension/factory.js +10 -3
- package/dist/routing/dispatch.d.ts +9 -2
- package/dist/routing/dispatch.js +115 -46
- package/dist/routing/state.d.ts +26 -0
- package/dist/routing/state.js +67 -1
- package/dist/routing/types.d.ts +7 -0
- package/dist/settings/console.d.ts +7 -0
- package/dist/settings/console.js +214 -35
- package/dist/settings/view.d.ts +3 -1
- package/dist/settings/view.js +8 -1
- package/package.json +1 -1
- package/skills/audit-docs/SKILL.md +3 -3
- package/skills/audit-pr/SKILL.md +8 -8
- package/skills/audit-pr/references/01_MERGE_GATES.md +3 -3
- package/skills/audit-pr/references/02_CLOSURE_AND_SCOPE_GATES.md +4 -5
- package/skills/audit-pr/references/03_AUDIT_PROCESS.md +1 -1
- package/skills/audit-pr/references/04_VERDICT.md +3 -3
- package/skills/audit-pr/references/05_ROUTING_AND_GUARDRAILS.md +2 -2
- package/skills/discover-repository-state/SKILL.md +3 -4
- package/skills/execute-phase/SKILL.md +1 -1
- package/skills/execute-phase/references/DESCOPE.md +1 -1
- package/skills/fold-findings/SKILL.md +8 -8
- package/skills/fold-findings/references/FOLD_PROCESS.md +6 -5
- package/skills/orchestration-envelope/SKILL.md +4 -3
- package/skills/phase-contract/SKILL.md +3 -4
- package/skills/pre-execution-review/SKILL.md +10 -7
- package/skills/pre-execution-review/references/POLICY.md +17 -8
- package/skills/product-audit/SKILL.md +12 -13
- package/skills/product-audit/references/AUDIT_PROCESS.md +1 -1
- package/skills/resolve-repository-state/SKILL.md +2 -3
- package/skills/review-change/SKILL.md +3 -3
- package/skills/review-change/references/ADVERSARIAL_RECOMMENDATION.md +1 -1
- package/skills/review-change/references/ADVERSARIAL_SETUP.md +1 -1
- package/skills/review-change/references/OUTPUT_AND_GUARDRAILS.md +5 -5
- package/skills/review-change/references/PERSIST_AND_DECIDE.md +7 -6
- package/skills/review-implementation/SKILL.md +1 -1
- package/skills/review-implementation/references/CLASSIFY.md +13 -11
- package/skills/triage-issue/SKILL.md +11 -10
- package/skills/triage-issue/references/ISSUE_PROCESS.md +11 -10
- package/skills/triage-issue/references/LABELS.md +3 -3
- package/skills/triage-issue/references/REVIEW_FINDING_PROCESS.md +6 -6
- package/skills/unit-lane/SKILL.md +24 -9
- package/skills/workflow-status/SKILL.md +12 -13
- package/skills/workflow-status/references/CRASH_RECOVERY.md +6 -6
- package/skills/workflow-status/references/ENVELOPE_CORE.md +1 -4
- package/skills/workflow-status/references/ENVELOPE_FIELDS.md +7 -7
- package/skills/workflow-status/references/PRE_EXECUTION.md +5 -0
- package/skills/workflow-status/references/SENSOR_SIGNALS.md +1 -1
package/README.md
CHANGED
|
@@ -97,6 +97,55 @@ The first workflow command you run after install says once that routing is
|
|
|
97
97
|
configurable, then never again. That acknowledgement is stored in
|
|
98
98
|
`~/.pi/agent/pi-agentic-workflow-state.json`, not in your config.
|
|
99
99
|
|
|
100
|
+
## Model profiles
|
|
101
|
+
|
|
102
|
+
The routing config above is the **`default` profile**. A profile is one coherent
|
|
103
|
+
set of routes, and you can define as many as you like under `profiles`:
|
|
104
|
+
|
|
105
|
+
```json
|
|
106
|
+
{
|
|
107
|
+
"recommendedModels": true,
|
|
108
|
+
"profileOrder": ["work", "cheap"],
|
|
109
|
+
"profileFallback": { "applyTo": "flow", "resume": "continue", "retryAfterSeconds": 86400 },
|
|
110
|
+
"profiles": {
|
|
111
|
+
"work": { "default": { "model": "anthropic/claude-opus-4-5", "thinking": "high" } },
|
|
112
|
+
"cheap": { "default": { "model": "openai/gpt-5.2", "thinking": "medium" } }
|
|
113
|
+
},
|
|
114
|
+
"default": { "model": "inherit", "thinking": "inherit" },
|
|
115
|
+
"commands": { "review-change": { "thinking": "max" } }
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
- **`profileOrder`** — the preference chain, first = most preferred/active. For each
|
|
120
|
+
command the router probes the profiles in order and the **first whose model is
|
|
121
|
+
usable** serves it; when the preferred profile's model is unavailable it falls
|
|
122
|
+
through to the next. A profile participates only for keys it actually declares.
|
|
123
|
+
- **`profiles`** — unlimited named profiles. The implicit **`default`** profile is
|
|
124
|
+
the top-level `default` + `commands` above, so a config with just those two keys
|
|
125
|
+
resolves exactly as before.
|
|
126
|
+
- **Built-in `nan` profile.** With `recommendedModels: true` (the default), when the
|
|
127
|
+
session has models from the `nan` provider, a code-only `nan` profile is appended
|
|
128
|
+
to the end of the chain (or used alone when you set no `profileOrder`). It carries
|
|
129
|
+
the per-command ladders from the README's NaN section. Any route you declare —
|
|
130
|
+
including an explicit `"inherit"` — wins over it. Set `recommendedModels: false`
|
|
131
|
+
to switch it off; the settings console writes the key explicitly on save when the
|
|
132
|
+
provider is available.
|
|
133
|
+
- **`profileFallback`** — what happens when the preferred profile's model is
|
|
134
|
+
unusable:
|
|
135
|
+
- `applyTo`: `"flow"` (default) records the switch so later commands keep using the
|
|
136
|
+
fallback profile; `"command"` falls back for this command only and the next
|
|
137
|
+
command retries the preferred profile.
|
|
138
|
+
- `resume`: `"continue"` (default) lets the in-flight stage run on the fallback
|
|
139
|
+
profile; `"restart"` makes the `advance` conductor defer the switch and re-run
|
|
140
|
+
the stage under the fallback profile instead.
|
|
141
|
+
- `retryAfterSeconds`: how long a `flow` demotion lasts before the preferred
|
|
142
|
+
profile is probed again (default `86400`, one day).
|
|
143
|
+
- **Rotation.** `/agentic-workflow-settings` is scope → profile → routes: pick the
|
|
144
|
+
scope, pick the profile to edit, then edit its routes. `Rotate the active profile`
|
|
145
|
+
moves a profile to the front of `profileOrder` and clears any recorded demotion.
|
|
146
|
+
The built-in `nan` profile is shown but not editable — create a profile to
|
|
147
|
+
override it.
|
|
148
|
+
|
|
100
149
|
## Path protection (optional)
|
|
101
150
|
|
|
102
151
|
The Tier 2 preventive guard blocks a `write` / `edit` tool call to an existing
|
package/dist/conductor/loop.js
CHANGED
|
@@ -62,6 +62,7 @@ export async function runConductorLoop(deps) {
|
|
|
62
62
|
const bin = deps.runtimeBin ?? "bun";
|
|
63
63
|
let consecutivePartials = 0;
|
|
64
64
|
let consecutiveSenses = 0;
|
|
65
|
+
let consecutiveDeferred = 0;
|
|
65
66
|
let invoked = 0;
|
|
66
67
|
for (let iteration = 1; iteration <= cap; iteration++) {
|
|
67
68
|
// ── 1. Sensor ───────────────────────────────────────────────────────
|
|
@@ -186,7 +187,28 @@ export async function runConductorLoop(deps) {
|
|
|
186
187
|
iterations: iteration - 1,
|
|
187
188
|
};
|
|
188
189
|
}
|
|
189
|
-
await deps.sendUserMessage(invocation);
|
|
190
|
+
const sendResult = await deps.sendUserMessage(invocation);
|
|
191
|
+
if (sendResult.ok === false) {
|
|
192
|
+
deps.appendRunLog(formatLogLine("stop", "stop-dispatch-refused", invocation, iteration, cap));
|
|
193
|
+
return {
|
|
194
|
+
banner: "ADVANCE: STOPPED",
|
|
195
|
+
stopCode: "stop-dispatch-refused",
|
|
196
|
+
detail: "the stage dispatch was refused (the router already explained why)",
|
|
197
|
+
iterations: iteration - 1,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
if (sendResult.deferred === true) {
|
|
201
|
+
// AC10 restart: the routing recorded the demotion instead of sending; re-run
|
|
202
|
+
// the iteration so the stage is decided and served under the fallback profile.
|
|
203
|
+
deps.appendRunLog(formatLogLine("switch", `${sendResult.profileSwitched?.from ?? "?"}->${sendResult.profileSwitched?.to ?? "?"}`, invocation, iteration, cap));
|
|
204
|
+
consecutiveDeferred++;
|
|
205
|
+
if (consecutiveDeferred >= 2) {
|
|
206
|
+
deps.appendRunLog(formatLogLine("stop", "stop-profile-switch-loop", "deferred-twice", iteration, cap));
|
|
207
|
+
return { banner: "ADVANCE: STOPPED", stopCode: "stop-profile-switch-loop", detail: "profile switch deferred twice without progress", iterations: iteration - 1 };
|
|
208
|
+
}
|
|
209
|
+
continue; // do NOT increment `invoked`
|
|
210
|
+
}
|
|
211
|
+
consecutiveDeferred = 0;
|
|
190
212
|
deps.appendRunLog(formatLogLine("invoke", decision.reasonCode, invocation, iteration, cap));
|
|
191
213
|
// One-stage-per-invocation wiring: the invoked skill must settle before
|
|
192
214
|
// the next sensor run can see its effect, so the default wiring stops
|
|
@@ -71,7 +71,13 @@ export interface LoopDeps {
|
|
|
71
71
|
invocation: (decision: ActionDecision, opts: InvocationOpts) => InvocationResult;
|
|
72
72
|
sendUserMessage: (invocation: string) => Promise<{
|
|
73
73
|
ok: boolean;
|
|
74
|
+
profileSwitched?: {
|
|
75
|
+
from: string;
|
|
76
|
+
to: string;
|
|
77
|
+
};
|
|
78
|
+
deferred?: boolean;
|
|
74
79
|
}>;
|
|
80
|
+
profileResume?: "continue" | "restart";
|
|
75
81
|
gitProbe: () => Promise<{
|
|
76
82
|
porcelain: string;
|
|
77
83
|
ahead: number;
|
package/dist/config/defaults.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { SHIPPED_PATH_POLICY, intersectPathPolicy } from "./path-policy.js";
|
|
2
|
+
import { DEFAULT_PROFILE_FALLBACK } from "./types.js";
|
|
2
3
|
/**
|
|
3
4
|
* The in-package default (SPEC S6, D-P6): a fresh install with no config file
|
|
4
5
|
* anywhere routes every command on the session model, and the fail-closed
|
|
@@ -10,5 +11,15 @@ export const DEFAULT_CONFIG = {
|
|
|
10
11
|
commands: {},
|
|
11
12
|
onUnavailableRoute: "stop",
|
|
12
13
|
onSettle: "keep",
|
|
14
|
+
// Feature 60 added the resolved path-protection policy to the effective
|
|
15
|
+
// config; the shipped floor with no override and no degradation.
|
|
13
16
|
pathProtection: intersectPathPolicy(SHIPPED_PATH_POLICY, {}),
|
|
17
|
+
// Feature 63 — model profiles: recommended built-in profiles, empty
|
|
18
|
+
// named profiles, empty order (default-only), and default demotion policy.
|
|
19
|
+
recommendedModels: true,
|
|
20
|
+
profiles: {},
|
|
21
|
+
profileOrder: [],
|
|
22
|
+
profileFallback: { ...DEFAULT_PROFILE_FALLBACK },
|
|
23
|
+
// Feature 63 — model profiles: declared keys (none, zero-config).
|
|
24
|
+
declared: { default: { model: false, thinking: false }, commands: {} },
|
|
14
25
|
};
|
package/dist/config/merge.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { DEFAULT_CONFIG, DEFAULT_ROUTE } from "./defaults.js";
|
|
2
2
|
import { SHIPPED_PATH_POLICY, intersectPathPolicy, mergePathProtectionOverrides } from "./path-policy.js";
|
|
3
|
-
import { DEFAULT_ADVANCE_CONFIG } from "./types.js";
|
|
3
|
+
import { DEFAULT_ADVANCE_CONFIG, DEFAULT_PROFILE_FALLBACK } from "./types.js";
|
|
4
4
|
/**
|
|
5
5
|
* Project-over-global merge (SPEC S5, S6).
|
|
6
6
|
*
|
|
@@ -19,6 +19,80 @@ function pick(fallback, ...values) {
|
|
|
19
19
|
return value;
|
|
20
20
|
return fallback;
|
|
21
21
|
}
|
|
22
|
+
/** Merge two RouteFiles key-by-key (used for profile routes). */
|
|
23
|
+
function mergeRouteFiles(globalRoute, projectRoute) {
|
|
24
|
+
if (globalRoute === undefined && projectRoute === undefined)
|
|
25
|
+
return undefined;
|
|
26
|
+
const out = {};
|
|
27
|
+
if (globalRoute?.model !== undefined || projectRoute?.model !== undefined) {
|
|
28
|
+
// A chain declared only at global scope survives.
|
|
29
|
+
// Per-key: project chain wins, or global chain, or inherited string.
|
|
30
|
+
if (projectRoute?.model !== undefined) {
|
|
31
|
+
out.model = projectRoute.model;
|
|
32
|
+
}
|
|
33
|
+
else if (globalRoute?.model !== undefined) {
|
|
34
|
+
out.model = globalRoute.model;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (globalRoute?.thinking !== undefined || projectRoute?.thinking !== undefined) {
|
|
38
|
+
out.thinking = pick(DEFAULT_ROUTE.thinking, projectRoute?.thinking, globalRoute?.thinking);
|
|
39
|
+
}
|
|
40
|
+
return Object.keys(out).length > 0 ? out : undefined;
|
|
41
|
+
}
|
|
42
|
+
/** Per-profile merge: each profile's default and commands merge key-by-key.
|
|
43
|
+
* A profile name may exist in one scope but not the other; the missing
|
|
44
|
+
* scope simply contributes nothing. */
|
|
45
|
+
function mergeProfiles(globalProfiles, projectProfiles) {
|
|
46
|
+
const allNames = new Set([
|
|
47
|
+
...Object.keys(globalProfiles ?? {}),
|
|
48
|
+
...Object.keys(projectProfiles ?? {}),
|
|
49
|
+
]);
|
|
50
|
+
const result = {};
|
|
51
|
+
for (const name of allNames) {
|
|
52
|
+
const gDef = globalProfiles?.[name];
|
|
53
|
+
const pDef = projectProfiles?.[name];
|
|
54
|
+
const mergedDef = {};
|
|
55
|
+
// Merge default routes key-by-key.
|
|
56
|
+
if (gDef?.default !== undefined || pDef?.default !== undefined) {
|
|
57
|
+
const mergedDefault = {};
|
|
58
|
+
if (pDef?.default !== undefined && pDef.default.model !== undefined) {
|
|
59
|
+
mergedDefault.model = pDef.default.model;
|
|
60
|
+
}
|
|
61
|
+
else if (gDef?.default !== undefined && gDef.default.model !== undefined) {
|
|
62
|
+
mergedDefault.model = gDef.default.model;
|
|
63
|
+
}
|
|
64
|
+
if (gDef?.default?.thinking !== undefined || pDef?.default?.thinking !== undefined) {
|
|
65
|
+
mergedDefault.thinking = pick(DEFAULT_ROUTE.thinking, pDef?.default?.thinking, gDef?.default?.thinking);
|
|
66
|
+
}
|
|
67
|
+
mergedDef.default = mergedDefault;
|
|
68
|
+
}
|
|
69
|
+
// Merge commands key-by-key.
|
|
70
|
+
const gCmds = gDef?.commands ?? {};
|
|
71
|
+
const pCmds = pDef?.commands ?? {};
|
|
72
|
+
const allCmdNames = new Set([...Object.keys(gCmds), ...Object.keys(pCmds)]);
|
|
73
|
+
if (allCmdNames.size > 0) {
|
|
74
|
+
mergedDef.commands = {};
|
|
75
|
+
for (const cmd of allCmdNames) {
|
|
76
|
+
const mergedCmd = mergeRouteFiles(gCmds[cmd], pCmds[cmd]);
|
|
77
|
+
if (mergedCmd)
|
|
78
|
+
mergedDef.commands[cmd] = mergedCmd;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (Object.keys(mergedDef).length > 0) {
|
|
82
|
+
result[name] = mergedDef;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return result;
|
|
86
|
+
}
|
|
87
|
+
/** Per-key merge for profileFallback: each field independently
|
|
88
|
+
* takes project value, then global value, then the shipped default. */
|
|
89
|
+
function mergeProfileFallback(globalFb, projectFb) {
|
|
90
|
+
return {
|
|
91
|
+
applyTo: pick(DEFAULT_PROFILE_FALLBACK.applyTo, projectFb?.applyTo, globalFb?.applyTo),
|
|
92
|
+
resume: pick(DEFAULT_PROFILE_FALLBACK.resume, projectFb?.resume, globalFb?.resume),
|
|
93
|
+
retryAfterSeconds: pick(DEFAULT_PROFILE_FALLBACK.retryAfterSeconds, projectFb?.retryAfterSeconds, globalFb?.retryAfterSeconds),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
22
96
|
function resolveRoute(defaults, globalRoute, projectRoute) {
|
|
23
97
|
return {
|
|
24
98
|
model: pick(DEFAULT_ROUTE.model, projectRoute?.model, globalRoute?.model, defaults.model),
|
|
@@ -33,12 +107,30 @@ export function mergeConfigs(globalFile = {}, projectFile = {}) {
|
|
|
33
107
|
for (const name of new Set([...Object.keys(globalCommands), ...Object.keys(projectCommands)])) {
|
|
34
108
|
commands[name] = resolveRoute(effectiveDefault, globalCommands[name], projectCommands[name]);
|
|
35
109
|
}
|
|
110
|
+
// Feature 63 — model profiles: computed declaredness from raw files.
|
|
111
|
+
const declaredDefault = {
|
|
112
|
+
model: globalFile.default?.model !== undefined || projectFile.default?.model !== undefined,
|
|
113
|
+
thinking: globalFile.default?.thinking !== undefined || projectFile.default?.thinking !== undefined,
|
|
114
|
+
};
|
|
115
|
+
const declaredCommands = {};
|
|
116
|
+
for (const name of new Set([...Object.keys(globalCommands), ...Object.keys(projectCommands)])) {
|
|
117
|
+
declaredCommands[name] = {
|
|
118
|
+
model: declaredDefault.model || globalCommands[name]?.model !== undefined || projectCommands[name]?.model !== undefined,
|
|
119
|
+
thinking: declaredDefault.thinking || globalCommands[name]?.thinking !== undefined || projectCommands[name]?.thinking !== undefined,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
36
122
|
const merged = {
|
|
37
123
|
default: effectiveDefault,
|
|
38
124
|
commands,
|
|
39
125
|
onUnavailableRoute: projectFile.onUnavailableRoute ?? globalFile.onUnavailableRoute ?? DEFAULT_CONFIG.onUnavailableRoute,
|
|
40
126
|
onSettle: projectFile.onSettle ?? globalFile.onSettle ?? DEFAULT_CONFIG.onSettle,
|
|
41
127
|
pathProtection: intersectPathPolicy(SHIPPED_PATH_POLICY, mergePathProtectionOverrides(globalFile.pathProtection, projectFile.pathProtection)),
|
|
128
|
+
// Feature 63 — model profiles.
|
|
129
|
+
recommendedModels: pick(true, projectFile.recommendedModels, globalFile.recommendedModels),
|
|
130
|
+
profiles: mergeProfiles(globalFile.profiles, projectFile.profiles),
|
|
131
|
+
profileOrder: pick([], projectFile.profileOrder, globalFile.profileOrder),
|
|
132
|
+
profileFallback: mergeProfileFallback(globalFile.profileFallback, projectFile.profileFallback),
|
|
133
|
+
declared: { default: declaredDefault, commands: declaredCommands },
|
|
42
134
|
};
|
|
43
135
|
// Advance knobs appear only when a scope declares them, so configs that
|
|
44
136
|
// predate feature 62 keep their exact resolved shape (no undefined key).
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { EffectiveConfig, ProfileCandidate } from "./types.js";
|
|
2
|
+
/** Options for recommended-profile resolution. */
|
|
3
|
+
export interface RecommendedOptions {
|
|
4
|
+
providerAvailable: (provider: string) => boolean;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Compute the ordered list of profile names to probe for a command.
|
|
8
|
+
*
|
|
9
|
+
* Rules:
|
|
10
|
+
* - Start from `config.profileOrder` if non-empty, else `["default"]`.
|
|
11
|
+
* - If `config.recommendedModels` is true and the `nan` provider is available,
|
|
12
|
+
* append `"nan"` (unless it is already present in the order).
|
|
13
|
+
* - Returns a fresh array so callers may mutate without side effects.
|
|
14
|
+
*/
|
|
15
|
+
export declare function effectiveProfileOrder(config: EffectiveConfig, opts: RecommendedOptions): string[];
|
|
16
|
+
/**
|
|
17
|
+
* Resolve a profile chain for one command.
|
|
18
|
+
*
|
|
19
|
+
* Iterates the ordered profile names from `effectiveProfileOrder`. For each name:
|
|
20
|
+
* - `nan` → uses `config.profiles.nan` if the user overrode it, else
|
|
21
|
+
* the built-in `RECOMMENDED_PROFILES.nan`.
|
|
22
|
+
* - `default` → the implicit profile made from `config.default` and
|
|
23
|
+
* `config.commands` (top-level, already-merged).
|
|
24
|
+
* - Any other name → `config.profiles[name]` (user-defined profile).
|
|
25
|
+
*
|
|
26
|
+
* Skip names with no profile definition.
|
|
27
|
+
* For each candidate, compute the route file from the command's explicit
|
|
28
|
+
* command route or the profile's default route. Skip if neither is defined.
|
|
29
|
+
*
|
|
30
|
+
* Built-in nan profile (`RECOMMENDED_PROFILES.nan`, never user-overridden)
|
|
31
|
+
* always reports `declared = { model: true, thinking: true }`.
|
|
32
|
+
*
|
|
33
|
+
* Returns candidates in order, each with its route and per-key declaration
|
|
34
|
+
* status.
|
|
35
|
+
*/
|
|
36
|
+
export declare function resolveProfileChain(config: EffectiveConfig, command: string, opts: RecommendedOptions): ProfileCandidate[];
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { RECOMMENDED_PROFILES } from "./recommended.js";
|
|
2
|
+
/**
|
|
3
|
+
* Compute the ordered list of profile names to probe for a command.
|
|
4
|
+
*
|
|
5
|
+
* Rules:
|
|
6
|
+
* - Start from `config.profileOrder` if non-empty, else `["default"]`.
|
|
7
|
+
* - If `config.recommendedModels` is true and the `nan` provider is available,
|
|
8
|
+
* append `"nan"` (unless it is already present in the order).
|
|
9
|
+
* - Returns a fresh array so callers may mutate without side effects.
|
|
10
|
+
*/
|
|
11
|
+
export function effectiveProfileOrder(config, opts) {
|
|
12
|
+
const base = config.profileOrder.length > 0 ? config.profileOrder : ["default"];
|
|
13
|
+
const result = [...base];
|
|
14
|
+
if (config.recommendedModels && opts.providerAvailable("nan")) {
|
|
15
|
+
if (!result.includes("nan")) {
|
|
16
|
+
result.push("nan");
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return result;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Resolve a profile chain for one command.
|
|
23
|
+
*
|
|
24
|
+
* Iterates the ordered profile names from `effectiveProfileOrder`. For each name:
|
|
25
|
+
* - `nan` → uses `config.profiles.nan` if the user overrode it, else
|
|
26
|
+
* the built-in `RECOMMENDED_PROFILES.nan`.
|
|
27
|
+
* - `default` → the implicit profile made from `config.default` and
|
|
28
|
+
* `config.commands` (top-level, already-merged).
|
|
29
|
+
* - Any other name → `config.profiles[name]` (user-defined profile).
|
|
30
|
+
*
|
|
31
|
+
* Skip names with no profile definition.
|
|
32
|
+
* For each candidate, compute the route file from the command's explicit
|
|
33
|
+
* command route or the profile's default route. Skip if neither is defined.
|
|
34
|
+
*
|
|
35
|
+
* Built-in nan profile (`RECOMMENDED_PROFILES.nan`, never user-overridden)
|
|
36
|
+
* always reports `declared = { model: true, thinking: true }`.
|
|
37
|
+
*
|
|
38
|
+
* Returns candidates in order, each with its route and per-key declaration
|
|
39
|
+
* status.
|
|
40
|
+
*/
|
|
41
|
+
export function resolveProfileChain(config, command, opts) {
|
|
42
|
+
const profileNames = effectiveProfileOrder(config, opts);
|
|
43
|
+
const candidates = [];
|
|
44
|
+
for (const name of profileNames) {
|
|
45
|
+
let def;
|
|
46
|
+
if (name === "nan") {
|
|
47
|
+
def = config.profiles.nan ?? RECOMMENDED_PROFILES.nan;
|
|
48
|
+
}
|
|
49
|
+
else if (name === "default") {
|
|
50
|
+
def = { default: config.default, commands: config.commands };
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
def = config.profiles[name];
|
|
54
|
+
}
|
|
55
|
+
if (!def) {
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
const routeFile = def.commands?.[command] ?? def.default;
|
|
59
|
+
if (routeFile === undefined) {
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
// Determine per-key declaredness from the raw file presence (not value).
|
|
63
|
+
// For the "default" profile the declared info comes from the merged
|
|
64
|
+
// EffectiveConfig.declared surface; for named profiles we check raw
|
|
65
|
+
// presence; the built-in nan always declares both.
|
|
66
|
+
let declared;
|
|
67
|
+
if (name === "nan" && config.profiles.nan === undefined) {
|
|
68
|
+
// Built-in nan always declares both keys.
|
|
69
|
+
declared = { model: true, thinking: true };
|
|
70
|
+
}
|
|
71
|
+
else if (name === "default") {
|
|
72
|
+
// Use the merged declared surface (carries raw-file presence).
|
|
73
|
+
declared = config.declared.commands[command] ?? config.declared.default;
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
// User-defined named profile (including a user-defined "nan"): check raw presence.
|
|
77
|
+
const routeFileDef = config.profiles[name];
|
|
78
|
+
const rFile = routeFileDef?.commands?.[command] ?? routeFileDef?.default;
|
|
79
|
+
declared = {
|
|
80
|
+
model: rFile?.model !== undefined,
|
|
81
|
+
thinking: rFile?.thinking !== undefined,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
// Skip candidates that declare nothing (covers both: truly empty
|
|
85
|
+
// routeFile, and the shipped inherit/inherit default).
|
|
86
|
+
if (!declared.model && !declared.thinking) {
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
const route = {
|
|
90
|
+
model: routeFile.model ?? "inherit",
|
|
91
|
+
thinking: routeFile.thinking ?? "inherit",
|
|
92
|
+
};
|
|
93
|
+
const builtIn = name === "nan" && config.profiles.nan === undefined;
|
|
94
|
+
candidates.push({
|
|
95
|
+
profile: name,
|
|
96
|
+
builtIn,
|
|
97
|
+
route,
|
|
98
|
+
declared,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
return candidates;
|
|
102
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { RouteFile } from "./types.js";
|
|
2
|
+
/** Provider identifiers shipped as built-in profiles. */
|
|
3
|
+
export declare const RECOMMENDED_PROVIDERS: readonly string[];
|
|
4
|
+
/** A built-in profile shape: optional default route + per-command overrides. */
|
|
5
|
+
export interface RecommendedProfile {
|
|
6
|
+
default?: RouteFile;
|
|
7
|
+
commands: Record<string, RouteFile>;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Built-in model profiles keyed by provider identifier.
|
|
11
|
+
* Only these four models are ever used: nan/glm5.3, nan/glm5.3-flash,
|
|
12
|
+
* nan/deepseek-v4-flash, nan/mimo-v2.5.
|
|
13
|
+
*/
|
|
14
|
+
export declare const RECOMMENDED_PROFILES: Readonly<Record<string, RecommendedProfile>>;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** Provider identifiers shipped as built-in profiles. */
|
|
2
|
+
export const RECOMMENDED_PROVIDERS = ["nan"];
|
|
3
|
+
/**
|
|
4
|
+
* Built-in model profiles keyed by provider identifier.
|
|
5
|
+
* Only these four models are ever used: nan/glm5.3, nan/glm5.3-flash,
|
|
6
|
+
* nan/deepseek-v4-flash, nan/mimo-v2.5.
|
|
7
|
+
*/
|
|
8
|
+
export const RECOMMENDED_PROFILES = {
|
|
9
|
+
nan: {
|
|
10
|
+
default: { model: ["nan/deepseek-v4-flash", "nan/glm5.3-flash"], thinking: "inherit" },
|
|
11
|
+
commands: {
|
|
12
|
+
"audit-pr": { model: ["nan/mimo-v2.5", "nan/deepseek-v4-flash"], thinking: "high" },
|
|
13
|
+
"product-audit": { model: ["nan/glm5.3", "nan/mimo-v2.5", "nan/deepseek-v4-flash"], thinking: "high" },
|
|
14
|
+
"review-change": { model: ["nan/mimo-v2.5", "nan/glm5.3-flash", "nan/deepseek-v4-flash"], thinking: "high" },
|
|
15
|
+
"unit-lane": { model: ["nan/mimo-v2.5", "nan/glm5.3-flash", "nan/deepseek-v4-flash"], thinking: "high" },
|
|
16
|
+
"triage-issue": { model: ["nan/glm5.3-flash", "nan/deepseek-v4-flash"], thinking: "inherit" },
|
|
17
|
+
"init-workspace": { model: ["nan/glm5.3-flash", "nan/deepseek-v4-flash"], thinking: "inherit" },
|
|
18
|
+
"execute-phase": { model: ["nan/deepseek-v4-flash", "nan/glm5.3-flash"], thinking: "inherit" },
|
|
19
|
+
"fold-findings": { model: ["nan/deepseek-v4-flash", "nan/glm5.3-flash"], thinking: "inherit" },
|
|
20
|
+
"log-session": { model: ["nan/deepseek-v4-flash", "nan/glm5.3-flash"], thinking: "inherit" },
|
|
21
|
+
"workflow-status": { model: ["nan/deepseek-v4-flash", "nan/glm5.3-flash"], thinking: "inherit" },
|
|
22
|
+
"audit-docs": { model: ["nan/deepseek-v4-flash", "nan/glm5.3-flash"], thinking: "inherit" },
|
|
23
|
+
"discover-repository-state": { model: ["nan/deepseek-v4-flash", "nan/glm5.3-flash"], thinking: "inherit" },
|
|
24
|
+
"resolve-repository-state": { model: ["nan/deepseek-v4-flash", "nan/glm5.3-flash"], thinking: "inherit" },
|
|
25
|
+
"advance": { model: ["nan/glm5.3-flash", "nan/deepseek-v4-flash"], thinking: "inherit" },
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
};
|
package/dist/config/schema.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { MAX_MODEL_CHAIN, SETTLE_POLICIES, THINKING_LEVELS, UNAVAILABLE_ROUTE_POLICIES } from "./types.js";
|
|
1
|
+
import { MAX_MODEL_CHAIN, SETTLE_POLICIES, THINKING_LEVELS, UNAVAILABLE_ROUTE_POLICIES, PROFILE_FALLBACK_APPLY_TO, PROFILE_FALLBACK_RESUME, } from "./types.js";
|
|
2
2
|
import { OPERATIONS, PATH_GLOB_MAX_LENGTH, PHASE_STATES, REQUIREMENTS } from "./path-policy.js";
|
|
3
3
|
/**
|
|
4
4
|
* Strict validator for one config file (SPEC S5-S8, D-E5).
|
|
@@ -9,7 +9,18 @@ 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([
|
|
12
|
+
const ROOT_KEYS = new Set([
|
|
13
|
+
"default",
|
|
14
|
+
"commands",
|
|
15
|
+
"onUnavailableRoute",
|
|
16
|
+
"onSettle",
|
|
17
|
+
"pathProtection",
|
|
18
|
+
"advance",
|
|
19
|
+
"recommendedModels",
|
|
20
|
+
"profiles",
|
|
21
|
+
"profileOrder",
|
|
22
|
+
"profileFallback",
|
|
23
|
+
]);
|
|
13
24
|
const ROUTE_KEYS = new Set(["model", "thinking"]);
|
|
14
25
|
const COMMAND_NAME = /^[a-z0-9][a-z0-9._-]*$/u;
|
|
15
26
|
function isRecord(value) {
|
|
@@ -41,6 +52,112 @@ function isUnavailableRoutePolicy(value) {
|
|
|
41
52
|
function isSettlePolicy(value) {
|
|
42
53
|
return SETTLE_POLICIES.includes(value);
|
|
43
54
|
}
|
|
55
|
+
/** Strict validator for `recommendedModels` (feature 63, AC1). */
|
|
56
|
+
function isRecommendedModels(value) {
|
|
57
|
+
return typeof value === "boolean";
|
|
58
|
+
}
|
|
59
|
+
/** Strict validator for the `profileOrder` array (feature 63, AC6). */
|
|
60
|
+
const PROFILE_ORDER_ENTRY = /^[a-z0-9][a-z0-9._-]*$/u;
|
|
61
|
+
function checkProfileOrder(value, issues) {
|
|
62
|
+
if (!Array.isArray(value)) {
|
|
63
|
+
issues.push({ path: "$.profileOrder", message: "must be an array of non-empty strings" });
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
if (value.length === 0) {
|
|
67
|
+
issues.push({ path: "$.profileOrder", message: "must be a non-empty array of non-empty strings" });
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
for (let i = 0; i < value.length; i++) {
|
|
71
|
+
const entry = value[i];
|
|
72
|
+
if (typeof entry !== "string" || !PROFILE_ORDER_ENTRY.test(entry)) {
|
|
73
|
+
issues.push({ path: "$.profileOrder", message: "must be a non-empty array of non-empty strings" });
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return value;
|
|
78
|
+
}
|
|
79
|
+
/** Strict validator for the `profileFallback` object (feature 63). */
|
|
80
|
+
const PROFILE_FALLBACK_KEYS = new Set(["applyTo", "resume", "retryAfterSeconds"]);
|
|
81
|
+
;
|
|
82
|
+
function checkProfileFallback(value, issues) {
|
|
83
|
+
if (!isRecord(value)) {
|
|
84
|
+
issues.push({ path: "$.profileFallback", message: "must be an object with applyTo, resume, retryAfterSeconds" });
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
const out = {};
|
|
88
|
+
for (const key of Object.keys(value)) {
|
|
89
|
+
if (!PROFILE_FALLBACK_KEYS.has(key)) {
|
|
90
|
+
issues.push({ path: `$.profileFallback.${key}`, message: `unknown profileFallback key "${key}" (allowed: applyTo, resume, retryAfterSeconds)` });
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
const v = value[key];
|
|
94
|
+
if (key === "applyTo") {
|
|
95
|
+
if (!PROFILE_FALLBACK_APPLY_TO.includes(v)) {
|
|
96
|
+
issues.push({ path: "$.profileFallback.applyTo", message: `must be one of ${PROFILE_FALLBACK_APPLY_TO.join(", ")}` });
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
out.applyTo = v;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
else if (key === "resume") {
|
|
103
|
+
if (!PROFILE_FALLBACK_RESUME.includes(v)) {
|
|
104
|
+
issues.push({ path: "$.profileFallback.resume", message: `must be one of ${PROFILE_FALLBACK_RESUME.join(", ")}` });
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
out.resume = v;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
else if (key === "retryAfterSeconds") {
|
|
111
|
+
if (typeof v !== "number" || !Number.isInteger(v) || v < 1) {
|
|
112
|
+
issues.push({ path: "$.profileFallback.retryAfterSeconds", message: "must be a positive integer" });
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
out.retryAfterSeconds = v;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return out;
|
|
120
|
+
}
|
|
121
|
+
/** Strict validator for one profile file (feature 63, AC5). */
|
|
122
|
+
const PROFILE_FILE_KEYS = new Set(["default", "commands"]);
|
|
123
|
+
;
|
|
124
|
+
function checkProfileFile(value, path, issues) {
|
|
125
|
+
if (!isRecord(value)) {
|
|
126
|
+
issues.push({ path, message: "profile must be an object with optional default/commands keys" });
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
|
129
|
+
const out = {};
|
|
130
|
+
for (const key of Object.keys(value)) {
|
|
131
|
+
if (!PROFILE_FILE_KEYS.has(key)) {
|
|
132
|
+
issues.push({ path: `${path}.${key}`, message: `unknown profile key "${key}" (allowed: default, commands)` });
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (key === "default") {
|
|
136
|
+
const route = checkRoute(value[key], `${path}.default`, issues);
|
|
137
|
+
if (route)
|
|
138
|
+
out.default = route;
|
|
139
|
+
}
|
|
140
|
+
else if (key === "commands") {
|
|
141
|
+
if (!isRecord(value[key])) {
|
|
142
|
+
issues.push({ path: `${path}.commands`, message: "commands must be an object mapping command names to routes" });
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
const commands = {};
|
|
146
|
+
for (const [cmdName, raw] of Object.entries(value[key])) {
|
|
147
|
+
if (!COMMAND_NAME.test(cmdName)) {
|
|
148
|
+
issues.push({ path: `${path}.commands.${displayKey(cmdName)}`, message: "command name must be a single lowercase slug" });
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
const route = checkRoute(raw, `${path}.commands.${displayKey(cmdName)}`, issues);
|
|
152
|
+
if (route)
|
|
153
|
+
commands[cmdName] = route;
|
|
154
|
+
}
|
|
155
|
+
out.commands = commands;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return out;
|
|
160
|
+
}
|
|
44
161
|
/** Strict validator for the optional `advance` conductor config (feature 62). */
|
|
45
162
|
const ADVANCE_KEYS = new Set(["iterationsCap", "sensitivePaths", "securityPaths", "runLogPath"]);
|
|
46
163
|
function checkAdvance(value, path, issues) {
|
|
@@ -214,7 +331,7 @@ function validateConfig(value) {
|
|
|
214
331
|
if (!ROOT_KEYS.has(key)) {
|
|
215
332
|
issues.push({
|
|
216
333
|
path: `$.${displayKey(key)}`,
|
|
217
|
-
message: `unknown config key "${key}" (allowed: default, commands, onUnavailableRoute, onSettle, pathProtection, advance)`,
|
|
334
|
+
message: `unknown config key "${key}" (allowed: default, commands, onUnavailableRoute, onSettle, pathProtection, advance, recommendedModels, profiles, profileOrder, profileFallback)`,
|
|
218
335
|
});
|
|
219
336
|
}
|
|
220
337
|
}
|
|
@@ -273,6 +390,45 @@ function validateConfig(value) {
|
|
|
273
390
|
if (override)
|
|
274
391
|
config.pathProtection = override;
|
|
275
392
|
}
|
|
393
|
+
if (value.recommendedModels !== undefined) {
|
|
394
|
+
if (!isRecommendedModels(value.recommendedModels)) {
|
|
395
|
+
issues.push({
|
|
396
|
+
path: "$.recommendedModels",
|
|
397
|
+
message: `must be a boolean, got ${describe(value.recommendedModels)}`,
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
else {
|
|
401
|
+
config.recommendedModels = value.recommendedModels;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
if (value.profiles !== undefined) {
|
|
405
|
+
if (!isRecord(value.profiles)) {
|
|
406
|
+
issues.push({ path: "$.profiles", message: "must be an object mapping profile names to profile definitions" });
|
|
407
|
+
}
|
|
408
|
+
else {
|
|
409
|
+
const profiles = {};
|
|
410
|
+
for (const [profileName, profileDef] of Object.entries(value.profiles)) {
|
|
411
|
+
if (!COMMAND_NAME.test(profileName)) {
|
|
412
|
+
issues.push({ path: `$.profiles.${displayKey(profileName)}`, message: "profile name must be a single lowercase slug" });
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
const profile = checkProfileFile(profileDef, `$.profiles.${displayKey(profileName)}`, issues);
|
|
416
|
+
if (profile)
|
|
417
|
+
profiles[profileName] = profile;
|
|
418
|
+
}
|
|
419
|
+
config.profiles = profiles;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
if (value.profileOrder !== undefined) {
|
|
423
|
+
const order = checkProfileOrder(value.profileOrder, issues);
|
|
424
|
+
if (order)
|
|
425
|
+
config.profileOrder = order;
|
|
426
|
+
}
|
|
427
|
+
if (value.profileFallback !== undefined) {
|
|
428
|
+
const fb = checkProfileFallback(value.profileFallback, issues);
|
|
429
|
+
if (fb)
|
|
430
|
+
config.profileFallback = fb;
|
|
431
|
+
}
|
|
276
432
|
return issues.length > 0 ? { ok: false, issues } : { ok: true, config };
|
|
277
433
|
}
|
|
278
434
|
/**
|