@rosetears/aili-pi 0.1.9 → 0.1.10

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.
Files changed (57) hide show
  1. package/README.md +6 -6
  2. package/THIRD_PARTY_NOTICES.md +12 -12
  3. package/docs/persistent-agents.md +114 -0
  4. package/manifests/adapter-evidence.json +53 -20
  5. package/manifests/capabilities.json +3 -3
  6. package/manifests/live-verification.json +18 -19
  7. package/manifests/provenance.json +12 -12
  8. package/manifests/roles.json +270 -57
  9. package/manifests/sbom.json +1 -128
  10. package/manifests/skill-compatibility.json +57 -61
  11. package/manifests/subagent-provenance.json +8 -15
  12. package/package.json +3 -3
  13. package/roles/agent-evaluator.md +8 -3
  14. package/roles/ai-regression-scout.md +8 -3
  15. package/roles/browser-qa-runner.md +8 -3
  16. package/roles/code-reviewer.md +8 -3
  17. package/roles/code-scout.md +8 -3
  18. package/roles/convergence-reviewer.md +8 -3
  19. package/roles/doc-researcher.md +8 -3
  20. package/roles/e2e-artifact-runner.md +8 -3
  21. package/roles/general.md +49 -0
  22. package/roles/implementer.md +8 -3
  23. package/roles/opensource-sanitizer.md +8 -3
  24. package/roles/plan-auditor.md +8 -3
  25. package/roles/pr-test-analyzer.md +8 -3
  26. package/roles/security-auditor.md +8 -3
  27. package/roles/silent-failure-reviewer.md +8 -3
  28. package/roles/spec-miner.md +8 -3
  29. package/roles/test-coverage-reviewer.md +8 -3
  30. package/roles/test-engineer.md +8 -3
  31. package/roles/web-performance-auditor.md +8 -3
  32. package/roles/web-researcher.md +8 -3
  33. package/scripts/sync-roles.ts +186 -24
  34. package/src/runtime/doctor.ts +38 -10
  35. package/src/runtime/global-resources.ts +5 -1
  36. package/src/runtime/index.ts +2 -2
  37. package/src/runtime/persistent-agents/hub.ts +429 -0
  38. package/src/runtime/persistent-agents/model-selection.ts +363 -0
  39. package/src/runtime/persistent-agents/output-delivery.ts +356 -0
  40. package/src/runtime/persistent-agents/permission.ts +223 -0
  41. package/src/runtime/persistent-agents/policy.ts +311 -0
  42. package/src/runtime/persistent-agents/production.ts +569 -0
  43. package/src/runtime/persistent-agents/runtime.ts +164 -0
  44. package/src/runtime/persistent-agents/sandbox.ts +68 -0
  45. package/src/runtime/persistent-agents/scheduler.ts +190 -0
  46. package/src/runtime/persistent-agents/session-factory.ts +184 -0
  47. package/src/runtime/persistent-agents/storage.ts +775 -0
  48. package/src/runtime/persistent-agents/task-coordinator.ts +460 -0
  49. package/src/runtime/persistent-agents/task-schema.ts +141 -0
  50. package/src/runtime/persistent-agents/types.ts +134 -0
  51. package/src/runtime/persistent-agents/workspace.ts +335 -0
  52. package/src/runtime/registry.ts +28 -32
  53. package/src/runtime/roles.ts +211 -18
  54. package/src/runtime/rose-context.ts +1 -1
  55. package/templates/APPEND_SYSTEM.md +1 -1
  56. package/upstream/opencode-global-agents.lock.json +1 -1
  57. package/src/runtime/subagents.ts +0 -249
@@ -2,6 +2,11 @@
2
2
  name: test-coverage-reviewer
3
3
  description: Read-only QA reviewer for test coverage adequacy. Use when a diff, package, or release needs coverage-gap review, untested-path identification, or verification sufficiency analysis without writing tests.
4
4
  tools: read,grep,find,ls
5
+ spawns: []
6
+ blocking: false
7
+ aili-profile-version: 2
8
+ aili-runtime-adapter-version: 2
9
+ aili-source-kind: canonical-adapter
5
10
  aili-source-revision: 7eb35f357ad489f5841ee10dac1e44549c1bdb76
6
11
  ---
7
12
 
@@ -9,7 +14,7 @@ aili-source-revision: 7eb35f357ad489f5841ee10dac1e44549c1bdb76
9
14
 
10
15
  ## Role
11
16
 
12
- You are a bounded, single-use Pi child role. Complete the supplied assignment once, return one terminal result or failure, and never resume this context. Your result is evidence for ROSE or the user, not final authority.
17
+ You are a bounded persistent Pi Agent role. Work only on the supplied assignment or follow-up turn within the same stable Agent identity. Your result is evidence for ROSE or the user, not final authority.
13
18
 
14
19
  ## Goal
15
20
 
@@ -41,7 +46,7 @@ Stop when permission is missing, the requested scope conflicts with repository r
41
46
 
42
47
  ## Pi adapter contract
43
48
 
44
- You run once in a fresh `pi --mode json --no-session` child. Recursive AILI task dispatch is unavailable.
45
- Use only the tools exposed by the Pi process. The task packet and parent policy may narrow this profile and never broaden it.
49
+ You run in a parent-scoped persistent official Pi Agent session. Each turn has one supplied assignment or follow-up; an idle session may park and later revive with its retained transcript.
50
+ Child Agent spawning is disabled for this specialized profile. Use only the effective tools exposed by the parent/role/capability/policy intersection; a task packet may narrow and never broaden them.
46
51
  Return exactly one JSON object with keys `status`, `summary`, `evidence`, `changedFiles`, `verification`, `blockers`, `risks`, and `confidence`.
47
52
  Do not include credentials, raw environment variables, authentication-store content, or unbounded command output.
@@ -2,6 +2,11 @@
2
2
  name: test-engineer
3
3
  description: QA engineer specialized in test strategy, test writing, test execution, CLI/browser verification, verification logs, and coverage analysis. Use for designing test suites, writing tests for existing code, executing test plans, or evaluating test quality.
4
4
  tools: read,write,edit
5
+ spawns: []
6
+ blocking: false
7
+ aili-profile-version: 2
8
+ aili-runtime-adapter-version: 2
9
+ aili-source-kind: canonical-adapter
5
10
  aili-source-revision: 7eb35f357ad489f5841ee10dac1e44549c1bdb76
6
11
  ---
7
12
 
@@ -9,7 +14,7 @@ aili-source-revision: 7eb35f357ad489f5841ee10dac1e44549c1bdb76
9
14
 
10
15
  ## Role
11
16
 
12
- You are a bounded, single-use Pi child role. Complete the supplied assignment once, return one terminal result or failure, and never resume this context. Your result is evidence for ROSE or the user, not final authority.
17
+ You are a bounded persistent Pi Agent role. Work only on the supplied assignment or follow-up turn within the same stable Agent identity. Your result is evidence for ROSE or the user, not final authority.
13
18
 
14
19
  ## Goal
15
20
 
@@ -42,7 +47,7 @@ Stop when permission is missing, the requested scope conflicts with repository r
42
47
 
43
48
  ## Pi adapter contract
44
49
 
45
- You run once in a fresh `pi --mode json --no-session` child. Recursive AILI task dispatch is unavailable.
46
- Use only the tools exposed by the Pi process. The task packet and parent policy may narrow this profile and never broaden it.
50
+ You run in a parent-scoped persistent official Pi Agent session. Each turn has one supplied assignment or follow-up; an idle session may park and later revive with its retained transcript.
51
+ Child Agent spawning is disabled for this specialized profile. Use only the effective tools exposed by the parent/role/capability/policy intersection; a task packet may narrow and never broaden them.
47
52
  Return exactly one JSON object with keys `status`, `summary`, `evidence`, `changedFiles`, `verification`, `blockers`, `risks`, and `confidence`.
48
53
  Do not include credentials, raw environment variables, authentication-store content, or unbounded command output.
@@ -2,6 +2,11 @@
2
2
  name: web-performance-auditor
3
3
  description: Web performance engineer focused on Core Web Vitals, loading, rendering, and network optimization. Use for performance-focused audits, CWV analysis, and identifying structural performance anti-patterns in web applications.
4
4
  tools: read,grep,find,ls
5
+ spawns: []
6
+ blocking: false
7
+ aili-profile-version: 2
8
+ aili-runtime-adapter-version: 2
9
+ aili-source-kind: canonical-adapter
5
10
  aili-source-revision: 7eb35f357ad489f5841ee10dac1e44549c1bdb76
6
11
  ---
7
12
 
@@ -9,7 +14,7 @@ aili-source-revision: 7eb35f357ad489f5841ee10dac1e44549c1bdb76
9
14
 
10
15
  ## Role
11
16
 
12
- You are a bounded, single-use Pi child role. Complete the supplied assignment once, return one terminal result or failure, and never resume this context. Your result is evidence for ROSE or the user, not final authority.
17
+ You are a bounded persistent Pi Agent role. Work only on the supplied assignment or follow-up turn within the same stable Agent identity. Your result is evidence for ROSE or the user, not final authority.
13
18
 
14
19
  ## Goal
15
20
 
@@ -41,7 +46,7 @@ Stop when permission is missing, the requested scope conflicts with repository r
41
46
 
42
47
  ## Pi adapter contract
43
48
 
44
- You run once in a fresh `pi --mode json --no-session` child. Recursive AILI task dispatch is unavailable.
45
- Use only the tools exposed by the Pi process. The task packet and parent policy may narrow this profile and never broaden it.
49
+ You run in a parent-scoped persistent official Pi Agent session. Each turn has one supplied assignment or follow-up; an idle session may park and later revive with its retained transcript.
50
+ Child Agent spawning is disabled for this specialized profile. Use only the effective tools exposed by the parent/role/capability/policy intersection; a task packet may narrow and never broaden them.
46
51
  Return exactly one JSON object with keys `status`, `summary`, `evidence`, `changedFiles`, `verification`, `blockers`, `risks`, and `confidence`.
47
52
  Do not include credentials, raw environment variables, authentication-store content, or unbounded command output.
@@ -2,6 +2,11 @@
2
2
  name: web-researcher
3
3
  description: Read-only public web research subagent. Uses web search/fetch for current official documentation, public repositories, releases, package metadata, compatibility, and deprecation evidence; never reads local files, edits, runs commands, or delegates.
4
4
  tools: []
5
+ spawns: []
6
+ blocking: false
7
+ aili-profile-version: 2
8
+ aili-runtime-adapter-version: 2
9
+ aili-source-kind: canonical-adapter
5
10
  aili-source-revision: 7eb35f357ad489f5841ee10dac1e44549c1bdb76
6
11
  ---
7
12
 
@@ -9,7 +14,7 @@ aili-source-revision: 7eb35f357ad489f5841ee10dac1e44549c1bdb76
9
14
 
10
15
  ## Role
11
16
 
12
- You are a bounded, single-use Pi child role. Complete the supplied assignment once, return one terminal result or failure, and never resume this context. Your result is evidence for ROSE or the user, not final authority.
17
+ You are a bounded persistent Pi Agent role. Work only on the supplied assignment or follow-up turn within the same stable Agent identity. Your result is evidence for ROSE or the user, not final authority.
13
18
 
14
19
  ## Goal
15
20
 
@@ -41,7 +46,7 @@ Stop when permission is missing, the requested scope conflicts with repository r
41
46
 
42
47
  ## Pi adapter contract
43
48
 
44
- You run once in a fresh `pi --mode json --no-session` child. Recursive AILI task dispatch is unavailable.
45
- Use only the tools exposed by the Pi process. The task packet and parent policy may narrow this profile and never broaden it.
49
+ You run in a parent-scoped persistent official Pi Agent session. Each turn has one supplied assignment or follow-up; an idle session may park and later revive with its retained transcript.
50
+ Child Agent spawning is disabled for this specialized profile. Use only the effective tools exposed by the parent/role/capability/policy intersection; a task packet may narrow and never broaden them.
46
51
  Return exactly one JSON object with keys `status`, `summary`, `evidence`, `changedFiles`, `verification`, `blockers`, `risks`, and `confidence`.
47
52
  Do not include credentials, raw environment variables, authentication-store content, or unbounded command output.
@@ -1,23 +1,28 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { execFile as execFileCallback } from "node:child_process";
3
- import { promisify } from "node:util";
4
3
  import { lstat, readdir, readFile, writeFile } from "node:fs/promises";
5
4
  import { basename, join, resolve } from "node:path";
5
+ import { promisify } from "node:util";
6
6
 
7
7
  const ROOT = resolve(import.meta.dirname, "..");
8
8
  const ROLES_DIR = join(ROOT, "roles");
9
9
  const MANIFEST_PATH = join(ROOT, "manifests", "roles.json");
10
10
  const SOURCE_COMMIT = "7eb35f357ad489f5841ee10dac1e44549c1bdb76";
11
11
  const SOURCE_REPOSITORY = "https://github.com/Rosetears520/aili-workflows.git";
12
+ const PROFILE_VERSION = 2;
13
+ const RUNTIME_ADAPTER_VERSION = 2;
12
14
  const execFile = promisify(execFileCallback);
13
15
 
14
- const ROLE_NAMES = [
16
+ const SPECIALIZED_ROLE_NAMES = [
15
17
  "agent-evaluator", "ai-regression-scout", "browser-qa-runner", "code-reviewer",
16
18
  "code-scout", "convergence-reviewer", "doc-researcher", "e2e-artifact-runner",
17
19
  "implementer", "opensource-sanitizer", "plan-auditor", "pr-test-analyzer",
18
20
  "security-auditor", "silent-failure-reviewer", "spec-miner", "test-coverage-reviewer",
19
21
  "test-engineer", "web-performance-auditor", "web-researcher",
20
22
  ] as const;
23
+ const SPECIALIZED_SELECTORS = SPECIALIZED_ROLE_NAMES.map((name) => `aili.${name}`);
24
+ const BUNDLED_NAMES = [...SPECIALIZED_ROLE_NAMES, "general"] as const;
25
+ const BUNDLED_SELECTORS = [...SPECIALIZED_SELECTORS, "general"];
21
26
 
22
27
  const OPTIONAL: Record<string, string[]> = {
23
28
  "browser-qa-runner": ["browser.qa"],
@@ -40,13 +45,21 @@ const KNOWN_SOURCE_PERMISSION_KEYS = new Set([
40
45
 
41
46
  interface RoleRecord {
42
47
  name: string;
48
+ selector: string;
43
49
  description: string;
44
50
  profilePath: string;
45
51
  profileHash: string;
46
- sourcePath: string;
52
+ profileVersion: 2;
53
+ runtimeAdapterVersion: 2;
54
+ sourceKind: "canonical-adapter" | "aili-owned";
55
+ sourcePath: string | null;
47
56
  sourceHash: string;
48
57
  tools: string[];
58
+ toolPolicy: "static" | "inherit-parent";
49
59
  capabilities: string[];
60
+ spawns: string[];
61
+ blocking: boolean;
62
+ model?: string;
50
63
  status: "adapted" | "optional" | "blocked";
51
64
  compatibilityReason: string;
52
65
  sourceFrontmatterDisposition: Record<string, string>;
@@ -96,27 +109,109 @@ function translatedPolicy(source: string): { tools: string[]; blocked: boolean;
96
109
  };
97
110
  }
98
111
 
99
- function profileContent(name: string, description: string, tools: string[], body: string): string {
112
+ function profileFrontmatter(args: {
113
+ name: string;
114
+ description: string;
115
+ tools: string[];
116
+ spawns: string[];
117
+ sourceRevision: string;
118
+ sourceKind: RoleRecord["sourceKind"];
119
+ }): string[] {
120
+ return [
121
+ "---",
122
+ `name: ${args.name}`,
123
+ `description: ${args.description}`,
124
+ `tools: ${args.tools.join(",") || "[]"}`,
125
+ `spawns: ${args.spawns.join(",") || "[]"}`,
126
+ "blocking: false",
127
+ `aili-profile-version: ${PROFILE_VERSION}`,
128
+ `aili-runtime-adapter-version: ${RUNTIME_ADAPTER_VERSION}`,
129
+ `aili-source-kind: ${args.sourceKind}`,
130
+ `aili-source-revision: ${args.sourceRevision}`,
131
+ "---",
132
+ ];
133
+ }
134
+
135
+ function specializedProfileContent(name: string, description: string, tools: string[], body: string): string {
100
136
  const adaptedBody = body
101
137
  .replaceAll("OpenCode subagent", "Pi child role")
102
138
  .replaceAll("OpenCode", "Pi")
103
139
  .replaceAll("Task tool", "AILI task capability")
140
+ .replace(
141
+ "You are a bounded, single-use Pi child role. Complete the supplied assignment once, return one terminal result or failure, and never resume this context.",
142
+ "You are a bounded persistent Pi Agent role. Work only on the supplied assignment or follow-up turn within the same stable Agent identity.",
143
+ )
104
144
  .replace(/## Output\n[\s\S]*?(?=\n## Stop)/, "## Output\n\nReturn exactly one JSON object with keys `status`, `summary`, `evidence`, `changedFiles`, `verification`, `blockers`, `risks`, and `confidence`.");
105
145
  return [
106
- "---",
107
- `name: ${name}`,
108
- `description: ${description}`,
109
- `tools: ${tools.join(",") || "[]"}`,
110
- `aili-source-revision: ${SOURCE_COMMIT}`,
111
- "---",
146
+ ...profileFrontmatter({
147
+ name,
148
+ description,
149
+ tools,
150
+ spawns: [],
151
+ sourceRevision: SOURCE_COMMIT,
152
+ sourceKind: "canonical-adapter",
153
+ }),
112
154
  "",
113
155
  adaptedBody,
114
156
  "",
115
157
  "## Pi adapter contract",
116
158
  "",
117
- "You run once in a fresh `pi --mode json --no-session` child. Recursive AILI task dispatch is unavailable.",
118
- "Use only the tools exposed by the Pi process. The task packet and parent policy may narrow this profile and never broaden it.",
159
+ "You run in a parent-scoped persistent official Pi Agent session. Each turn has one supplied assignment or follow-up; an idle session may park and later revive with its retained transcript.",
160
+ "Child Agent spawning is disabled for this specialized profile. Use only the effective tools exposed by the parent/role/capability/policy intersection; a task packet may narrow and never broaden them.",
161
+ "Return exactly one JSON object with keys `status`, `summary`, `evidence`, `changedFiles`, `verification`, `blockers`, `risks`, and `confidence`.",
162
+ "Do not include credentials, raw environment variables, authentication-store content, or unbounded command output.",
163
+ "",
164
+ ].join("\n");
165
+ }
166
+
167
+ function generalProfileContent(): string {
168
+ const description = "General persistent Agent for focused delegated work using the parent's current active tool ceiling and explicit spawn policy.";
169
+ return [
170
+ ...profileFrontmatter({
171
+ name: "general",
172
+ description,
173
+ tools: [],
174
+ spawns: SPECIALIZED_SELECTORS,
175
+ sourceRevision: "aili-owned-general-v1",
176
+ sourceKind: "aili-owned",
177
+ }),
178
+ "",
179
+ "# General Agent",
180
+ "",
181
+ "## Role",
182
+ "",
183
+ "You are AILI's general persistent worker Agent. Complete the current delegated assignment and retain relevant context for later follow-up turns under the same stable Agent identity.",
184
+ "",
185
+ "## Goal",
186
+ "",
187
+ "Deliver the requested bounded outcome using only the effective tools inherited from the parent and only the specialized child roles allowed by the runtime spawn policy.",
188
+ "",
189
+ "## Success criteria",
190
+ "",
191
+ "- Stay focused on the supplied task and explicit context.",
192
+ "- Use tools only when they materially advance the task.",
193
+ "- Delegate only to an allowed non-self specialized role when it has clear benefit.",
194
+ "- Return concise, evidence-grounded results and preserve unresolved blockers.",
195
+ "",
196
+ "## Constraints",
197
+ "",
198
+ "- Parent permissions, active tools, project trust, credential guards, recursion depth, and workspace policy always apply and can only narrow authority.",
199
+ "- Do not infer access to the parent's conversation history beyond explicit task/context and trusted resources.",
200
+ "- Do not write persistent configuration, delete history, or perform external/destructive operations without the required user confirmation.",
201
+ "- Never claim a child task, tool call, verification, or delivery succeeded without evidence.",
202
+ "",
203
+ "## Output",
204
+ "",
119
205
  "Return exactly one JSON object with keys `status`, `summary`, `evidence`, `changedFiles`, `verification`, `blockers`, `risks`, and `confidence`.",
206
+ "",
207
+ "## Stop",
208
+ "",
209
+ "Stop when the assignment is complete, a material decision or permission is missing, required evidence is unavailable, or continuing would exceed the accepted task boundary.",
210
+ "",
211
+ "## Pi adapter contract",
212
+ "",
213
+ "You run in a parent-scoped persistent official Pi Agent session. Each turn has one supplied assignment or follow-up; an idle session may park and later revive with its retained transcript.",
214
+ "Use only the effective parent-active/capability/policy tool intersection. The runtime may expose `task` only for allowed non-self specialized selectors and within the accepted depth ceiling.",
120
215
  "Do not include credentials, raw environment variables, authentication-store content, or unbounded command output.",
121
216
  "",
122
217
  ].join("\n");
@@ -132,8 +227,9 @@ async function generate(sourceRoot: string): Promise<void> {
132
227
  if (commit !== SOURCE_COMMIT) throw new Error(`source revision mismatch: ${commit}`);
133
228
  if (origin !== SOURCE_REPOSITORY) throw new Error(`source origin mismatch: ${origin}`);
134
229
  if (status !== "") throw new Error("source worktree must be clean");
230
+
135
231
  const records: RoleRecord[] = [];
136
- for (const name of ROLE_NAMES) {
232
+ for (const name of SPECIALIZED_ROLE_NAMES) {
137
233
  const sourcePath = join(sourceRoot, "agents", `${name}.md`);
138
234
  if ((await lstat(sourcePath)).isSymbolicLink()) throw new Error(`${name}: source role must not be a symlink`);
139
235
  const source = await readFile(sourcePath, "utf8");
@@ -143,59 +239,125 @@ async function generate(sourceRoot: string): Promise<void> {
143
239
  if (name === "web-researcher") tools.length = 0;
144
240
  const capabilities = ["repo.read", ...(tools.some((tool) => tool === "write" || tool === "edit") ? ["repo.write"] : []), ...(OPTIONAL[name] ?? [])];
145
241
  if (name === "web-researcher") capabilities.splice(0, 1);
146
- const profile = profileContent(name, description, tools, body);
242
+ const profile = specializedProfileContent(name, description, tools, body);
147
243
  await writeFile(join(ROLES_DIR, `${name}.md`), profile, "utf8");
148
244
  records.push({
149
245
  name,
246
+ selector: `aili.${name}`,
150
247
  description,
151
248
  profilePath: `roles/${name}.md`,
152
249
  profileHash: hash(profile),
250
+ profileVersion: PROFILE_VERSION,
251
+ runtimeAdapterVersion: RUNTIME_ADAPTER_VERSION,
252
+ sourceKind: "canonical-adapter",
153
253
  sourcePath: `agents/${name}.md`,
154
254
  sourceHash: hash(source),
155
255
  tools,
256
+ toolPolicy: "static",
156
257
  capabilities,
258
+ spawns: [],
259
+ blocking: false,
157
260
  status: OPTIONAL[name] ? "optional" : policy.blocked ? "blocked" : "adapted",
158
261
  compatibilityReason: OPTIONAL[name] ? `Optional capabilities are required: ${OPTIONAL[name].join(", ")}.` : policy.reason,
159
262
  sourceFrontmatterDisposition: {
160
- mode: "replaced by fresh Pi child-process execution",
263
+ mode: "translated into a parent-scoped persistent official Pi Agent session",
161
264
  hidden: "not interpreted by Pi",
162
265
  permission: "translated into the explicit Pi tool/capability ceiling; source YAML is never executed",
163
266
  },
164
267
  });
165
268
  }
269
+
270
+ const general = generalProfileContent();
271
+ await writeFile(join(ROLES_DIR, "general.md"), general, "utf8");
272
+ records.push({
273
+ name: "general",
274
+ selector: "general",
275
+ description: "General persistent Agent for focused delegated work using the parent's current active tool ceiling and explicit spawn policy.",
276
+ profilePath: "roles/general.md",
277
+ profileHash: hash(general),
278
+ profileVersion: PROFILE_VERSION,
279
+ runtimeAdapterVersion: RUNTIME_ADAPTER_VERSION,
280
+ sourceKind: "aili-owned",
281
+ sourcePath: null,
282
+ sourceHash: hash(general),
283
+ tools: [],
284
+ toolPolicy: "inherit-parent",
285
+ capabilities: ["parent.active"],
286
+ spawns: SPECIALIZED_SELECTORS,
287
+ blocking: false,
288
+ status: "adapted",
289
+ compatibilityReason: "AILI-owned OMP-inspired general worker; effective tools inherit and can only narrow the parent active ceiling.",
290
+ sourceFrontmatterDisposition: {
291
+ mode: "AILI-owned persistent Agent profile",
292
+ hidden: "not applicable",
293
+ permission: "runtime parent-active/capability/policy intersection",
294
+ },
295
+ });
296
+
166
297
  await writeFile(MANIFEST_PATH, `${JSON.stringify({
167
- schemaVersion: 1,
298
+ schemaVersion: 2,
299
+ runtimeAdapterVersion: RUNTIME_ADAPTER_VERSION,
168
300
  source: { repository: SOURCE_REPOSITORY, commit: SOURCE_COMMIT },
301
+ bundledSelectors: BUNDLED_SELECTORS,
169
302
  outputContract: ["status", "summary", "evidence", "changedFiles", "verification", "blockers", "risks", "confidence"],
303
+ turnAuditFields: ["selector", "profileHash", "sourceHash", "profileVersion", "runtimeAdapterVersion", "effectiveTools", "provider", "model", "thinking"],
170
304
  records,
171
305
  }, null, 2)}\n`, "utf8");
172
306
  }
173
307
 
174
308
  async function verify(): Promise<void> {
175
- const manifest = JSON.parse(await readFile(MANIFEST_PATH, "utf8")) as { schemaVersion: number; records: RoleRecord[] };
309
+ const manifest = JSON.parse(await readFile(MANIFEST_PATH, "utf8")) as {
310
+ schemaVersion: number;
311
+ runtimeAdapterVersion: number;
312
+ source: { repository: string; commit: string };
313
+ bundledSelectors: string[];
314
+ turnAuditFields: string[];
315
+ records: RoleRecord[];
316
+ };
176
317
  const errors: string[] = [];
177
- if (manifest.schemaVersion !== 1) errors.push("roles manifest schemaVersion must be 1");
178
- const expected = new Set(ROLE_NAMES);
318
+ if (manifest.schemaVersion !== 2) errors.push("roles manifest schemaVersion must be 2");
319
+ if (manifest.runtimeAdapterVersion !== RUNTIME_ADAPTER_VERSION) errors.push("roles runtimeAdapterVersion mismatch");
320
+ if (manifest.source?.repository !== SOURCE_REPOSITORY || manifest.source?.commit !== SOURCE_COMMIT) errors.push("roles source provenance mismatch");
321
+ if (JSON.stringify(manifest.bundledSelectors) !== JSON.stringify(BUNDLED_SELECTORS)) errors.push("bundled selector inventory mismatch");
322
+ if (JSON.stringify(manifest.turnAuditFields) !== JSON.stringify(["selector", "profileHash", "sourceHash", "profileVersion", "runtimeAdapterVersion", "effectiveTools", "provider", "model", "thinking"])) errors.push("turn audit field inventory mismatch");
323
+ const expected = new Set(BUNDLED_NAMES);
179
324
  const seen = new Set<string>();
325
+ const selectors = new Set<string>();
180
326
  for (const role of manifest.records ?? []) {
181
- if (!expected.has(role.name as typeof ROLE_NAMES[number])) errors.push(`${role.name}: unexpected role`);
327
+ if (!expected.has(role.name as typeof BUNDLED_NAMES[number])) errors.push(`${role.name}: unexpected role`);
182
328
  if (seen.has(role.name)) errors.push(`${role.name}: duplicate role`);
329
+ if (selectors.has(role.selector)) errors.push(`${role.selector}: duplicate selector`);
183
330
  seen.add(role.name);
331
+ selectors.add(role.selector);
332
+ if (role.profilePath !== `roles/${role.name}.md`) errors.push(`${role.name}: invalid profile path`);
184
333
  const content = await readFile(join(ROOT, role.profilePath), "utf8").catch(() => "");
185
334
  if (!content) errors.push(`${role.name}: missing profile`);
186
335
  else {
187
336
  if (hash(content) !== role.profileHash) errors.push(`${role.name}: profile hash drift`);
188
- if (content.includes("OpenCode subagent") || content.includes(".agents/skills/") || /^\s*permission:/m.test(content)) errors.push(`${role.name}: unsupported OpenCode semantics remain active`);
337
+ if (content.includes("OpenCode subagent") || content.includes("single-use") || content.includes("--no-session") || content.includes("Recursive AILI task dispatch is unavailable")) errors.push(`${role.name}: obsolete runtime adapter wording remains`);
338
+ if (!content.includes("parent-scoped persistent official Pi Agent session")) errors.push(`${role.name}: persistent adapter contract missing`);
189
339
  if (!content.includes("Return exactly one JSON object")) errors.push(`${role.name}: output contract missing`);
190
340
  }
191
- if (!Array.isArray(role.tools) || !Array.isArray(role.capabilities)) errors.push(`${role.name}: invalid policy arrays`);
341
+ if (role.profileVersion !== PROFILE_VERSION || role.runtimeAdapterVersion !== RUNTIME_ADAPTER_VERSION) errors.push(`${role.name}: profile/runtime version mismatch`);
342
+ if (!Array.isArray(role.tools) || !Array.isArray(role.capabilities) || !Array.isArray(role.spawns)) errors.push(`${role.name}: invalid policy arrays`);
343
+ if (role.blocking !== false) errors.push(`${role.name}: unexpected blocking policy`);
192
344
  if (!role.compatibilityReason) errors.push(`${role.name}: compatibility reason missing`);
193
345
  if (role.status !== "adapted" && role.status !== "optional" && role.status !== "blocked") errors.push(`${role.name}: invalid compatibility status`);
346
+ if (role.name === "general") {
347
+ if (role.selector !== "general" || role.toolPolicy !== "inherit-parent") errors.push("general: selector/tool policy mismatch");
348
+ if (JSON.stringify(role.spawns) !== JSON.stringify(SPECIALIZED_SELECTORS)) errors.push("general: spawn inventory mismatch");
349
+ if (role.sourceKind !== "aili-owned" || role.sourcePath !== null) errors.push("general: source ownership mismatch");
350
+ if (content && role.sourceHash !== hash(content)) errors.push("general: source hash mismatch");
351
+ } else {
352
+ if (role.selector !== `aili.${role.name}` || role.toolPolicy !== "static") errors.push(`${role.name}: selector/tool policy mismatch`);
353
+ if (role.spawns.length !== 0) errors.push(`${role.name}: specialized spawns must be empty`);
354
+ if (role.sourceKind !== "canonical-adapter" || role.sourcePath !== `agents/${role.name}.md`) errors.push(`${role.name}: source ownership mismatch`);
355
+ }
194
356
  }
195
357
  for (const name of expected) if (!seen.has(name)) errors.push(`${name}: missing role`);
196
358
  const files = (await readdir(ROLES_DIR)).filter((name) => name.endsWith(".md")).sort();
197
- const expectedFiles = ROLE_NAMES.map((name) => `${name}.md`).sort();
198
- if (JSON.stringify(files) !== JSON.stringify(expectedFiles)) errors.push(`role file inventory mismatch: ${files.length}/19`);
359
+ const expectedFiles = BUNDLED_NAMES.map((name) => `${name}.md`).sort();
360
+ if (JSON.stringify(files) !== JSON.stringify(expectedFiles)) errors.push(`role file inventory mismatch: ${files.length}/20`);
199
361
  if (errors.length > 0) throw new Error(errors.join("\n"));
200
362
  console.log(`Role profiles verified: ${manifest.records.length}`);
201
363
  }
@@ -1,10 +1,10 @@
1
1
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import { readFile } from "node:fs/promises";
3
3
  import { detectLifecycleConflicts } from "./conflicts.js";
4
- import { loadRegistry, validateProvenance, validateRegistry } from "./registry.js";
4
+ import { loadRegistry, validateLiveVerification, validateProvenance, validateRegistry } from "./registry.js";
5
5
  import { inspectGlobalResources } from "./global-resources.js";
6
6
  import { nativeIntegrationDiagnostics } from "./native-integrations.js";
7
- import { subagentDiagnostics } from "./subagents.js";
7
+ import { validateRoleProfiles } from "./roles.js";
8
8
 
9
9
  export type DoctorStatus = "PASS" | "WARN" | "SKIP" | "ERROR" | "UNVERIFIED";
10
10
 
@@ -24,23 +24,23 @@ const ROOT = new URL("../../", import.meta.url);
24
24
 
25
25
  export async function runDoctor(
26
26
  pi: Pick<ExtensionAPI, "getCommands">,
27
- options: { platform?: NodeJS.Platform } = {},
27
+ options: { platform?: NodeJS.Platform; home?: string } = {},
28
28
  ): Promise<DoctorReport> {
29
29
  const results: DoctorResult[] = [];
30
30
  try {
31
31
  const packageJson = JSON.parse(await readFile(new URL("package.json", ROOT), "utf8")) as {
32
32
  version?: string;
33
33
  engines?: { node?: string };
34
- pi?: { extensions?: string[]; prompts?: string[]; skills?: string[] };
34
+ pi?: { extensions?: string[]; prompts?: string[]; skills?: string[]; themes?: string[] };
35
35
  };
36
36
  const dependencies = (packageJson as { dependencies?: Record<string, string> }).dependencies ?? {};
37
- const expectedDependencies = ["@agwab/pi-subagent@0.4.8", "pi-permission-modes@2.2.0", "pi-quota-status@0.3.0", "pi-web-access@0.13.0"];
37
+ const expectedDependencies = ["pi-permission-modes@2.2.0", "pi-quota-status@0.3.0", "pi-web-access@0.13.0"];
38
38
  const dependencyState = expectedDependencies.every((entry) => {
39
39
  const separator = entry.lastIndexOf("@");
40
40
  return dependencies[entry.slice(0, separator)] === entry.slice(separator + 1);
41
41
  });
42
42
  results.push({ id: "package", status: dependencyState ? "PASS" : "ERROR", evidence: `version=${packageJson.version ?? "unverified"}; node=${packageJson.engines?.node ?? "unverified"}; native_dependencies=${dependencyState ? "exact" : "drift"}` });
43
- const resources = [...(packageJson.pi?.extensions ?? []), ...(packageJson.pi?.prompts ?? []), ...(packageJson.pi?.skills ?? [])];
43
+ const resources = [...(packageJson.pi?.extensions ?? []), ...(packageJson.pi?.prompts ?? []), ...(packageJson.pi?.skills ?? []), ...(packageJson.pi?.themes ?? [])];
44
44
  results.push({ id: "package.resources", status: resources.length === 11 ? "PASS" : "ERROR", evidence: `declared=${resources.length}` });
45
45
  } catch (error) {
46
46
  results.push({ id: "package", status: "ERROR", evidence: boundedError(error) });
@@ -55,7 +55,8 @@ export async function runDoctor(
55
55
  results.push({ id: "skill.snapshot", status: "ERROR", evidence: boundedError(error) });
56
56
  }
57
57
 
58
- const conflicts = detectLifecycleConflicts(pi.getCommands());
58
+ const commands = pi.getCommands();
59
+ const conflicts = detectLifecycleConflicts(commands);
59
60
  results.push({ id: "rose.prompts", status: conflicts.length === 0 ? "PASS" : "ERROR", evidence: conflicts.length === 0 ? "five lifecycle/review prompts have unique ownership" : `conflicts=${conflicts.map((item) => item.name).join(",")}` });
60
61
 
61
62
  try {
@@ -68,11 +69,38 @@ export async function runDoctor(
68
69
  results.push({ id: "capability.registry", status: "ERROR", evidence: boundedError(error) });
69
70
  }
70
71
 
71
- const staticSubagents = await subagentDiagnostics();
72
- results.push({ id: "roles.subagents", ...staticSubagents });
72
+ try {
73
+ const [profileErrors, liveErrors] = await Promise.all([
74
+ validateRoleProfiles(),
75
+ validateLiveVerification(),
76
+ ...[
77
+ "src/runtime/persistent-agents/runtime.ts",
78
+ "src/runtime/persistent-agents/storage.ts",
79
+ "src/runtime/persistent-agents/task-coordinator.ts",
80
+ "src/runtime/persistent-agents/hub.ts",
81
+ "src/runtime/persistent-agents/output-delivery.ts",
82
+ ].map((path) => readFile(new URL(path, ROOT), "utf8")),
83
+ ]);
84
+ results.push({
85
+ id: "roles.agents",
86
+ status: profileErrors.length === 0 ? "PASS" : "ERROR",
87
+ evidence: profileErrors.length === 0 ? "profiles=20; selectors=19 specialized + general" : profileErrors.slice(0, 3).join("; "),
88
+ });
89
+ results.push({
90
+ id: "agent.framework",
91
+ status: profileErrors.length === 0 && liveErrors.length === 0 ? "PASS" : profileErrors.length > 0 ? "ERROR" : "UNVERIFIED",
92
+ evidence: profileErrors.length > 0
93
+ ? `public runtime registered but profile validation failed: ${profileErrors.slice(0, 3).join("; ")}`
94
+ : liveErrors.length > 0
95
+ ? `public tools=task,hub; legacy subagent absent; ${liveErrors.slice(0, 2).join("; ")}`
96
+ : "public tools=task,hub; legacy subagent absent; deterministic and authorized provider/sandbox/external-workspace gates pass",
97
+ });
98
+ } catch (error) {
99
+ results.push({ id: "agent.framework", status: "ERROR", evidence: boundedError(error) });
100
+ }
73
101
  results.push({ id: "permission.native", ...nativeIntegrationDiagnostics(pi.getCommands()) });
74
102
  try {
75
- const global = await inspectGlobalResources();
103
+ const global = await inspectGlobalResources(options.home);
76
104
  const ready = global.appendSystem === "installed" && global.roles.missing.length === 0;
77
105
  results.push({ id: "global.resources", status: ready ? "PASS" : "UNVERIFIED", evidence: `append=${global.appendSystem}; roles=${global.roles.installed}/${global.roles.expected}; stale=${global.roles.stale.length}; path=${global.roleDirectory}` });
78
106
  } catch (error) {
@@ -55,7 +55,11 @@ async function requireRealDirectory(path: string): Promise<void> {
55
55
  }
56
56
 
57
57
  async function expectedRoles(): Promise<RoleProfile[]> {
58
- return await loadRoleProfiles();
58
+ // The 19 specialized profiles retain their existing global `aili.<role>`
59
+ // installation surface. `general` is owned by the new internal task runtime;
60
+ // installing it under agents/aili would create the forbidden `aili.general`
61
+ // alias.
62
+ return (await loadRoleProfiles()).filter((role) => role.selector !== "general");
59
63
  }
60
64
 
61
65
  export async function inspectGlobalResources(home = homedir()): Promise<GlobalResourceReport> {
@@ -2,14 +2,14 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import type { RuntimeComponent } from "./contracts.js";
3
3
  import { registerRoseContext } from "./rose-context.js";
4
4
  import { registerDoctor } from "./doctor.js";
5
- import { registerSubagent } from "./subagents.js";
5
+ import { registerPersistentAgentRuntime } from "./persistent-agents/production.js";
6
6
  import { registerNativeIntegrations } from "./native-integrations.js";
7
7
  import { registerGlobalResourceCommand } from "./global-resources.js";
8
8
 
9
9
  export const runtimeComponents: readonly RuntimeComponent[] = [
10
10
  { id: "rose-context", availability: "available", register: registerRoseContext },
11
11
  { id: "lifecycle-routing", availability: "available" },
12
- { id: "task-runtime", availability: "available", register: registerSubagent },
12
+ { id: "task-runtime", availability: "available", register: registerPersistentAgentRuntime },
13
13
  { id: "native-integrations", availability: "available", register: registerNativeIntegrations },
14
14
  { id: "global-resources", availability: "available", register: registerGlobalResourceCommand },
15
15
  { id: "capability-registry", availability: "available" },