@xingular/harness 1.1.1 → 1.2.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/CHANGELOG.md +13 -0
- package/README.md +28 -25
- package/dist/cli.js +6675 -2501
- package/dist/sdk/accounts/accounts.d.ts +27 -3
- package/dist/sdk/accounts/accounts.js +78 -24
- package/dist/sdk/accounts/secrets.d.ts +12 -0
- package/dist/sdk/accounts/secrets.js +114 -7
- package/dist/sdk/accounts/store.d.ts +26 -1
- package/dist/sdk/accounts/store.js +1 -1
- package/dist/sdk/agents/builtin.js +15 -1
- package/dist/sdk/agents/planResearch.d.ts +5 -0
- package/dist/sdk/agents/planResearch.js +19 -0
- package/dist/sdk/agents/profiles.d.ts +2 -1
- package/dist/sdk/agents/profiles.js +9 -4
- package/dist/sdk/annotations/pageScript.js +349 -48
- package/dist/sdk/annotations/part.js +34 -23
- package/dist/sdk/annotations/store.d.ts +7 -0
- package/dist/sdk/annotations/store.js +13 -5
- package/dist/sdk/browser/backend.d.ts +16 -0
- package/dist/sdk/browser/backend.js +17 -0
- package/dist/sdk/browser/contracts.d.ts +19 -0
- package/dist/sdk/browser/contracts.js +116 -0
- package/dist/sdk/browser/tools.d.ts +1 -4
- package/dist/sdk/browser/tools.js +14 -91
- package/dist/sdk/cli.js +5 -4
- package/dist/sdk/commands/list.d.ts +3 -0
- package/dist/sdk/commands/list.js +75 -0
- package/dist/sdk/commands/registry.d.ts +250 -0
- package/dist/sdk/commands/registry.js +47 -0
- package/dist/sdk/commands/routes.d.ts +16 -0
- package/dist/sdk/commands/routes.js +33 -0
- package/dist/sdk/core/commandBudget.d.ts +23 -0
- package/dist/sdk/core/commandBudget.js +57 -0
- package/dist/sdk/core/index.js +17 -1
- package/dist/sdk/core/interactiveProfile.d.ts +46 -0
- package/dist/sdk/core/interactiveProfile.js +143 -0
- package/dist/sdk/core/workspace.d.ts +13 -0
- package/dist/sdk/core/workspace.js +88 -0
- package/dist/sdk/core/workspaceBashTool.d.ts +4 -0
- package/dist/sdk/core/workspaceBashTool.js +36 -0
- package/dist/sdk/core/workspaceSandbox.d.ts +31 -0
- package/dist/sdk/core/workspaceSandbox.js +320 -0
- package/dist/sdk/core/workspaceWriteOperations.d.ts +22 -0
- package/dist/sdk/core/workspaceWriteOperations.js +206 -0
- package/dist/sdk/doctor/types.d.ts +2 -2
- package/dist/sdk/harness.d.ts +7 -0
- package/dist/sdk/harness.js +42 -2
- package/dist/sdk/index.d.ts +6 -1
- package/dist/sdk/index.js +3 -0
- package/dist/sdk/pi/runtime.d.ts +2 -1
- package/dist/sdk/pi/runtime.js +2 -2
- package/dist/sdk/plugins/builtin.js +29 -12
- package/dist/sdk/plugins/builtinRegistry.d.ts +10 -3
- package/dist/sdk/plugins/builtinRegistry.js +30 -8
- package/dist/sdk/plugins/catalogue.js +2 -1
- package/dist/sdk/plugins/commandPrefix.d.ts +8 -0
- package/dist/sdk/plugins/commandPrefix.js +21 -0
- package/dist/sdk/plugins/firstParty.d.ts +1 -1
- package/dist/sdk/plugins/firstParty.js +8 -2
- package/dist/sdk/plugins/inventory.js +4 -3
- package/dist/sdk/plugins/manifest.d.ts +2 -0
- package/dist/sdk/plugins/manifest.js +2 -1
- package/dist/sdk/plugins/surfaces.d.ts +5 -0
- package/dist/sdk/plugins/surfaces.js +11 -0
- package/dist/sdk/plugins/types.d.ts +15 -0
- package/dist/sdk/prompt/compile.d.ts +14 -0
- package/dist/sdk/prompt/compile.js +65 -29
- package/dist/sdk/prompt/standard.d.ts +17 -0
- package/dist/sdk/prompt/standard.js +21 -0
- package/dist/sdk/server/methods.js +18 -0
- package/dist/sdk/server/remote.js +36 -11
- package/dist/sdk/sessions/compactionCheckpoint.d.ts +11 -0
- package/dist/sdk/sessions/compactionCheckpoint.js +79 -0
- package/dist/sdk/sessions/continuation.d.ts +3 -0
- package/dist/sdk/sessions/continuation.js +5 -1
- package/dist/sdk/sessions/contracts.d.ts +168 -0
- package/dist/sdk/sessions/contracts.js +59 -1
- package/dist/sdk/sessions/control.d.ts +72 -0
- package/dist/sdk/sessions/control.js +158 -0
- package/dist/sdk/sessions/initialBash.d.ts +7 -0
- package/dist/sdk/sessions/initialBash.js +7 -0
- package/dist/sdk/sessions/planMode.d.ts +14 -3
- package/dist/sdk/sessions/planMode.js +73 -16
- package/dist/sdk/sessions/registry.d.ts +1 -1
- package/dist/sdk/sessions/registry.js +2 -2
- package/dist/sdk/sessions/runStore.d.ts +14 -0
- package/dist/sdk/sessions/runStore.js +35 -0
- package/dist/sdk/sessions/runner.d.ts +40 -2
- package/dist/sdk/sessions/runner.js +161 -13
- package/dist/sdk/sessions/sessions.d.ts +23 -3
- package/dist/sdk/sessions/sessions.js +202 -17
- package/dist/sdk/sessions/subagentService.d.ts +9 -0
- package/dist/sdk/sessions/subagentService.js +63 -4
- package/dist/sdk/sessions/workSlice.d.ts +103 -0
- package/dist/sdk/sessions/workSlice.js +297 -0
- package/dist/sdk/sessions/workspaceBoundary.d.ts +15 -0
- package/dist/sdk/sessions/workspaceBoundary.js +46 -0
- package/dist/sdk/terminal/browserControl.js +2 -1
- package/dist/sdk/terminal/controller.d.ts +11 -3
- package/dist/sdk/terminal/controller.js +40 -13
- package/dist/sdk/terminal/opentui/theme.d.ts +43 -0
- package/dist/sdk/terminal/opentui/theme.js +87 -0
- package/dist/sdk/terminal/piExtension.d.ts +6 -1
- package/dist/sdk/terminal/piExtension.js +154 -54
- package/dist/sdk/terminal/standalone.d.ts +7 -0
- package/dist/sdk/terminal/standalone.js +72 -13
- package/dist/sdk/terminal/subagentMonitor.d.ts +11 -2
- package/dist/sdk/terminal/subagentMonitor.js +32 -1
- package/dist/sdk/terminal/terminals.d.ts +19 -2
- package/dist/sdk/terminal/terminals.js +245 -35
- package/dist/sdk/terminal/tui/annotateCommand.d.ts +1 -1
- package/dist/sdk/terminal/tui/annotateCommand.js +3 -3
- package/dist/sdk/terminal/tui/commandPages.d.ts +3 -1
- package/dist/sdk/terminal/tui/commandPages.js +3 -0
- package/dist/sdk/terminal/tui/commands.d.ts +2 -0
- package/dist/sdk/terminal/tui/commands.js +205 -124
- package/dist/sdk/terminal/tui/list.d.ts +14 -1
- package/dist/sdk/terminal/tui/list.js +96 -14
- package/dist/sdk/terminal/tui/pageOverlay.js +13 -4
- package/dist/sdk/terminal/tui/pages/annotatePage.d.ts +1 -1
- package/dist/sdk/terminal/tui/pages/notesPage.d.ts +5 -5
- package/dist/sdk/terminal/tui/pages/notesPage.js +18 -18
- package/dist/sdk/terminal/tui/pages/pluginsPage.d.ts +0 -5
- package/dist/sdk/terminal/tui/pages/pluginsPage.js +9 -3
- package/dist/sdk/terminal/tui/pages/subagentsPage.d.ts +1 -0
- package/dist/sdk/terminal/tui/pages/subagentsPage.js +98 -16
- package/dist/sdk/terminal/tui/pages/usagePage.d.ts +43 -0
- package/dist/sdk/terminal/tui/pages/usagePage.js +370 -0
- package/dist/sdk/terminal/tui/piAdapter.d.ts +1 -2
- package/dist/sdk/terminal/tui/piAdapter.js +3 -5
- package/dist/sdk/terminal/tui/pluginCommands.d.ts +2 -0
- package/dist/sdk/terminal/tui/pluginCommands.js +13 -9
- package/dist/sdk/terminal/tui/runtime.js +23 -3
- package/dist/sdk/terminal/tui/sessionPicker.d.ts +40 -0
- package/dist/sdk/terminal/tui/sessionPicker.js +141 -0
- package/dist/sdk/tools/checklist.js +1 -0
- package/dist/sdk/tools/checkpoint.d.ts +27 -0
- package/dist/sdk/tools/checkpoint.js +154 -0
- package/dist/sdk/tools/getCapabilities.d.ts +31 -0
- package/dist/sdk/tools/getCapabilities.js +34 -0
- package/dist/sdk/tools/spawnSubagent.js +4 -3
- package/dist/sdk/usage/analytics.d.ts +20 -0
- package/dist/sdk/usage/analytics.js +151 -0
- package/dist/sdk/usage/ledger.d.ts +38 -0
- package/dist/sdk/usage/ledger.js +142 -0
- package/dist/sdk/usage/limits/adapters.d.ts +11 -0
- package/dist/sdk/usage/limits/adapters.js +349 -0
- package/dist/sdk/usage/limits/capture.d.ts +8 -0
- package/dist/sdk/usage/limits/capture.js +47 -0
- package/dist/sdk/usage/limits/http.d.ts +12 -0
- package/dist/sdk/usage/limits/http.js +51 -0
- package/dist/sdk/usage/limits/normalize.d.ts +15 -0
- package/dist/sdk/usage/limits/normalize.js +211 -0
- package/dist/sdk/usage/limits/service.d.ts +30 -0
- package/dist/sdk/usage/limits/service.js +176 -0
- package/dist/sdk/usage/limits/types.d.ts +65 -0
- package/dist/sdk/usage/limits/types.js +1 -0
- package/dist/sdk/usage/serverMethods.d.ts +3 -0
- package/dist/sdk/usage/serverMethods.js +22 -0
- package/dist/sdk/usage/service.d.ts +32 -0
- package/dist/sdk/usage/service.js +66 -0
- package/dist/sdk/usage/settings.d.ts +7 -0
- package/dist/sdk/usage/settings.js +28 -0
- package/dist/sdk/usage/types.d.ts +60 -0
- package/dist/sdk/usage/types.js +4 -0
- package/dist/terminal-opentui.js +5207 -1883
- package/dist/terminal-pi.js +1658 -271
- package/docs/architecture/02-sessions-and-runs.md +128 -26
- package/docs/architecture/04-prompt.md +50 -12
- package/docs/architecture/06-plugins-and-capabilities.md +24 -7
- package/docs/architecture/07-tools-and-gate.md +104 -24
- package/docs/architecture/08-memory-and-history.md +4 -2
- package/docs/architecture/09-accounts-and-connectors.md +31 -1
- package/docs/architecture/10-server-protocol.md +58 -3
- package/docs/architecture/11-terminal.md +105 -42
- package/docs/architecture/12-storage.md +30 -2
- package/docs/architecture/13-world-tools.md +22 -1
- package/docs/architecture/14-surfaces-and-parity.md +54 -7
- package/docs/architecture/README.md +3 -1
- package/docs/architecture/proposals/PROP-0017-page-annotations.md +6 -2
- package/docs/architecture/proposals/PROP-0018-annotation-queue-and-app-targets.md +1 -1
- package/docs/architecture/proposals/PROP-0031-plugin-command-owner-namespaces.md +54 -0
- package/docs/architecture/proposals/PROP-0032-planning-quality.md +196 -0
- package/docs/architecture/proposals/PROP-0033-compaction-plan-checkpoints.md +75 -0
- package/docs/architecture/proposals/PROP-0034-conversation-fast-mode.md +120 -0
- package/docs/architecture/proposals/PROP-0035-retain-subagent-status.md +47 -0
- package/docs/architecture/proposals/PROP-0036-first-class-harness-commands.md +59 -0
- package/docs/architecture/proposals/PROP-0037-workspace-write-boundary.md +63 -0
- package/docs/architecture/proposals/PROP-0038-annotate-open-shadow-roots.md +73 -0
- package/docs/architecture/proposals/PROP-0039-workspace-boundary-hardening.md +139 -0
- package/docs/architecture/proposals/PROP-0040-one-launch-path.md +87 -0
- package/docs/architecture/proposals/PROP-0041-bounded-work-slices.md +247 -0
- package/docs/architecture/proposals/PROP-0042-usage-and-provider-limits.md +127 -0
- package/docs/architecture/proposals/PROP-0043-read-only-plan-bash.md +74 -0
- package/docs/architecture/proposals/README.md +14 -0
- package/docs/decisions/ADR-0001-owned-terminal.md +1 -1
- package/docs/decisions/ADR-0021-required-command-namespaces.md +1 -1
- package/docs/decisions/ADR-0031-plugin-command-owner-namespaces.md +37 -0
- package/docs/decisions/ADR-0032-compaction-run-checkpoints.md +33 -0
- package/docs/decisions/ADR-0033-plan-mode-research-delegation.md +47 -0
- package/docs/decisions/ADR-0034-first-class-harness-terminal-commands.md +25 -0
- package/docs/decisions/ADR-0035-one-launch-path.md +40 -0
- package/docs/decisions/ADR-0036-workspace-write-operation-boundary.md +54 -0
- package/docs/decisions/ADR-0037-usage-ledger-and-provider-limits.md +44 -0
- package/docs/decisions/ADR-0038-plan-bash-os-read-only.md +29 -0
- package/docs/experiments/EXP-0003-performance-audit/data/session-runtime-replication.json +420 -0
- package/docs/experiments/EXP-0003-performance-audit/data/session-runtime.json +420 -0
- package/docs/experiments/EXP-0003-performance-audit/data/terminal-compact.json +376 -0
- package/docs/experiments/EXP-0003-performance-audit/data/terminal-history.json +422 -0
- package/docs/experiments/EXP-0003-performance-audit/data/terminal-question.json +95 -0
- package/docs/experiments/EXP-0003-performance-audit/data/terminal-wide.json +1036 -0
- package/docs/experiments/EXP-0003-performance-audit/plan.md +109 -0
- package/docs/experiments/EXP-0003-performance-audit/results.md +151 -0
- package/docs/handoff-2026-10-25-app-parity-and-solo-tabs.md +202 -0
- package/docs/parity.md +80 -24
- package/docs/releases/1.1.1.md +28 -0
- package/docs/releases/1.2.0.md +49 -0
- package/docs/releases/4.0.0.md +10 -9
- package/docs/releases/README.md +35 -30
- package/docs/study/00-index.md +3 -0
- package/docs/study/STUDY-0003-long-horizon-runs.md +210 -0
- package/docs/terminal-tui.md +100 -59
- package/package.json +4 -3
|
@@ -3,8 +3,8 @@ import { type Bus } from "../core/bus.js";
|
|
|
3
3
|
import type { RunEvent, RunRecord } from "../sessions/contracts.js";
|
|
4
4
|
import * as choices from "./choices.js";
|
|
5
5
|
import * as env from "./env.js";
|
|
6
|
-
import { type SecretStore } from "./secrets.js";
|
|
7
|
-
import type { Account, AccountsOverview } from "./store.js";
|
|
6
|
+
import { type CredentialLockOptions, type SecretStore } from "./secrets.js";
|
|
7
|
+
import type { Account, AccountsOverview, Limit } from "./store.js";
|
|
8
8
|
export type IssueCategory = 'auth_expired' | 'rate_limited' | 'quota' | 'network' | 'unknown';
|
|
9
9
|
/** Best-effort classification of a provider error message. */
|
|
10
10
|
export declare function classifyMessage(message: string): IssueCategory | null;
|
|
@@ -35,11 +35,13 @@ export interface AccountsOptions {
|
|
|
35
35
|
dir: string;
|
|
36
36
|
secrets: SecretStore;
|
|
37
37
|
bus: Bus;
|
|
38
|
+
credentialLock?: CredentialLockOptions;
|
|
38
39
|
}
|
|
39
40
|
export declare class Accounts {
|
|
40
41
|
readonly dir: string;
|
|
41
42
|
private readonly secrets;
|
|
42
43
|
private readonly bus;
|
|
44
|
+
private readonly credentialLockOptions;
|
|
43
45
|
private readonly runtimes;
|
|
44
46
|
private readonly logins;
|
|
45
47
|
private readonly latestLogins;
|
|
@@ -48,6 +50,11 @@ export declare class Accounts {
|
|
|
48
50
|
/** The credentials this process wrote to, or found in, each account's auth.json when its first terminal began. */
|
|
49
51
|
private readonly terminalSnapshots;
|
|
50
52
|
private installationDeviceIdPromise;
|
|
53
|
+
/**
|
|
54
|
+
* Lets the limits service observe each account runtime's provider responses. The Harness sets
|
|
55
|
+
* it once; with none, a runtime behaves exactly as before.
|
|
56
|
+
*/
|
|
57
|
+
observeModels: ((models: unknown, accountId: string) => void) | null;
|
|
51
58
|
constructor(options: AccountsOptions);
|
|
52
59
|
private changed;
|
|
53
60
|
overview(): AccountsOverview;
|
|
@@ -60,13 +67,17 @@ export declare class Accounts {
|
|
|
60
67
|
resolve(accountId?: string | null, poolId?: string | null): Account;
|
|
61
68
|
context(account: Account): env.AccountContext;
|
|
62
69
|
readCredential(accountId: string, provider: string): Promise<Credential | null>;
|
|
70
|
+
private credentialLock;
|
|
63
71
|
/** Stores a provider's credential, or removes it when `credential` is `null`. */
|
|
64
72
|
writeCredential(accountId: string, provider: string, credential: Credential | null): Promise<void>;
|
|
73
|
+
/** The entire refresh read/modify/write is coordinated across services and processes. */
|
|
74
|
+
modifyCredential(accountId: string, provider: string, change: (current: Credential | undefined) => Promise<Credential | undefined>): Promise<Credential | undefined>;
|
|
75
|
+
private persistCredential;
|
|
65
76
|
listCredentials(accountId: string): Promise<{
|
|
66
77
|
providerId: string;
|
|
67
78
|
type: 'api_key' | 'oauth';
|
|
68
79
|
}[]>;
|
|
69
|
-
/** Pi
|
|
80
|
+
/** Pi and explicit account edits share the same cross-process credential lock. */
|
|
70
81
|
private credentialStore;
|
|
71
82
|
/** Pi's OAuth device ID belongs to this Harness installation, not to one isolated account. */
|
|
72
83
|
installationDeviceId(): Promise<string>;
|
|
@@ -160,6 +171,19 @@ export declare class Accounts {
|
|
|
160
171
|
private releaseLease;
|
|
161
172
|
/** Tracks account health and limits from run events. `run` is the event's run record, when it is known. */
|
|
162
173
|
onRunEvent(event: RunEvent, run: RunRecord | null): void;
|
|
174
|
+
/**
|
|
175
|
+
* Records what a provider reported about its limits. An endpoint reading replaces that provider's
|
|
176
|
+
* limits; a header reading merges by id. Windows whose reset has passed are dropped. No credential
|
|
177
|
+
* is stored here.
|
|
178
|
+
*/
|
|
179
|
+
applyLimits(accountId: string, readouts: readonly {
|
|
180
|
+
provider: string;
|
|
181
|
+
limits: Limit[];
|
|
182
|
+
source: 'headers' | 'provider' | 'estimate' | 'unavailable';
|
|
183
|
+
reason: string | null;
|
|
184
|
+
observedAt: string;
|
|
185
|
+
plan?: string | null;
|
|
186
|
+
}[]): AccountsOverview;
|
|
163
187
|
private applyStatus;
|
|
164
188
|
}
|
|
165
189
|
export {};
|
|
@@ -10,7 +10,7 @@ import { atomicWrite, now, readJson, safeId } from "../core/fs.js";
|
|
|
10
10
|
import { loadPi } from "../pi/runtime.js";
|
|
11
11
|
import * as choices from "./choices.js";
|
|
12
12
|
import * as env from "./env.js";
|
|
13
|
-
import { secretKey } from "./secrets.js";
|
|
13
|
+
import { secretKey, withCredentialLock } from "./secrets.js";
|
|
14
14
|
import * as store from "./store.js";
|
|
15
15
|
/** Best-effort classification of a provider error message. */
|
|
16
16
|
export function classifyMessage(message) {
|
|
@@ -64,6 +64,7 @@ export class Accounts {
|
|
|
64
64
|
dir;
|
|
65
65
|
secrets;
|
|
66
66
|
bus;
|
|
67
|
+
credentialLockOptions;
|
|
67
68
|
runtimes = new Map();
|
|
68
69
|
logins = new Map();
|
|
69
70
|
latestLogins = new Map();
|
|
@@ -72,10 +73,16 @@ export class Accounts {
|
|
|
72
73
|
/** The credentials this process wrote to, or found in, each account's auth.json when its first terminal began. */
|
|
73
74
|
terminalSnapshots = new Map();
|
|
74
75
|
installationDeviceIdPromise = null;
|
|
76
|
+
/**
|
|
77
|
+
* Lets the limits service observe each account runtime's provider responses. The Harness sets
|
|
78
|
+
* it once; with none, a runtime behaves exactly as before.
|
|
79
|
+
*/
|
|
80
|
+
observeModels = null;
|
|
75
81
|
constructor(options) {
|
|
76
82
|
this.dir = options.dir;
|
|
77
83
|
this.secrets = options.secrets;
|
|
78
84
|
this.bus = options.bus;
|
|
85
|
+
this.credentialLockOptions = options.credentialLock ?? {};
|
|
79
86
|
}
|
|
80
87
|
changed(payload = {}) {
|
|
81
88
|
this.bus.emit({ type: 'accounts.changed', ...payload });
|
|
@@ -116,18 +123,40 @@ export class Accounts {
|
|
|
116
123
|
return null;
|
|
117
124
|
}
|
|
118
125
|
}
|
|
126
|
+
credentialLock(key, work) {
|
|
127
|
+
return withCredentialLock(join(this.dir, '.credential-locks'), key, work, this.credentialLockOptions);
|
|
128
|
+
}
|
|
119
129
|
/** Stores a provider's credential, or removes it when `credential` is `null`. */
|
|
120
130
|
async writeCredential(accountId, provider, credential) {
|
|
121
131
|
this.find(accountId);
|
|
122
132
|
const name = validProvider(provider);
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
133
|
+
await this.credentialLock(`provider/${accountId}/${name}`, () => this.persistCredential(accountId, name, credential));
|
|
134
|
+
}
|
|
135
|
+
/** The entire refresh read/modify/write is coordinated across services and processes. */
|
|
136
|
+
async modifyCredential(accountId, provider, change) {
|
|
137
|
+
this.find(accountId);
|
|
138
|
+
const name = validProvider(provider);
|
|
139
|
+
return this.credentialLock(`provider/${accountId}/${name}`, async () => {
|
|
140
|
+
const current = (await this.readCredential(accountId, name)) ?? undefined;
|
|
141
|
+
const updated = await change(current);
|
|
142
|
+
if (updated !== undefined)
|
|
143
|
+
await this.persistCredential(accountId, name, updated);
|
|
144
|
+
return updated ?? current;
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
async persistCredential(accountId, name, credential) {
|
|
148
|
+
// Different providers still share the account's provider index. Keep its short commit serialized.
|
|
149
|
+
await this.credentialLock(`account/${accountId}`, async () => {
|
|
150
|
+
this.find(accountId);
|
|
151
|
+
if (credential === null)
|
|
152
|
+
await this.secrets.delete(secretKey(accountId, `pi-auth/${name}`));
|
|
153
|
+
else
|
|
154
|
+
await this.secrets.set(secretKey(accountId, `pi-auth/${name}`), JSON.stringify(credential));
|
|
155
|
+
store.updateState(this.dir, accountId, (state) => {
|
|
156
|
+
state.piProviders = state.piProviders.filter((existing) => existing !== name);
|
|
157
|
+
if (credential !== null)
|
|
158
|
+
state.piProviders = [...state.piProviders, name].sort();
|
|
159
|
+
});
|
|
131
160
|
});
|
|
132
161
|
}
|
|
133
162
|
async listCredentials(accountId) {
|
|
@@ -140,25 +169,13 @@ export class Accounts {
|
|
|
140
169
|
}
|
|
141
170
|
return listed;
|
|
142
171
|
}
|
|
143
|
-
/** Pi
|
|
172
|
+
/** Pi and explicit account edits share the same cross-process credential lock. */
|
|
144
173
|
credentialStore(accountId) {
|
|
145
|
-
let chain = Promise.resolve();
|
|
146
|
-
const serialize = (work) => {
|
|
147
|
-
const next = chain.then(work);
|
|
148
|
-
chain = next.catch(() => undefined);
|
|
149
|
-
return next;
|
|
150
|
-
};
|
|
151
174
|
return {
|
|
152
175
|
read: async (providerId) => (await this.readCredential(accountId, providerId)) ?? undefined,
|
|
153
176
|
list: () => this.listCredentials(accountId),
|
|
154
|
-
modify: (providerId, change) =>
|
|
155
|
-
|
|
156
|
-
const updated = await change(current);
|
|
157
|
-
if (updated !== undefined)
|
|
158
|
-
await this.writeCredential(accountId, providerId, updated);
|
|
159
|
-
return updated ?? current;
|
|
160
|
-
}),
|
|
161
|
-
delete: (providerId) => serialize(() => this.writeCredential(accountId, providerId, null))
|
|
177
|
+
modify: (providerId, change) => this.modifyCredential(accountId, providerId, change),
|
|
178
|
+
delete: (providerId) => this.writeCredential(accountId, providerId, null)
|
|
162
179
|
};
|
|
163
180
|
}
|
|
164
181
|
/** Pi's OAuth device ID belongs to this Harness installation, not to one isolated account. */
|
|
@@ -187,6 +204,14 @@ export class Accounts {
|
|
|
187
204
|
const configDir = 'configDir' in account ? account.configDir : env.ensureHome(this.dir, account);
|
|
188
205
|
runtime = loadPi()
|
|
189
206
|
.then(({ ModelRuntime }) => ModelRuntime.create({ credentials: this.credentialStore(id), modelsPath: join(configDir, 'models.json') }))
|
|
207
|
+
.then((created) => {
|
|
208
|
+
// Reading provider responses never changes what a request does; a broken observer is ignored.
|
|
209
|
+
try {
|
|
210
|
+
this.observeModels?.(created, id);
|
|
211
|
+
}
|
|
212
|
+
catch { /* observation is best effort */ }
|
|
213
|
+
return created;
|
|
214
|
+
})
|
|
190
215
|
.catch((error) => {
|
|
191
216
|
this.runtimes.delete(id);
|
|
192
217
|
throw error;
|
|
@@ -690,6 +715,35 @@ export class Accounts {
|
|
|
690
715
|
this.changed({ accountId, health: state.health, previous });
|
|
691
716
|
}
|
|
692
717
|
}
|
|
718
|
+
/**
|
|
719
|
+
* Records what a provider reported about its limits. An endpoint reading replaces that provider's
|
|
720
|
+
* limits; a header reading merges by id. Windows whose reset has passed are dropped. No credential
|
|
721
|
+
* is stored here.
|
|
722
|
+
*/
|
|
723
|
+
applyLimits(accountId, readouts) {
|
|
724
|
+
this.find(accountId);
|
|
725
|
+
const expired = (limit) => {
|
|
726
|
+
const time = limit.resetsAt ? Date.parse(limit.resetsAt) : Number.NaN;
|
|
727
|
+
return !Number.isNaN(time) && time <= Date.now();
|
|
728
|
+
};
|
|
729
|
+
store.updateState(this.dir, accountId, (state) => {
|
|
730
|
+
for (const readout of readouts) {
|
|
731
|
+
// An endpoint read is the provider's whole answer; a header read only adds what it saw.
|
|
732
|
+
const kept = new Map((readout.source === 'provider' ? [] : state.limits.filter((limit) => limit.provider === readout.provider && !expired(limit)))
|
|
733
|
+
.map((limit) => [limit.id, limit]));
|
|
734
|
+
for (const limit of readout.limits)
|
|
735
|
+
kept.set(limit.id, limit);
|
|
736
|
+
state.limits = [...state.limits.filter((limit) => limit.provider !== readout.provider), ...kept.values()];
|
|
737
|
+
const entry = { provider: readout.provider, source: readout.source, reason: readout.reason, observedAt: readout.observedAt };
|
|
738
|
+
state.providerLimits = [...state.providerLimits.filter((existing) => existing.provider !== readout.provider), entry];
|
|
739
|
+
if (readout.plan)
|
|
740
|
+
state.plan = readout.plan;
|
|
741
|
+
}
|
|
742
|
+
state.limits.sort((left, right) => (left.id < right.id ? -1 : left.id > right.id ? 1 : 0));
|
|
743
|
+
});
|
|
744
|
+
this.changed({ accountId });
|
|
745
|
+
return this.overview();
|
|
746
|
+
}
|
|
693
747
|
applyStatus(event) {
|
|
694
748
|
const payload = event.payload;
|
|
695
749
|
if (typeof payload.accountId !== 'string')
|
|
@@ -3,6 +3,18 @@ export interface SecretStore {
|
|
|
3
3
|
set(key: string, value: string): Promise<void>;
|
|
4
4
|
delete(key: string): Promise<void>;
|
|
5
5
|
}
|
|
6
|
+
export interface CredentialLockOptions {
|
|
7
|
+
/** Acquisition only; an acquired callback is never unlocked while still running. */
|
|
8
|
+
timeoutMs?: number;
|
|
9
|
+
pollMs?: number;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Cross-process Lamport bakery lock. Unique participant directories make dead-pid reclamation
|
|
13
|
+
* safe: a stale cleanup can never unlink a successor's lock. No age-based stealing of live refreshes.
|
|
14
|
+
* The directory contains only hashed keys, pids, random IDs and tickets, never credential contents.
|
|
15
|
+
* All processes sharing a backend must use the same coordination directory on a local filesystem.
|
|
16
|
+
*/
|
|
17
|
+
export declare function withCredentialLock<T>(dir: string, key: string, work: () => Promise<T>, options?: CredentialLockOptions): Promise<T>;
|
|
6
18
|
/** `<account id>/<secret name>`: the key an account's secret is stored under. */
|
|
7
19
|
export declare const secretKey: (accountId: string, name: string) => string;
|
|
8
20
|
/** Secrets held only for the life of the process. For tests and throwaway runs. */
|
|
@@ -1,8 +1,115 @@
|
|
|
1
1
|
// Where credentials live. The harness never writes a secret to its own files: it asks the
|
|
2
2
|
// store the embedding application provides (a Keychain, an encrypted vault), by key.
|
|
3
|
-
import { chmodSync, mkdirSync } from 'node:fs';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
3
|
+
import { chmodSync, closeSync, fsyncSync, lstatSync, mkdirSync, openSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
5
|
+
import { dirname, join } from 'node:path';
|
|
6
|
+
import { performance } from 'node:perf_hooks';
|
|
7
|
+
import { readJsonOr } from "../core/fs.js";
|
|
8
|
+
/** Exclusive temporary files are private before the first byte is written, not just after rename. */
|
|
9
|
+
function privateWrite(path, value) {
|
|
10
|
+
const temporary = join(dirname(path), `.credential-${randomUUID()}.tmp`);
|
|
11
|
+
try {
|
|
12
|
+
const fd = openSync(temporary, 'wx', 0o600);
|
|
13
|
+
try {
|
|
14
|
+
writeFileSync(fd, value);
|
|
15
|
+
fsyncSync(fd);
|
|
16
|
+
}
|
|
17
|
+
finally {
|
|
18
|
+
closeSync(fd);
|
|
19
|
+
}
|
|
20
|
+
renameSync(temporary, path);
|
|
21
|
+
}
|
|
22
|
+
finally {
|
|
23
|
+
rmSync(temporary, { force: true });
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Cross-process Lamport bakery lock. Unique participant directories make dead-pid reclamation
|
|
28
|
+
* safe: a stale cleanup can never unlink a successor's lock. No age-based stealing of live refreshes.
|
|
29
|
+
* The directory contains only hashed keys, pids, random IDs and tickets, never credential contents.
|
|
30
|
+
* All processes sharing a backend must use the same coordination directory on a local filesystem.
|
|
31
|
+
*/
|
|
32
|
+
export async function withCredentialLock(dir, key, work, options = {}) {
|
|
33
|
+
const timeout = options.timeoutMs ?? 30_000;
|
|
34
|
+
const poll = options.pollMs ?? 20;
|
|
35
|
+
if (!Number.isFinite(timeout) || timeout < 0 || timeout > 300_000 || !Number.isFinite(poll) || poll < 1 || poll > 1000) {
|
|
36
|
+
throw new Error('Invalid credential lock wait bounds.');
|
|
37
|
+
}
|
|
38
|
+
const started = performance.now();
|
|
39
|
+
const folder = join(dir, createHash('sha256').update(key).digest('hex'));
|
|
40
|
+
mkdirSync(folder, { recursive: true, mode: 0o700 });
|
|
41
|
+
if (!lstatSync(dir).isDirectory() || !lstatSync(folder).isDirectory())
|
|
42
|
+
throw new Error('Invalid credential lock directory.');
|
|
43
|
+
chmodSync(dir, 0o700);
|
|
44
|
+
chmodSync(folder, 0o700);
|
|
45
|
+
const id = `${process.pid}-${randomUUID()}`;
|
|
46
|
+
const own = join(folder, id);
|
|
47
|
+
mkdirSync(own, { mode: 0o700 });
|
|
48
|
+
const participants = () => {
|
|
49
|
+
const entries = [];
|
|
50
|
+
for (const name of readdirSync(folder)) {
|
|
51
|
+
if (!/^\d+-[0-9a-f-]{36}$/.test(name))
|
|
52
|
+
throw new Error('Invalid credential lock metadata.');
|
|
53
|
+
const pid = Number(name.slice(0, name.indexOf('-')));
|
|
54
|
+
if (!Number.isSafeInteger(pid) || pid <= 0)
|
|
55
|
+
throw new Error('Invalid credential lock metadata.');
|
|
56
|
+
let alive = true;
|
|
57
|
+
try {
|
|
58
|
+
process.kill(pid, 0);
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
if (error.code === 'ESRCH')
|
|
62
|
+
alive = false;
|
|
63
|
+
}
|
|
64
|
+
const participant = join(folder, name);
|
|
65
|
+
if (!alive) {
|
|
66
|
+
rmSync(participant, { recursive: true, force: true });
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
try {
|
|
70
|
+
// The absent ticket is the bakery algorithm's choosing flag.
|
|
71
|
+
const text = readFileSync(join(participant, 'ticket'), 'utf8');
|
|
72
|
+
const ticket = Number(text);
|
|
73
|
+
if (!Number.isSafeInteger(ticket) || ticket <= 0)
|
|
74
|
+
throw new Error('Invalid credential lock metadata.');
|
|
75
|
+
entries.push({ id: name, ticket });
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
if (error.code !== 'ENOENT')
|
|
79
|
+
throw error;
|
|
80
|
+
if (lstatExists(participant))
|
|
81
|
+
entries.push({ id: name, ticket: null });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return entries;
|
|
85
|
+
};
|
|
86
|
+
try {
|
|
87
|
+
const ticket = Math.max(0, ...participants().map(entry => entry.ticket ?? 0)) + 1;
|
|
88
|
+
privateWrite(join(own, 'ticket'), String(ticket));
|
|
89
|
+
for (;;) {
|
|
90
|
+
const blocked = participants().some(entry => entry.id !== id && (entry.ticket === null || entry.ticket < ticket || (entry.ticket === ticket && entry.id < id)));
|
|
91
|
+
if (!blocked)
|
|
92
|
+
return await work();
|
|
93
|
+
if (performance.now() - started >= timeout)
|
|
94
|
+
throw new Error('Timed out waiting for credential modification; no credentials were changed.');
|
|
95
|
+
await new Promise(resolve => setTimeout(resolve, Math.min(poll, Math.max(1, timeout - (performance.now() - started)))));
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
finally {
|
|
99
|
+
rmSync(own, { recursive: true, force: true });
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function lstatExists(path) {
|
|
103
|
+
try {
|
|
104
|
+
lstatSync(path);
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
if (error.code === 'ENOENT')
|
|
109
|
+
return false;
|
|
110
|
+
throw error;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
6
113
|
/** `<account id>/<secret name>`: the key an account's secret is stored under. */
|
|
7
114
|
export const secretKey = (accountId, name) => `${accountId}/${name}`;
|
|
8
115
|
/** Secrets held only for the life of the process. For tests and throwaway runs. */
|
|
@@ -23,18 +130,18 @@ export function fileSecretStore(path) {
|
|
|
23
130
|
const read = () => readJsonOr(path, {});
|
|
24
131
|
const write = (values) => {
|
|
25
132
|
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
26
|
-
|
|
133
|
+
privateWrite(path, `${JSON.stringify(values, null, 2)}\n`);
|
|
27
134
|
chmodSync(path, 0o600);
|
|
28
135
|
};
|
|
29
136
|
return {
|
|
30
137
|
get: async (key) => read()[key] ?? null,
|
|
31
|
-
set:
|
|
32
|
-
delete: async (
|
|
138
|
+
set: (key, value) => withCredentialLock(`${path}.locks`, 'file', async () => write({ ...read(), [key]: value })),
|
|
139
|
+
delete: (key) => withCredentialLock(`${path}.locks`, 'file', async () => {
|
|
33
140
|
const values = read();
|
|
34
141
|
if (key in values) {
|
|
35
142
|
delete values[key];
|
|
36
143
|
write(values);
|
|
37
144
|
}
|
|
38
|
-
}
|
|
145
|
+
})
|
|
39
146
|
};
|
|
40
147
|
}
|
|
@@ -25,10 +25,33 @@ export interface Limit {
|
|
|
25
25
|
id: string;
|
|
26
26
|
label: string;
|
|
27
27
|
usedPercent?: number | null;
|
|
28
|
+
/** The same window from the other side: quota left. Additive for surfaces that read limits as remaining. */
|
|
29
|
+
remainingPercent?: number | null;
|
|
28
30
|
resetsAt?: string | null;
|
|
29
31
|
windowMinutes?: number | null;
|
|
30
|
-
/** `allowed`, `warning`, or `
|
|
32
|
+
/** `allowed`, `warning`, `rejected`, or `configured` when the provider reports it. */
|
|
31
33
|
status?: string | null;
|
|
34
|
+
/** Which provider the reading belongs to. Older records may omit it. */
|
|
35
|
+
provider?: string | null;
|
|
36
|
+
/** What kind of ceiling this is. */
|
|
37
|
+
kind?: 'window' | 'rate' | 'credit' | 'configured' | 'error';
|
|
38
|
+
/** The unit of `used`, `limit` and `remaining`. */
|
|
39
|
+
unit?: 'percent' | 'requests' | 'tokens' | 'usd' | 'currency' | 'count';
|
|
40
|
+
used?: number | null;
|
|
41
|
+
limit?: number | null;
|
|
42
|
+
remaining?: number | null;
|
|
43
|
+
/** How the reading was obtained. */
|
|
44
|
+
source?: 'headers' | 'provider' | 'estimate' | 'error';
|
|
45
|
+
observedAt?: string | null;
|
|
46
|
+
/** A short provider note, such as a balance currency. */
|
|
47
|
+
detail?: string | null;
|
|
48
|
+
}
|
|
49
|
+
/** The last readout for one provider of one account, including when nothing could be read. */
|
|
50
|
+
export interface ProviderLimitState {
|
|
51
|
+
provider: string;
|
|
52
|
+
source: 'headers' | 'provider' | 'estimate' | 'unavailable';
|
|
53
|
+
reason: string | null;
|
|
54
|
+
observedAt: string;
|
|
32
55
|
}
|
|
33
56
|
export interface Issue {
|
|
34
57
|
/** `auth_expired`, `rate_limited`, `quota`, `network`, or `unknown`. */
|
|
@@ -45,6 +68,8 @@ export interface AccountState {
|
|
|
45
68
|
plan: string | null;
|
|
46
69
|
identity: string | null;
|
|
47
70
|
limits: Limit[];
|
|
71
|
+
/** What the last limit readout found for each provider, including an unavailable one. */
|
|
72
|
+
providerLimits: ProviderLimitState[];
|
|
48
73
|
lastError: Issue | null;
|
|
49
74
|
lastCheckedAt: string | null;
|
|
50
75
|
cliVersion: string | null;
|
|
@@ -18,7 +18,7 @@ import { appendLine, newId, now, readJsonOr, safeId, writeJson } from "../core/f
|
|
|
18
18
|
import { charCount, compareText, lines, takeChars } from "../core/text.js";
|
|
19
19
|
/** The only provider the harness runs. Records of other providers are kept in the file and ignored. */
|
|
20
20
|
export const PROVIDER = 'pi';
|
|
21
|
-
export const emptyState = () => ({ health: 'unknown', signedIn: null, authMethod: null, plan: null, identity: null, limits: [], lastError: null, lastCheckedAt: null, cliVersion: null, capabilities: [], piProviders: [] });
|
|
21
|
+
export const emptyState = () => ({ health: 'unknown', signedIn: null, authMethod: null, plan: null, identity: null, limits: [], providerLimits: [], lastError: null, lastCheckedAt: null, cliVersion: null, capabilities: [], piProviders: [] });
|
|
22
22
|
// ---- Files ------------------------------------------------------------------------------
|
|
23
23
|
const registryPath = (dir) => join(dir, 'accounts.json');
|
|
24
24
|
const statePath = (dir, accountId) => join(dir, safeId(accountId), 'state.json');
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { PLAN_RESEARCH_PLUGINS } from "./planResearch.js";
|
|
1
2
|
const RECALL = ['history_search', 'history_read', 'trajectory_search', 'trajectory_read', 'wiki_search', 'wiki_read', 'wiki_list', 'skill_load'];
|
|
2
3
|
const PROGRESS = ['checklist_write', 'session_name'];
|
|
3
4
|
const WEB = ['web_search', 'web_fetch'];
|
|
@@ -29,6 +30,19 @@ export const BUILTIN_PROFILES = [
|
|
|
29
30
|
'- Return conclusions, not file dumps: what you found, where, and how sure you are.'
|
|
30
31
|
].join('\n'),
|
|
31
32
|
tools: ['read', 'grep', 'find', 'ls', ...PROGRESS, ...RECALL, ...WEB],
|
|
32
|
-
plugins:
|
|
33
|
+
plugins: [...PLAN_RESEARCH_PLUGINS], variant: null, model: null, effort: null
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
name: 'researcher',
|
|
37
|
+
description: 'Researches code, history and public sources without changing anything.',
|
|
38
|
+
color: 'purple',
|
|
39
|
+
instructions: [
|
|
40
|
+
'You investigate the bounded research question in your brief and return concise, evidence-backed findings to the parent.',
|
|
41
|
+
'- Use read-only repository and public-web sources. Never modify files, run commands, call connected apps, or ask the user.',
|
|
42
|
+
'- Prefer primary sources. Cite repository claims as path:line and external claims with source URLs.',
|
|
43
|
+
'- Separate observed facts from inference, state uncertainty, and return only findings relevant to the brief; do not implement.'
|
|
44
|
+
].join('\n'),
|
|
45
|
+
tools: ['read', 'grep', 'find', 'ls', ...PROGRESS, ...RECALL, ...WEB],
|
|
46
|
+
plugins: [...PLAN_RESEARCH_PLUGINS], variant: null, model: null, effort: null
|
|
33
47
|
}
|
|
34
48
|
];
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { AgentProfile } from "./profiles.js";
|
|
2
|
+
export declare const PLAN_RESEARCH_TOOLS: ReadonlySet<string>;
|
|
3
|
+
export declare const PLAN_RESEARCH_PLUGINS: readonly string[];
|
|
4
|
+
export declare const PLAN_RESEARCH_EVIDENCE_TOOLS: ReadonlySet<string>;
|
|
5
|
+
export declare function planResearchProfile(profile: Pick<AgentProfile, 'tools' | 'plugins'> | null | undefined): boolean;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export const PLAN_RESEARCH_TOOLS = new Set([
|
|
2
|
+
'read', 'grep', 'find', 'ls', 'checklist_write', 'session_name',
|
|
3
|
+
'history_search', 'history_read', 'trajectory_search', 'trajectory_read',
|
|
4
|
+
'wiki_entrypoint', 'wiki_search', 'wiki_read', 'wiki_list', 'memory_facts', 'skill_load',
|
|
5
|
+
'web_search', 'web_fetch'
|
|
6
|
+
]);
|
|
7
|
+
export const PLAN_RESEARCH_PLUGINS = ['history', 'variant', 'web', 'wiki'];
|
|
8
|
+
export const PLAN_RESEARCH_EVIDENCE_TOOLS = new Set([
|
|
9
|
+
'read', 'grep', 'find', 'ls', 'history_search', 'trajectory_search', 'wiki_search', 'web_search', 'web_fetch'
|
|
10
|
+
]);
|
|
11
|
+
export function planResearchProfile(profile) {
|
|
12
|
+
return Boolean(profile
|
|
13
|
+
&& Array.isArray(profile.tools)
|
|
14
|
+
&& profile.tools.length > 0
|
|
15
|
+
&& profile.tools.some(name => PLAN_RESEARCH_EVIDENCE_TOOLS.has(name))
|
|
16
|
+
&& profile.tools.every(name => PLAN_RESEARCH_TOOLS.has(name))
|
|
17
|
+
&& Array.isArray(profile.plugins)
|
|
18
|
+
&& profile.plugins.every(name => PLAN_RESEARCH_PLUGINS.includes(name)));
|
|
19
|
+
}
|
|
@@ -71,10 +71,11 @@ export declare function removeProfile(root: string, cwd: string | null | undefin
|
|
|
71
71
|
/** Where the profile list sits among the prompt sections: the slot a variant's subagents used. */
|
|
72
72
|
export declare const PROFILE_SECTION_ORDER = 64;
|
|
73
73
|
/** The prompt section that lists the profiles a run can start subagents from. */
|
|
74
|
-
export declare function profileSection(root: string, cwd: string | null | undefined, variantDir?: string | null): {
|
|
74
|
+
export declare function profileSection(root: string, cwd: string | null | undefined, variantDir?: string | null, planResearchOnly?: boolean): {
|
|
75
75
|
id: string;
|
|
76
76
|
title: string;
|
|
77
77
|
body: string;
|
|
78
78
|
budget: number;
|
|
79
79
|
order: number;
|
|
80
|
+
dynamic: true;
|
|
80
81
|
};
|
|
@@ -10,6 +10,7 @@ import { parse, serialize } from "../core/frontmatter.js";
|
|
|
10
10
|
import { atomicWrite } from "../core/fs.js";
|
|
11
11
|
import { subagentOf } from "../variant/materialise.js";
|
|
12
12
|
import { BUILTIN_PROFILES } from "./builtin.js";
|
|
13
|
+
import { planResearchProfile } from "./planResearch.js";
|
|
13
14
|
export const PROFILE_NAME = /^[a-z0-9][a-z0-9_-]*$/;
|
|
14
15
|
export const PROFILE_COLORS = ['blue', 'green', 'yellow', 'red', 'magenta', 'cyan', 'orange', 'purple'];
|
|
15
16
|
/** Nearest folder above `cwd` that holds `.git`, or `cwd` itself. */
|
|
@@ -206,16 +207,20 @@ export function removeProfile(root, cwd, input) {
|
|
|
206
207
|
/** Where the profile list sits among the prompt sections: the slot a variant's subagents used. */
|
|
207
208
|
export const PROFILE_SECTION_ORDER = 64;
|
|
208
209
|
/** The prompt section that lists the profiles a run can start subagents from. */
|
|
209
|
-
export function profileSection(root, cwd, variantDir) {
|
|
210
|
+
export function profileSection(root, cwd, variantDir, planResearchOnly = false) {
|
|
210
211
|
const { profiles } = listProfiles(root, cwd, variantDir);
|
|
212
|
+
const listed = planResearchOnly ? profiles.filter(planResearchProfile) : profiles;
|
|
211
213
|
return {
|
|
212
214
|
id: 'subagents',
|
|
213
215
|
title: 'Subagents',
|
|
214
216
|
body: [
|
|
215
|
-
|
|
216
|
-
|
|
217
|
+
planResearchOnly
|
|
218
|
+
? 'Plan mode may start only a named profile listed here, for read-only research. Do not omit agent or use a profile not listed.'
|
|
219
|
+
: 'Delegate when it meaningfully reduces work: choose a substantial, bounded task that can proceed independently, such as unfamiliar cross-file research likely to take several searches or a self-contained implementation. Do a known-file lookup, one or two simple tool calls, a tiny edit or tightly coupled work yourself. Prefer a named profile: explorer for repository research, researcher for public-source research, and worker for a defined change it can own end to end; omit agent only when no profile fits. Each child starts without this conversation, so give it the goal, relevant context or paths, non-goals, expected result and acceptance or verification; use its own checklist for multi-step work. Prefer context-heavy research with concise evidence-backed findings. Parallelize only independent tasks and never assign overlapping edits. Return without waiting only when useful independent work remains; otherwise wait. You own integration, verification and the user-facing report. A profile can only narrow your tools.',
|
|
220
|
+
...(listed.length ? listed.map((profile) => `- ${profile.name} — ${profile.description}`) : ['- No eligible read-only research profiles are configured.'])
|
|
217
221
|
].join('\n'),
|
|
218
222
|
budget: 6_000,
|
|
219
|
-
order: PROFILE_SECTION_ORDER
|
|
223
|
+
order: PROFILE_SECTION_ORDER,
|
|
224
|
+
dynamic: true
|
|
220
225
|
};
|
|
221
226
|
}
|