@aiwg/cli 2026.9.0 → 2026.9.2

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 (107) hide show
  1. package/agentic/code/providers/capability-matrix.yaml +41 -0
  2. package/agentic/code/providers/model-capabilities.v1.json +11 -0
  3. package/agentic/code/providers/model-catalog.v1.json +8 -0
  4. package/agentic/code/providers/pi/aiwg-bridge.ts +26 -0
  5. package/bin/aiwg.mjs +15 -3
  6. package/dist/src/api/index.d.ts +3 -0
  7. package/dist/src/api/index.js +3 -0
  8. package/dist/src/auth/credential-store.js +6 -0
  9. package/dist/src/channel/manager.mjs +2 -2
  10. package/dist/src/cli/handlers/dataset.js +186 -0
  11. package/dist/src/cli/handlers/help.js +2 -1
  12. package/dist/src/cli/handlers/index.js +5 -1
  13. package/dist/src/cli/handlers/init.js +1 -0
  14. package/dist/src/cli/handlers/output-mode.js +18 -1
  15. package/dist/src/cli/handlers/run.js +11 -3
  16. package/dist/src/cli/handlers/schema.js +221 -0
  17. package/dist/src/cli/handlers/sessions.js +30 -12
  18. package/dist/src/cli/handlers/steward.js +11 -1
  19. package/dist/src/cli/handlers/use.js +6 -2
  20. package/dist/src/cli/hooks/builtin/activity-log-hook.js +6 -0
  21. package/dist/src/cli/router.js +1 -1
  22. package/dist/src/cli/scope-resolver.js +22 -0
  23. package/dist/src/dataset/adapter-sdk.d.ts +41 -0
  24. package/dist/src/dataset/adapter-sdk.js +147 -0
  25. package/dist/src/dataset/adapter-types.d.ts +179 -0
  26. package/dist/src/dataset/adapter-types.js +2 -0
  27. package/dist/src/dataset/adapters.d.ts +104 -0
  28. package/dist/src/dataset/adapters.js +518 -0
  29. package/dist/src/dataset/conformance-types.d.ts +84 -0
  30. package/dist/src/dataset/conformance-types.js +3 -0
  31. package/dist/src/dataset/conformance.d.ts +13 -0
  32. package/dist/src/dataset/conformance.js +90 -0
  33. package/dist/src/dataset/contracts.d.ts +17 -0
  34. package/dist/src/dataset/contracts.js +236 -0
  35. package/dist/src/dataset/file-orchestration-repository.d.ts +19 -0
  36. package/dist/src/dataset/file-orchestration-repository.js +68 -0
  37. package/dist/src/dataset/fortemi-execution-bridge.d.ts +33 -0
  38. package/dist/src/dataset/fortemi-execution-bridge.js +35 -0
  39. package/dist/src/dataset/index.d.ts +21 -0
  40. package/dist/src/dataset/index.js +21 -0
  41. package/dist/src/dataset/ledger-types.d.ts +141 -0
  42. package/dist/src/dataset/ledger-types.js +2 -0
  43. package/dist/src/dataset/ledger.d.ts +27 -0
  44. package/dist/src/dataset/ledger.js +100 -0
  45. package/dist/src/dataset/local-execution-backend.d.ts +10 -0
  46. package/dist/src/dataset/local-execution-backend.js +32 -0
  47. package/dist/src/dataset/orchestration-repository.d.ts +29 -0
  48. package/dist/src/dataset/orchestration-repository.js +34 -0
  49. package/dist/src/dataset/orchestration-service.d.ts +56 -0
  50. package/dist/src/dataset/orchestration-service.js +466 -0
  51. package/dist/src/dataset/orchestration-types.d.ts +83 -0
  52. package/dist/src/dataset/orchestration-types.js +2 -0
  53. package/dist/src/dataset/presentation.d.ts +3 -0
  54. package/dist/src/dataset/presentation.js +8 -0
  55. package/dist/src/dataset/projections.d.ts +42 -0
  56. package/dist/src/dataset/projections.js +192 -0
  57. package/dist/src/dataset/schema-governance.d.ts +71 -0
  58. package/dist/src/dataset/schema-governance.js +135 -0
  59. package/dist/src/dataset/standards-types.d.ts +66 -0
  60. package/dist/src/dataset/standards-types.js +10 -0
  61. package/dist/src/dataset/standards.d.ts +13 -0
  62. package/dist/src/dataset/standards.js +291 -0
  63. package/dist/src/dataset/types.d.ts +258 -0
  64. package/dist/src/dataset/types.js +2 -0
  65. package/dist/src/extensions/commands/definitions.js +27 -1
  66. package/dist/src/installation/manager.mjs +5 -1
  67. package/dist/src/models/model-capabilities.v1.json +11 -0
  68. package/dist/src/models/model-catalog.v1.json +8 -0
  69. package/dist/src/models/model-discovery.js +32 -0
  70. package/dist/src/models/provider-policy.js +3 -2
  71. package/dist/src/output-modes/index.js +4 -0
  72. package/dist/src/output-modes/registry.js +68 -24
  73. package/dist/src/output-modes/runtime.js +10 -8
  74. package/dist/src/plugin/skill-command-translator.js +1 -0
  75. package/dist/src/providers/capability-matrix.yaml +41 -0
  76. package/dist/src/providers/provider-definitions.js +72 -0
  77. package/dist/src/providers/provider-inventory.js +1 -0
  78. package/dist/src/schema/catalog.js +234 -0
  79. package/dist/src/schema/compatibility.js +42 -0
  80. package/dist/src/schema/diagnostics.js +36 -0
  81. package/dist/src/schema/index.js +8 -0
  82. package/dist/src/schema/policy.js +58 -0
  83. package/dist/src/schema/resolver.js +76 -0
  84. package/dist/src/schema/types.js +2 -0
  85. package/dist/src/schema/validator.js +82 -0
  86. package/dist/src/sessions/adapters/pi.js +141 -0
  87. package/dist/src/sessions/contracts.js +1 -1
  88. package/dist/src/sessions/index.js +1 -0
  89. package/dist/src/sessions/workspace-discovery.js +10 -0
  90. package/dist/src/storage/backends/fortemi.js +6 -0
  91. package/dist/src/storage/config.js +18 -4
  92. package/dist/src/storage/fortemi-qualification.js +106 -0
  93. package/dist/src/storage/index.js +1 -0
  94. package/dist/src/storage/types.js +1 -1
  95. package/package.json +3 -1
  96. package/schemas/dataset/conformance-manifest.v1.schema.json +35 -0
  97. package/schemas/dataset/conformance-receipt.v1.schema.json +22 -0
  98. package/schemas/dataset/dataset-contracts.v1.schema.json +117 -0
  99. package/schemas/dataset/dataset-deprecations.v1.schema.json +37 -0
  100. package/schemas/dataset/dataset-schema-governance.v1.schema.json +92 -0
  101. package/schemas/dataset/dataset-standards-exchange.v1.schema.json +59 -0
  102. package/schemas/dataset/profiles/openlineage-1.0.0.schema.json +15 -0
  103. package/schemas/dataset/profiles/prov-json-20130430.schema.json +12 -0
  104. package/schemas/dataset/run-ledger.v1.schema.json +39 -0
  105. package/schemas/dataset/source-adapter.v1.schema.json +112 -0
  106. package/tools/agents/deploy-agents.mjs +9 -3
  107. package/tools/agents/providers/pi.mjs +176 -0
@@ -52,6 +52,14 @@
52
52
  },
53
53
  "sourceUrl": "https://opencode.ai/docs/models", "verifiedAt": "2026-07-20"
54
54
  },
55
+ "pi": {
56
+ "roles": {
57
+ "reasoning": { "id": "configured/reasoning", "status": "unverified", "observed": false },
58
+ "coding": { "id": "configured/coding", "status": "unverified", "observed": false },
59
+ "efficiency": { "id": "configured/efficiency", "status": "unverified", "observed": false }
60
+ },
61
+ "sourceUrl": "https://github.com/earendil-works/pi/tree/main/packages/coding-agent#providers--models", "verifiedAt": "2026-09-04"
62
+ },
55
63
  "warp": {
56
64
  "roles": {
57
65
  "reasoning": { "id": "profile-selected", "status": "unverified", "observed": false },
@@ -67,6 +67,13 @@ export const PROVIDER_DISCOVERY_DECISIONS = {
67
67
  reason: 'OpenHuman profiles accept semantic model hints but expose no standardized local model-list command.',
68
68
  documentation: 'https://github.com/roctinam/openhuman',
69
69
  },
70
+ pi: {
71
+ provider: 'pi',
72
+ status: 'native',
73
+ interface: 'pi --list-models',
74
+ reason: 'Pi exposes the configured provider/model catalog through a read-only non-interactive table.',
75
+ documentation: 'https://github.com/earendil-works/pi/tree/main/packages/coding-agent#cli-reference',
76
+ },
70
77
  warp: {
71
78
  provider: 'warp',
72
79
  status: 'unsupported',
@@ -161,6 +168,30 @@ export async function discoverOpenCodeModels(command = 'opencode', runner = runM
161
168
  : {}),
162
169
  };
163
170
  }
171
+ export async function discoverPiModels(command = 'pi', runner = runModelDiscoveryCommand) {
172
+ const observedAt = new Date().toISOString();
173
+ const [version, result] = await Promise.all([
174
+ runtimeVersion(command, runner),
175
+ runner(command, ['--list-models'], { cwd: tmpdir(), timeoutMs: 15_000 }),
176
+ ]);
177
+ if (result.exitCode !== 0) {
178
+ const error = result.stderr.trim() || `Pi --list-models exited ${result.exitCode}`;
179
+ return { provider: 'pi', source: 'native', observedAt,
180
+ ...(version ? { runtimeVersion: version } : {}), accountScope: 'local-runtime',
181
+ models: [], errorKind: classifyDiscoveryError(error), error };
182
+ }
183
+ const lines = result.stdout.split(/\r?\n/).map(line => line.trim()).filter(Boolean);
184
+ const models = lines.slice(1).flatMap(line => {
185
+ const columns = line.split(/\s{2,}/);
186
+ if (columns.length < 2 || !/^[a-z0-9][a-z0-9._-]*$/i.test(columns[0]))
187
+ return [];
188
+ return [{ id: `${columns[0]}/${columns[1]}` }];
189
+ });
190
+ return { provider: 'pi', source: 'native', observedAt,
191
+ ...(version ? { runtimeVersion: version } : {}), accountScope: 'local-runtime', models,
192
+ ...(models.length === 0 ? { errorKind: 'invalid-output',
193
+ error: 'Pi returned no parseable provider/model rows.' } : {}) };
194
+ }
164
195
  export async function discoverOpenClawModels(command = 'openclaw', runner = runModelDiscoveryCommand) {
165
196
  const observedAt = new Date().toISOString();
166
197
  const [version, result] = await Promise.all([
@@ -455,6 +486,7 @@ export async function resolveDynamicModelCatalog(options) {
455
486
  codex: () => discoverCodexModels(),
456
487
  opencode: () => discoverOpenCodeModels(),
457
488
  openclaw: () => discoverOpenClawModels(),
489
+ pi: () => discoverPiModels(),
458
490
  };
459
491
  const providerDiscovery = {};
460
492
  for (const provider of available) {
@@ -27,7 +27,7 @@ const capabilityData = requireModelResource('model-capabilities.v1.json');
27
27
  const catalogData = requireModelResource('model-catalog.v1.json');
28
28
  const ProviderSchema = z.enum([
29
29
  'claude', 'codex', 'copilot', 'cursor', 'factory', 'hermes',
30
- 'opencode', 'openclaw', 'openhuman', 'warp', 'windsurf',
30
+ 'opencode', 'openclaw', 'openhuman', 'pi', 'warp', 'windsurf',
31
31
  ]);
32
32
  const OutcomeSchema = z.enum([
33
33
  'native', 'compiled', 'inherited', 'global-only', 'informational', 'unsupported',
@@ -107,7 +107,7 @@ export function loadProviderModelCapabilities() {
107
107
  const expected = new Set(ProviderSchema.options);
108
108
  const actual = new Set(Object.keys(registryCache.providers));
109
109
  if (actual.size !== expected.size || [...expected].some(id => !actual.has(id))) {
110
- throw new Error('Provider model capability registry must cover all 11 providers');
110
+ throw new Error('Provider model capability registry must cover all 12 providers');
111
111
  }
112
112
  }
113
113
  return registryCache;
@@ -159,6 +159,7 @@ function fieldNames(provider) {
159
159
  case 'copilot':
160
160
  case 'cursor':
161
161
  case 'opencode': return { model: 'model' };
162
+ case 'pi': return { model: 'model', effort: 'thinking' };
162
163
  case 'openhuman': return { model: 'model_hint' };
163
164
  case 'openclaw': return { model: 'subagents.model' };
164
165
  default: return {};
@@ -0,0 +1,4 @@
1
+ export * from './types.js';
2
+ export * from './registry.js';
3
+ export * from './runtime.js';
4
+ //# sourceMappingURL=index.js.map
@@ -1,23 +1,57 @@
1
1
  import { access, mkdir, readFile, readdir, writeFile } from 'node:fs/promises';
2
2
  import { constants } from 'node:fs';
3
- import { homedir, tmpdir } from 'node:os';
3
+ import { tmpdir } from 'node:os';
4
4
  import { createHash } from 'node:crypto';
5
5
  import { dirname, extname, join, resolve } from 'node:path';
6
6
  import { parse, stringify } from 'yaml';
7
+ import { z } from 'zod';
8
+ import { resolveUserConfigDir } from '../config/user-config-dir.mjs';
9
+ const MODE_ID_PATTERN = /^[a-z0-9][a-z0-9.-]*$/;
7
10
  const PROTECTED = ['code', 'commands', 'citations', 'quoted-text', 'identifiers', 'machine-readable-blocks'];
8
11
  const STAGE_ORDER = ['semantic', 'voice', 'controlled-language', 'structure', 'presentation'];
12
+ const modeIdSchema = z.string().regex(MODE_ID_PATTERN, 'must start with a lowercase letter or number and contain only lowercase letters, numbers, dots, or hyphens');
13
+ const uniqueModeIds = z.array(modeIdSchema).refine(values => new Set(values).size === values.length, 'must not contain duplicates');
14
+ const outputModeProfileSchema = z.object({
15
+ id: modeIdSchema,
16
+ version: z.string().min(1),
17
+ description: z.string().min(1),
18
+ kind: z.enum(['voice', 'controlled-language', 'structure', 'presentation']),
19
+ stage: z.enum(STAGE_ORDER),
20
+ order: z.number().int().optional(),
21
+ instructions: z.string(),
22
+ provenance: z.object({ source: z.string().min(1), license: z.string().min(1) }).strict(),
23
+ validation: z.object({
24
+ level: z.enum(['advisory', 'validated', 'conformance']),
25
+ hook: z.string().min(1).optional(),
26
+ standardVersion: z.string().min(1).optional(),
27
+ }).strict(),
28
+ compatible: uniqueModeIds.optional(),
29
+ conflicts: uniqueModeIds.optional(),
30
+ requires: uniqueModeIds.optional(),
31
+ supersedes: uniqueModeIds.optional(),
32
+ protectedContent: z.array(z.enum(PROTECTED)).refine(values => new Set(values).size === values.length, 'must not contain duplicates').optional(),
33
+ contextCost: z.number().int().nonnegative().optional(),
34
+ mergeStrategy: z.literal('weighted-voice').optional(),
35
+ }).strict().superRefine((profile, ctx) => {
36
+ if (profile.validation.level !== 'advisory' && !profile.validation.hook) {
37
+ ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['validation', 'hook'], message: `${profile.validation.level} modes require a validator hook` });
38
+ }
39
+ if (profile.validation.level === 'conformance' && !profile.validation.standardVersion) {
40
+ ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['validation', 'standardVersion'], message: 'conformance modes require a standard version' });
41
+ }
42
+ });
9
43
  const BUILTINS = [
10
44
  {
11
45
  id: 'unaltered', version: '1.0.0', description: 'No-op mode; preserves the provider output path unchanged.',
12
46
  kind: 'presentation', stage: 'presentation', order: -1000, instructions: '',
13
47
  provenance: { source: 'AIWG', license: 'MIT' }, validation: { level: 'advisory' }, contextCost: 0,
14
- protectedContent: PROTECTED,
48
+ protectedContent: [...PROTECTED],
15
49
  },
16
50
  {
17
51
  id: 'wittgenstein-inspired', version: '1.0.0', description: 'Concise, proposition-oriented stylistic profile; not impersonation or attribution.',
18
52
  kind: 'voice', stage: 'voice', order: 100, instructions: 'Prefer concise propositions, clarify terms in use, and expose category errors. Do not imitate or attribute text to Ludwig Wittgenstein.',
19
53
  provenance: { source: 'AIWG original style guidance', license: 'MIT' }, validation: { level: 'advisory' }, contextCost: 48,
20
- protectedContent: PROTECTED,
54
+ protectedContent: [...PROTECTED],
21
55
  },
22
56
  {
23
57
  id: 'asd-ste', version: '1.0.0', description: 'Operator-configured ASD Simplified Technical English adapter.',
@@ -25,13 +59,13 @@ const BUILTINS = [
25
59
  instructions: 'Apply only operator-supplied ASD-STE rules and approved terminology. Without licensed rules and a configured validator, describe output as advisory and never claim conformance.',
26
60
  provenance: { source: 'AIWG adapter; standard content supplied by operator', license: 'MIT adapter only' },
27
61
  validation: { level: 'advisory', standardVersion: 'operator-configured' }, contextCost: 64,
28
- protectedContent: PROTECTED,
62
+ protectedContent: [...PROTECTED],
29
63
  },
30
64
  ];
31
65
  function profileDirs(cwd) {
32
66
  return [
33
67
  { dir: join(cwd, '.aiwg', 'output-modes'), source: 'project' },
34
- { dir: join(homedir(), '.config', 'aiwg', 'output-modes'), source: 'user' },
68
+ { dir: join(resolveUserConfigDir(), 'output-modes'), source: 'user' },
35
69
  ];
36
70
  }
37
71
  async function readable(path) {
@@ -43,31 +77,33 @@ async function readable(path) {
43
77
  return false;
44
78
  }
45
79
  }
46
- function validateProfile(value, path) {
47
- if (!value || typeof value !== 'object')
48
- throw new Error(`Invalid output mode profile at ${path}: expected an object`);
49
- const p = value;
50
- for (const field of ['id', 'version', 'description', 'kind', 'stage', 'instructions', 'provenance', 'validation']) {
51
- if (p[field] === undefined)
52
- throw new Error(`Invalid output mode profile at ${path}: missing ${field}`);
80
+ export function validateOutputModeProfile(value, path = '<profile>') {
81
+ const result = outputModeProfileSchema.safeParse(value);
82
+ if (!result.success) {
83
+ const details = result.error.issues
84
+ .map(issue => `${issue.path.length ? issue.path.join('.') : 'profile'}: ${issue.message}`)
85
+ .join('; ');
86
+ throw new Error(`Invalid output mode profile at ${path}: ${details}`);
53
87
  }
54
- if (!['voice', 'controlled-language', 'structure', 'presentation'].includes(String(p.kind)))
55
- throw new Error(`Invalid output mode kind in ${path}: ${p.kind}`);
56
- if (!STAGE_ORDER.includes(String(p.stage)))
57
- throw new Error(`Invalid output mode stage in ${path}: ${p.stage}`);
58
- return p;
88
+ return result.data;
59
89
  }
60
90
  async function loadDirectory(dir, source) {
61
91
  if (!(await readable(dir)))
62
92
  return [];
63
93
  const result = [];
94
+ const seen = new Map();
64
95
  for (const name of (await readdir(dir)).sort()) {
65
96
  if (!['.yaml', '.yml', '.json'].includes(extname(name)))
66
97
  continue;
67
98
  const sourcePath = join(dir, name);
68
99
  const raw = await readFile(sourcePath, 'utf8');
69
100
  const value = extname(name) === '.json' ? JSON.parse(raw) : parse(raw);
70
- result.push({ ...validateProfile(value, sourcePath), source, sourcePath });
101
+ const profile = validateOutputModeProfile(value, sourcePath);
102
+ const previous = seen.get(profile.id);
103
+ if (previous)
104
+ throw new Error(`Duplicate output mode '${profile.id}' in ${previous} and ${sourcePath}. Keep one definition per scope.`);
105
+ seen.set(profile.id, sourcePath);
106
+ result.push({ ...profile, source, sourcePath });
71
107
  }
72
108
  return result;
73
109
  }
@@ -84,7 +120,7 @@ async function loadVoiceAdapters(frameworkRoot) {
84
120
  id, version: String(voice.version ?? '1.0.0'), description: String(voice.description ?? `Adapted voice profile: ${id}`),
85
121
  kind: 'voice', stage: 'voice', order: 100, instructions: `Apply the existing voice profile '${id}' through voice-apply.`,
86
122
  provenance: { source: sourcePath, license: String(voice.license ?? 'project license') },
87
- validation: { level: 'advisory' }, protectedContent: PROTECTED, contextCost: 32,
123
+ validation: { level: 'advisory' }, protectedContent: [...PROTECTED], contextCost: 32,
88
124
  mergeStrategy: 'weighted-voice', source: 'voice-adapter', sourcePath,
89
125
  });
90
126
  }
@@ -115,7 +151,10 @@ export async function readOutputModeState(cwd, scope) {
115
151
  if (!(await readable(path)))
116
152
  return { version: 1, modes: [] };
117
153
  const value = parse(await readFile(path, 'utf8'));
118
- return { version: 1, modes: Array.isArray(value.modes) ? value.modes.map(String) : [] };
154
+ if (!value || value.version !== 1 || !Array.isArray(value.modes) || value.modes.some(mode => typeof mode !== 'string' || !MODE_ID_PATTERN.test(mode))) {
155
+ throw new Error(`Invalid output mode state at ${path}: expected version 1 and an array of valid mode IDs.`);
156
+ }
157
+ return { version: 1, modes: [...new Set(value.modes)] };
119
158
  }
120
159
  export async function writeOutputModeState(cwd, scope, modes) {
121
160
  const path = statePath(cwd, scope);
@@ -123,11 +162,16 @@ export async function writeOutputModeState(cwd, scope, modes) {
123
162
  await writeFile(path, stringify({ version: 1, modes }), 'utf8');
124
163
  return path;
125
164
  }
126
- export async function resolveOutputModes(cwd, frameworkRoot, invocation = []) {
165
+ export async function resolveOutputModes(cwd, frameworkRoot, invocation = [], overrides = {}) {
127
166
  const registry = await loadOutputModeRegistry(cwd, frameworkRoot);
128
- const project = await readOutputModeState(cwd, 'project');
129
- const session = await readOutputModeState(cwd, 'session');
130
- const selected = [...project.modes.map(id => ({ id, scope: 'project' })), ...session.modes.map(id => ({ id, scope: 'session' })), ...invocation.map(id => ({ id, scope: 'invocation' }))];
167
+ const project = overrides.project ?? (await readOutputModeState(cwd, 'project')).modes;
168
+ const session = overrides.session ?? (await readOutputModeState(cwd, 'session')).modes;
169
+ for (const [scope, modes] of [['project', project], ['session', session], ['invocation', invocation]]) {
170
+ if (!Array.isArray(modes) || modes.some(id => typeof id !== 'string' || !MODE_ID_PATTERN.test(id))) {
171
+ throw new Error(`Invalid ${scope} output mode selection: expected valid mode IDs.`);
172
+ }
173
+ }
174
+ const selected = [...project.map(id => ({ id, scope: 'project' })), ...session.map(id => ({ id, scope: 'session' })), ...invocation.map(id => ({ id, scope: 'invocation' }))];
131
175
  const effective = new Map();
132
176
  const diagnostics = [];
133
177
  for (const item of selected) {
@@ -16,8 +16,11 @@ function protectedPattern(classes) {
16
16
  function protect(content, classes) {
17
17
  const literals = [];
18
18
  const pattern = protectedPattern(classes);
19
+ let tokenPrefix = '\uE000AIWG_OUTPUT_MODE_';
20
+ while (content.includes(tokenPrefix))
21
+ tokenPrefix += '_';
19
22
  const protectedContent = pattern ? content.replace(pattern, value => {
20
- const token = `\uE000${literals.length}\uE001`;
23
+ const token = `${tokenPrefix}${literals.length}\uE001`;
21
24
  literals.push({ token, value });
22
25
  return token;
23
26
  }) : content;
@@ -26,9 +29,10 @@ function protect(content, classes) {
26
29
  function restore(content, literals, mode) {
27
30
  let restored = content;
28
31
  for (const literal of literals) {
29
- if (!restored.includes(literal.token))
30
- throw new Error(`Output mode '${mode}' modified or removed a protected literal.`);
31
- restored = restored.replaceAll(literal.token, literal.value);
32
+ const occurrences = restored.split(literal.token).length - 1;
33
+ if (occurrences !== 1)
34
+ throw new Error(`Output mode '${mode}' modified, removed, or duplicated a protected literal.`);
35
+ restored = restored.replace(literal.token, literal.value);
32
36
  }
33
37
  return restored;
34
38
  }
@@ -39,9 +43,10 @@ export async function applyOutputModes(input, modes, options) {
39
43
  const diagnostics = [];
40
44
  const applied = [];
41
45
  for (const mode of modes) {
42
- const snapshot = content;
43
46
  const masked = protect(content, mode.protectedContent ?? []);
44
47
  const transformed = await options.transform(masked.content, mode);
48
+ if (typeof transformed !== 'string')
49
+ throw new Error(`Output mode '${mode.id}' transform returned a non-string result.`);
45
50
  content = restore(transformed, masked.literals, mode.id);
46
51
  if (mode.validation.level !== 'advisory') {
47
52
  if (!options.validate)
@@ -54,9 +59,6 @@ export async function applyOutputModes(input, modes, options) {
54
59
  return { content: input, diagnostics, applied, fallback: 'unaltered' };
55
60
  }
56
61
  }
57
- // A transform may only change semantic presentation, never return an absent result.
58
- if (typeof content !== 'string')
59
- content = snapshot;
60
62
  applied.push(mode.id);
61
63
  }
62
64
  return { content, diagnostics, applied, fallback: 'none' };
@@ -38,6 +38,7 @@ const SKILLS_ONLY_PROVIDERS = new Set([
38
38
  'cursor',
39
39
  'hermes',
40
40
  'openhuman',
41
+ 'pi',
41
42
  ]);
42
43
  /**
43
44
  * Check if a provider needs command files generated from skills.
@@ -470,6 +470,47 @@ providers:
470
470
  deploy_target: mixed
471
471
  aggregated_output: true
472
472
 
473
+ pi:
474
+ display_name: Pi Coding Agent
475
+ aliases:
476
+ - pi-coding-agent
477
+ status: experimental
478
+ daemon_tier: unsupported
479
+ daemon_pty_adapter: false
480
+ artifact_paths:
481
+ agents: .agents/skills/
482
+ commands: .pi/prompts/
483
+ skills: .pi/skills/
484
+ skills_cross_agent: .agents/skills/
485
+ rules: AGENTS.md
486
+ behaviors: .pi/extensions/
487
+ native_features:
488
+ cron: false
489
+ agent_teams: false
490
+ tasks: false
491
+ mcp: false
492
+ behaviors: true
493
+ mission_control: false
494
+ daemon: false
495
+ emulation:
496
+ cron: external-trigger
497
+ agent_teams: aiwg-mc
498
+ tasks: aiwg-mc
499
+ mcp: null
500
+ behaviors: null
501
+ mission_control: aiwg-mc
502
+ daemon: null
503
+ hook_wiring:
504
+ at_link_support: false
505
+ fallback: full-inject
506
+ context_file: AGENTS.md
507
+ interaction:
508
+ structured_questions: none
509
+ fallback: markdown
510
+ notes: Pi does not expose a built-in structured question tool.
511
+ deploy_target: project
512
+ aggregated_output: false
513
+
473
514
  # Feature definitions — documents what each feature key means and
474
515
  # what emulation strategies are available when native support is absent.
475
516
  features:
@@ -21,6 +21,7 @@ const ProviderDefinitionSchema = z.object({
21
21
  'opencode',
22
22
  'openclaw',
23
23
  'openhuman',
24
+ 'pi',
24
25
  'warp',
25
26
  'windsurf',
26
27
  'generic',
@@ -131,6 +132,7 @@ export const PROVIDER_IDS = [
131
132
  'opencode',
132
133
  'openclaw',
133
134
  'openhuman',
135
+ 'pi',
134
136
  'warp',
135
137
  'windsurf',
136
138
  'generic',
@@ -191,6 +193,17 @@ const CONTEXT_CONTRACTS = {
191
193
  bootstrapTargets: ['AGENTS.md'], maxContextBytes: null, recommendedMaxLines: null, nestedContext: false, support: 'degraded',
192
194
  verification: { method: 'AIWG mixed-scope adapter contract; host loading remains capability-dependent', source: 'agentic/code/providers/capability-matrix.yaml', lastVerified: VERIFIED_ON },
193
195
  },
196
+ pi: {
197
+ startupFiles: ['AGENTS.override.md', 'AGENTS.md', 'CLAUDE.md'],
198
+ precedence: ['provider/system', 'root-to-cwd context chain', 'AGENTS.override.md supersedes same-directory AGENTS.md and CLAUDE.md'],
199
+ loadMode: 'prose-directive', includeSyntax: null, configRegistration: null,
200
+ bootstrapTargets: ['AGENTS.md'], maxContextBytes: null, recommendedMaxLines: null, nestedContext: true, support: 'supported',
201
+ verification: {
202
+ method: 'Pi coding-agent resource loader and official README context-file documentation',
203
+ source: 'https://github.com/earendil-works/pi/blob/79680533c6b898894f2d2421c7f640b212d3dfdd/packages/coding-agent/README.md#context-files',
204
+ lastVerified: '2026-09-03',
205
+ },
206
+ },
194
207
  warp: {
195
208
  startupFiles: ['WARP.md', 'AGENTS.md'], precedence: ['provider/system', 'subdirectory rule', 'root rule', 'global rule'],
196
209
  loadMode: 'prose-directive', includeSyntax: null, configRegistration: null,
@@ -656,6 +669,65 @@ const BUILT_IN_SEEDS = [
656
669
  },
657
670
  matrixRef: 'openhuman',
658
671
  },
672
+ {
673
+ id: 'pi',
674
+ aliases: ['pi-coding-agent'],
675
+ builtIn: true,
676
+ surfaces: {
677
+ primary: 'pi',
678
+ compatibility: ['pi-coding-agent'],
679
+ precedence: ['AGENTS.override.md', 'AGENTS.md', '.agents/skills/', '.pi/skills/', '.pi/prompts/'],
680
+ related: [],
681
+ },
682
+ detection: {
683
+ // PI_CODING_AGENT_DIR only relocates configuration; it is deliberately
684
+ // not an active-runtime marker. Availability is proven by process or executable.
685
+ env: [],
686
+ process: ['pi'],
687
+ capabilityId: 'pi',
688
+ },
689
+ paths: {
690
+ artifacts: {
691
+ agents: '.agents/skills',
692
+ commands: '.pi/prompts',
693
+ skills: '.pi/.aiwg/skills',
694
+ rules: null,
695
+ behaviors: '.pi/extensions',
696
+ },
697
+ kernelSkills: '.agents/skills',
698
+ contextDiscovery: {
699
+ agents: '.agents/skills',
700
+ skills: '.agents/skills',
701
+ rules: null,
702
+ behaviors: '.pi/extensions',
703
+ },
704
+ configFile: 'AGENTS.md',
705
+ contextFiles: { aiwgMd: true, agentsMd: true, claudeMdHook: false, hookFile: null, contextFile: 'AGENTS.md' },
706
+ },
707
+ smithPaths: {
708
+ agents: '.agents/skills',
709
+ commands: '.pi/prompts',
710
+ skills: '.pi/skills',
711
+ rules: null,
712
+ fileExtension: '.md',
713
+ configFile: 'AGENTS.md',
714
+ aggregated: false,
715
+ },
716
+ skillNamespace: {
717
+ deploymentGroup: 'deep-recursion',
718
+ pathType: 'project',
719
+ skillsBaseDir: '.pi/skills',
720
+ subdirLayout: true,
721
+ },
722
+ adapters: {
723
+ agentFormat: 'agents-md',
724
+ hookBridge: null,
725
+ mcpInjection: null,
726
+ contextAggregation: 'agents-md',
727
+ ruleFormat: 'agents-md-section',
728
+ },
729
+ matrixRef: 'pi',
730
+ },
659
731
  {
660
732
  id: 'warp',
661
733
  aliases: [],
@@ -14,6 +14,7 @@ const PROVIDER_EXECUTABLES = {
14
14
  opencode: ['opencode'],
15
15
  openclaw: ['openclaw'],
16
16
  openhuman: ['openhuman'],
17
+ pi: ['pi'],
17
18
  warp: ['warp'],
18
19
  windsurf: ['windsurf'],
19
20
  };