triad-plus 1.2.0 → 1.4.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 +23 -0
- package/README.md +11 -6
- package/adapters/antigravity/.agents/agents/triad-evaluator/agent.md +8 -1
- package/adapters/antigravity/.agents/agents/triad-orchestrator/agent.md +7 -5
- package/adapters/antigravity/.agents/skills/triad/SKILL.md +6 -5
- package/adapters/claude-code/.claude/agents/triad-evaluator.md +9 -1
- package/adapters/claude-code/.claude/commands/triad.md +6 -5
- package/adapters/codex/README.md +6 -4
- package/adapters/codex/prompts/triad.md +27 -9
- package/adapters/codex/runtime.json +4 -0
- package/adapters/copilot/.github/agents/triad-developer.agent.md +22 -0
- package/adapters/copilot/.github/agents/triad-evaluator.agent.md +26 -0
- package/adapters/copilot/.github/agents/triad-orchestrator.agent.md +52 -0
- package/adapters/copilot/.github/agents/triad-reviewer.agent.md +22 -0
- package/adapters/copilot/.github/skills/triad/SKILL.md +52 -0
- package/adapters/copilot/README.md +20 -0
- package/adapters/copilot/runtime.json +9 -0
- package/adapters/hermes/skills/triad/SKILL.md +6 -5
- package/adapters/opencode/.opencode/agents/triad-developer.md +4 -0
- package/adapters/opencode/.opencode/agents/triad-evaluator.md +14 -5
- package/adapters/opencode/.opencode/agents/triad-orchestrator.md +7 -5
- package/adapters/opencode/.opencode/agents/triad-reviewer.md +4 -0
- package/adapters/opencode/.opencode/commands/triad.md +6 -5
- package/adapters/registry.mjs +42 -0
- package/bin/triad-plus.js +91 -18
- package/docs/codex-replication.md +8 -4
- package/docs/compatibility.md +2 -1
- package/docs/configuration.md +6 -5
- package/docs/npx-installation.md +2 -1
- package/docs/operating-guide.it.md +19 -1
- package/docs/operating-guide.md +19 -1
- package/docs/runtimes.md +24 -1
- package/docs/verification.md +25 -0
- package/integrations/codex/README.md +20 -12
- package/package.json +1 -1
- package/runtime/legacy-adapters.json +9 -1
- package/runtime/triad-runtime-capabilities.mjs +30 -2
- package/runtime/triad-verify.mjs +22 -0
- package/schemas/runtime-capabilities.schema.json +1 -1
- package/skills/triad-loop-bootstrap/SKILL.md +3 -1
- package/skills/triad-loop-bootstrap/assets/loop-template/feature-card.template.md +6 -0
- package/skills/triad-loop-bootstrap/assets/loop-template/handoff-report.template.md +8 -0
- package/skills/triad-loop-bootstrap/assets/loop-template/run-state.yaml +8 -0
- package/skills/triad-loop-bootstrap/assets/loop-template/runtime/assignments/assignment.template.json +10 -0
- package/skills/triad-loop-bootstrap/assets/project.yaml +10 -0
- package/skills/triad-loop-developer/SKILL.md +12 -0
- package/skills/triad-loop-evaluator/SKILL.md +22 -2
- package/skills/triad-loop-orchestrator/SKILL.md +65 -11
- package/skills/triad-loop-reviewer/SKILL.md +13 -0
package/adapters/registry.mjs
CHANGED
|
@@ -210,6 +210,48 @@ const definitions = [
|
|
|
210
210
|
const root = join(hermesProfileHome(), 'skills');
|
|
211
211
|
return [join(root, 'triad'), ...sharedSkillNames.map((name) => join(root, name))];
|
|
212
212
|
}
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
id: 'copilot',
|
|
216
|
+
label: 'GitHub Copilot',
|
|
217
|
+
binary: 'copilot',
|
|
218
|
+
entry: '/triad',
|
|
219
|
+
fallbackEntry: '/agent',
|
|
220
|
+
lifecycle: null,
|
|
221
|
+
modelBinding: 'project-frontmatter',
|
|
222
|
+
modelFields: ['model', 'reasoningEffort'],
|
|
223
|
+
modelRoles: roleDefinitions.map((role) => role.id),
|
|
224
|
+
projectAssets: [
|
|
225
|
+
{ source: 'adapters/copilot/.github/agents', destination: '.github/agents' },
|
|
226
|
+
{ source: 'adapters/copilot/.github/skills/triad', destination: '.github/skills/triad' },
|
|
227
|
+
sharedSkills('.github/skills'),
|
|
228
|
+
...projectRuntimeAssets('copilot')
|
|
229
|
+
],
|
|
230
|
+
globalAssets: [
|
|
231
|
+
{ source: 'adapters/copilot/.github/agents', destination: () => hostHome('.copilot', 'agents') },
|
|
232
|
+
{ source: 'adapters/copilot/.github/skills/triad', destination: () => hostHome('.copilot', 'skills', 'triad') },
|
|
233
|
+
sharedSkills(() => hostHome('.copilot', 'skills'))
|
|
234
|
+
],
|
|
235
|
+
projectPaths(controlRoot) {
|
|
236
|
+
const root = join(controlRoot, '.github');
|
|
237
|
+
return [
|
|
238
|
+
...roleDefinitions.map((role) => join(root, 'agents', `triad-${role.id}.agent.md`)),
|
|
239
|
+
join(root, 'skills', 'triad'),
|
|
240
|
+
...sharedSkillNames.map((name) => join(root, 'skills', name)),
|
|
241
|
+
join(controlRoot, '.triad-runtime')
|
|
242
|
+
];
|
|
243
|
+
},
|
|
244
|
+
globalPaths() {
|
|
245
|
+
const root = hostHome('.copilot');
|
|
246
|
+
return [
|
|
247
|
+
...roleDefinitions.map((role) => join(root, 'agents', `triad-${role.id}.agent.md`)),
|
|
248
|
+
join(root, 'skills', 'triad'),
|
|
249
|
+
...sharedSkillNames.map((name) => join(root, 'skills', name))
|
|
250
|
+
];
|
|
251
|
+
},
|
|
252
|
+
roleModelPaths(controlRoot) {
|
|
253
|
+
return roleDefinitions.map((role) => join(controlRoot, '.github', 'agents', `triad-${role.id}.agent.md`));
|
|
254
|
+
}
|
|
213
255
|
}
|
|
214
256
|
];
|
|
215
257
|
|
package/bin/triad-plus.js
CHANGED
|
@@ -132,29 +132,40 @@ function teamConfigPath(controlRoot) {
|
|
|
132
132
|
|
|
133
133
|
const overlayStart = '<!-- triad-plus:managed-instructions:start -->';
|
|
134
134
|
const overlayEnd = '<!-- triad-plus:managed-instructions:end -->';
|
|
135
|
-
|
|
135
|
+
function instructionOverlay(team) {
|
|
136
|
+
const displayName = typeof team?.roles?.orchestrator?.displayName === 'string' && team.roles.orchestrator.displayName.trim()
|
|
137
|
+
? team.roles.orchestrator.displayName.trim()
|
|
138
|
+
: 'Triad Orchestrator';
|
|
139
|
+
return `${overlayStart}
|
|
136
140
|
## Triad+ role-run overlay
|
|
137
141
|
|
|
138
142
|
When a Triad+ entry point is invoked in this control workspace, read
|
|
139
143
|
\`.triad-plus/team.json\` before the first owner-facing reply. The active
|
|
140
|
-
Orchestrator
|
|
141
|
-
is a
|
|
142
|
-
|
|
144
|
+
Orchestrator is \`${displayName}\` for this run. Its first owner-facing message
|
|
145
|
+
is a presentation, not a bootstrap report: begin with a first-person sentence
|
|
146
|
+
that explicitly names \`${displayName}\` and says it is the Triad+ Orchestrator,
|
|
147
|
+
then state whether the run is new or resumed and the received input. Do this
|
|
148
|
+
before reporting bootstrap, inspecting artifacts, delegating, or asking a
|
|
149
|
+
question. This is a role-run presentation rule; it does not change technical
|
|
150
|
+
authority, repository policy, safety instructions, or the host's identity
|
|
151
|
+
outside Triad+.
|
|
143
152
|
${overlayEnd}`;
|
|
153
|
+
}
|
|
144
154
|
|
|
145
|
-
async function overlayPlan(controlRoot) {
|
|
155
|
+
async function overlayPlan(controlRoot, team) {
|
|
146
156
|
const target = join(controlRoot, 'AGENTS.md');
|
|
147
|
-
|
|
157
|
+
const overlay = instructionOverlay(team);
|
|
158
|
+
if (!(await exists(target))) return { target, action: 'create', content: `# Project instructions\n\n${overlay}\n` };
|
|
148
159
|
const source = await readFile(target, 'utf8');
|
|
149
160
|
const start = source.indexOf(overlayStart);
|
|
150
161
|
const end = source.indexOf(overlayEnd);
|
|
151
|
-
if (start === -1 && end === -1) return { target, action: 'append', content: `${source.replace(/\s*$/, '')}\n\n${
|
|
162
|
+
if (start === -1 && end === -1) return { target, action: 'append', content: `${source.replace(/\s*$/, '')}\n\n${overlay}\n` };
|
|
152
163
|
if (start < 0 || end < start) throw new Error(`Cannot safely update managed instruction block: ${target}`);
|
|
153
|
-
return { target, action: 'update', content: `${source.slice(0, start)}${
|
|
164
|
+
return { target, action: 'update', content: `${source.slice(0, start)}${overlay}${source.slice(end + overlayEnd.length)}` };
|
|
154
165
|
}
|
|
155
166
|
|
|
156
|
-
async function applyOverlay(controlRoot, apply) {
|
|
157
|
-
const plan = await overlayPlan(controlRoot);
|
|
167
|
+
async function applyOverlay(controlRoot, apply, team) {
|
|
168
|
+
const plan = await overlayPlan(controlRoot, team);
|
|
158
169
|
process.stdout.write(` Instructions ${apply ? plan.action : `would ${plan.action}`} ${plan.target}\n`);
|
|
159
170
|
if (apply) await writeFile(plan.target, plan.content, 'utf8');
|
|
160
171
|
}
|
|
@@ -181,14 +192,20 @@ async function writeTeamConfig(controlRoot, team) {
|
|
|
181
192
|
await writeFile(target, `${JSON.stringify(team, null, 2)}\n`, 'utf8');
|
|
182
193
|
}
|
|
183
194
|
|
|
184
|
-
async function applyMarkdownModel(target, model) {
|
|
185
|
-
if (!
|
|
195
|
+
async function applyMarkdownModel(target, configuration, fields = ['model']) {
|
|
196
|
+
if (!fields.some((field) => configuration?.[field === 'reasoningEffort' ? 'reasoning_effort' : field])) return;
|
|
186
197
|
const source = await readFile(target, 'utf8');
|
|
187
198
|
if (!source.startsWith('---\n')) throw new Error(`Agent definition has no YAML frontmatter: ${target}`);
|
|
188
199
|
const closing = source.indexOf('\n---\n', 4);
|
|
189
200
|
if (closing === -1) throw new Error(`Agent definition has invalid YAML frontmatter: ${target}`);
|
|
190
|
-
|
|
191
|
-
|
|
201
|
+
let frontmatter = source.slice(4, closing);
|
|
202
|
+
for (const field of fields) frontmatter = frontmatter.replace(new RegExp(`^${field}:\\s*.*\\n?`, 'm'), '');
|
|
203
|
+
const values = fields
|
|
204
|
+
.map((field) => ({ field, value: field === 'reasoningEffort' ? configuration.reasoning_effort : configuration[field] }))
|
|
205
|
+
.filter(({ value }) => value !== null && value !== undefined && value !== '');
|
|
206
|
+
if (values.length === 0) return;
|
|
207
|
+
const additions = values.map(({ field, value }) => `${field}: ${JSON.stringify(value)}`).join('\n');
|
|
208
|
+
await writeFile(target, `---\n${frontmatter}${additions}\n${source.slice(closing)}`, 'utf8');
|
|
192
209
|
}
|
|
193
210
|
|
|
194
211
|
async function writeRoleProfiles(paths, team) {
|
|
@@ -199,6 +216,7 @@ async function writeRoleProfiles(paths, team) {
|
|
|
199
216
|
`Act as ${configuration.displayName}, the ${role.label} role in Triad+.`,
|
|
200
217
|
`Persona: ${configuration.persona || 'professional and role-focused'}.`,
|
|
201
218
|
'Read .triad-plus/team.json in the active project-control workspace before working.',
|
|
219
|
+
`At the beginning of each activation, identify yourself as ${configuration.displayName}, the Triad+ ${role.label}, in your first role report.`,
|
|
202
220
|
'Technical role IDs define authority; display names never change it.'
|
|
203
221
|
].join('\n');
|
|
204
222
|
const profile = [
|
|
@@ -219,7 +237,9 @@ async function applyTeamBinding(adapter, controlRoot, team, installContext) {
|
|
|
219
237
|
const paths = adapter.roleModelPaths(controlRoot, installContext);
|
|
220
238
|
const roles = (adapter.modelRoles ?? roleDefinitions.map((role) => role.id))
|
|
221
239
|
.map((roleId) => roleDefinitions.find((role) => role.id === roleId));
|
|
222
|
-
for (const [index, role] of roles.entries())
|
|
240
|
+
for (const [index, role] of roles.entries()) {
|
|
241
|
+
await applyMarkdownModel(paths[index], team.roles[role.id], adapter.modelFields ?? ['model']);
|
|
242
|
+
}
|
|
223
243
|
return;
|
|
224
244
|
}
|
|
225
245
|
if (adapter.modelBinding === 'global-profiles') {
|
|
@@ -242,6 +262,11 @@ async function collisions(paths) {
|
|
|
242
262
|
return result;
|
|
243
263
|
}
|
|
244
264
|
|
|
265
|
+
async function allExist(paths) {
|
|
266
|
+
for (const target of paths) if (!(await exists(target))) return false;
|
|
267
|
+
return true;
|
|
268
|
+
}
|
|
269
|
+
|
|
245
270
|
function commandVersion(binary) {
|
|
246
271
|
const candidates = Array.isArray(binary) ? binary : [binary];
|
|
247
272
|
for (const candidate of candidates) {
|
|
@@ -251,6 +276,17 @@ function commandVersion(binary) {
|
|
|
251
276
|
return null;
|
|
252
277
|
}
|
|
253
278
|
|
|
279
|
+
function capabilitySnapshot(controlRoot, manifestPath) {
|
|
280
|
+
if (!manifestPath) return null;
|
|
281
|
+
const detector = join(packageRoot, 'runtime', 'triad-runtime-capabilities.mjs');
|
|
282
|
+
const result = spawnSync(process.execPath, [detector, '--adapter', manifestPath], {
|
|
283
|
+
cwd: controlRoot,
|
|
284
|
+
encoding: 'utf8'
|
|
285
|
+
});
|
|
286
|
+
if (result.status !== 0) return null;
|
|
287
|
+
try { return JSON.parse(result.stdout); } catch { return null; }
|
|
288
|
+
}
|
|
289
|
+
|
|
254
290
|
async function collectTeamConfiguration(prompt, adapter) {
|
|
255
291
|
const language = (await prompt.question('Conversation language [English]: ')).trim() || 'English';
|
|
256
292
|
const ownerName = (await prompt.question('How should Triad+ address the project owner [Owner]: ')).trim() || 'Owner';
|
|
@@ -291,7 +327,7 @@ async function init(options) {
|
|
|
291
327
|
if (existing.length > 0) throw new Error(`Installation aborted; existing paths would be overwritten:\n${existing.map((target) => ` ${target}`).join('\n')}`);
|
|
292
328
|
await installAssets(adapter.projectAssets, controlRoot, installContext);
|
|
293
329
|
if (team) await writeTeamConfig(controlRoot, team);
|
|
294
|
-
if (team) await applyOverlay(controlRoot, true);
|
|
330
|
+
if (team) await applyOverlay(controlRoot, true, team);
|
|
295
331
|
if (team) await applyTeamBinding(adapter, controlRoot, team, installContext);
|
|
296
332
|
if (options.global) await installAssets(adapter.globalAssets, controlRoot, installContext);
|
|
297
333
|
process.stdout.write(`Triad+ installed for ${adapter.label} in ${controlRoot}\n`);
|
|
@@ -308,6 +344,35 @@ async function currentTeam(controlRoot) {
|
|
|
308
344
|
catch { throw new Error(`Cannot safely upgrade an invalid team config: ${target}`); }
|
|
309
345
|
}
|
|
310
346
|
|
|
347
|
+
const deliveryStateDefault = `
|
|
348
|
+
delivery:
|
|
349
|
+
status: not_delivered
|
|
350
|
+
handoff: null
|
|
351
|
+
branches: []
|
|
352
|
+
evaluator_report: null
|
|
353
|
+
delivered_at: null
|
|
354
|
+
owner_message: null
|
|
355
|
+
`;
|
|
356
|
+
|
|
357
|
+
async function upgradeRunStateDelivery(controlRoot, backupRoot, apply) {
|
|
358
|
+
const target = join(controlRoot, '.loop', 'run-state.yaml');
|
|
359
|
+
if (!(await exists(target))) {
|
|
360
|
+
process.stdout.write(' Delivery state skipped: no initialized .loop/run-state.yaml\n');
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
const source = await readFile(target, 'utf8');
|
|
364
|
+
if (/^delivery:\s*$/m.test(source)) {
|
|
365
|
+
process.stdout.write(` Delivery state already present ${target}\n`);
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
process.stdout.write(` ${apply ? 'Initialize' : 'Would initialize'} delivery state ${target} (backup)\n`);
|
|
369
|
+
if (!apply) return;
|
|
370
|
+
const backup = join(backupRoot, 'project', 'run-state.yaml');
|
|
371
|
+
await mkdir(dirname(backup), { recursive: true });
|
|
372
|
+
await cp(target, backup);
|
|
373
|
+
await writeFile(target, `${source.replace(/\s*$/, '')}\n${deliveryStateDefault}`, 'utf8');
|
|
374
|
+
}
|
|
375
|
+
|
|
311
376
|
async function upgrade(options) {
|
|
312
377
|
const adapter = getAdapter(options.host);
|
|
313
378
|
if (!adapter) throw new Error(`Choose --host ${listAdapters().map((item) => item.id).join(', ')}.`);
|
|
@@ -320,7 +385,8 @@ async function upgrade(options) {
|
|
|
320
385
|
const backupRoot = join(controlRoot, '.triad-plus', 'backups', stamp);
|
|
321
386
|
process.stdout.write(`Triad+ upgrade ${options.apply ? 'applying' : 'plan'} for ${adapter.label}\n`);
|
|
322
387
|
await refreshAssets(adapter.projectAssets, controlRoot, installContext, join(backupRoot, 'project'), options.apply);
|
|
323
|
-
|
|
388
|
+
await upgradeRunStateDelivery(controlRoot, backupRoot, options.apply);
|
|
389
|
+
if (team) await applyOverlay(controlRoot, options.apply, team);
|
|
324
390
|
else process.stdout.write(' Instructions skipped: .triad-plus/team.json is not configured\n');
|
|
325
391
|
if (options.global) {
|
|
326
392
|
await refreshAssets(adapter.globalAssets, controlRoot, installContext, join(backupRoot, 'global'), options.apply);
|
|
@@ -348,13 +414,20 @@ async function doctor(options) {
|
|
|
348
414
|
const manifestPath = join(controlRoot, '.triad-runtime', 'adapter.json');
|
|
349
415
|
let manifest = false;
|
|
350
416
|
try { manifest = (JSON.parse(await readFile(manifestPath, 'utf8'))?.id === adapter.id); } catch {}
|
|
417
|
+
const targets = adapter.projectPaths(controlRoot, installContext);
|
|
418
|
+
const roleTargets = targets.filter((target) => /[/\\]agents[/\\]triad-/.test(target));
|
|
419
|
+
const triadSkillTargets = targets.filter((target) => /[/\\]skills[/\\]triad$/.test(target));
|
|
420
|
+
const capability = manifest ? capabilitySnapshot(controlRoot, manifestPath) : null;
|
|
351
421
|
process.stdout.write(`${adapter.label.padEnd(14)} ${absent.length ? 'not installed' : 'OK'}\n`);
|
|
352
422
|
process.stdout.write(` Host runtime ${binary ? `OK (${binary})` : 'not installed or version unavailable'}\n`);
|
|
353
423
|
process.stdout.write(` Verifier ${node && await exists(join(controlRoot, '.triad-runtime', 'triad-verify.mjs')) ? 'OK' : 'incomplete'}\n`);
|
|
354
424
|
process.stdout.write(` Adapter ${manifest ? 'OK' : 'missing or different adapter'}\n`);
|
|
425
|
+
if (roleTargets.length) process.stdout.write(` Role agents ${await allExist(roleTargets) ? 'OK' : 'missing'}\n`);
|
|
426
|
+
if (triadSkillTargets.length) process.stdout.write(` Triad skill ${await allExist(triadSkillTargets) ? 'OK' : 'missing'}\n`);
|
|
427
|
+
process.stdout.write(` Verification ${capability?.verification?.selected_mode ?? 'unavailable'}${capability?.verification?.reason ? ` (${capability.verification.reason})` : ''}\n`);
|
|
355
428
|
process.stdout.write(` Team config ${team === 'invalid' ? 'invalid' : team ? 'OK' : 'not configured'}\n`);
|
|
356
429
|
process.stdout.write(` Evaluator+ ${team?.roles?.evaluator?.enabled === true ? 'configured' : 'not configured'}\n`);
|
|
357
|
-
const overlay = await overlayPlan(controlRoot).catch(() => null);
|
|
430
|
+
const overlay = await overlayPlan(controlRoot, team).catch(() => null);
|
|
358
431
|
process.stdout.write(` Instructions ${overlay ? overlay.action === 'update' ? 'managed' : `needs ${overlay.action}` : 'invalid managed block'}\n`);
|
|
359
432
|
const globalAgents = join(codexHome(), 'AGENTS.md');
|
|
360
433
|
if (await exists(globalAgents)) {
|
|
@@ -4,7 +4,11 @@ Install with `npx triad-plus init --host codex --control <path> --global`.
|
|
|
4
4
|
Open the control workspace and run `/prompts:triad <absolute-prd-path>`.
|
|
5
5
|
|
|
6
6
|
Codex uses role profiles for the Orchestrator, Developer, Reviewer, and optional
|
|
7
|
-
Evaluator+.
|
|
8
|
-
`SubagentStop`
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
Evaluator+. Verification uses explicit dispatch by default, even when a complete,
|
|
8
|
+
version-compatible async `SubagentStop` hook is installed. This keeps the normal
|
|
9
|
+
unattended path on the directly auditable verifier route.
|
|
10
|
+
|
|
11
|
+
The async hook remains available as an experimental opt-in by setting
|
|
12
|
+
`requested_mode=async_hook` in capability detection. Hook output is evidence
|
|
13
|
+
only; it never changes Triad state by itself. If the requested experimental hook
|
|
14
|
+
is unavailable, capability detection fails safe to explicit dispatch.
|
package/docs/compatibility.md
CHANGED
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
| Runtime | Install | Orchestrator | Developer | Reviewer | Evaluator+ | Verification dispatch | Hook support |
|
|
4
4
|
| --- | --- | --- | --- | --- | --- | --- | --- |
|
|
5
|
-
| Codex | Yes | Yes | Yes | Yes | Yes | Explicit
|
|
5
|
+
| Codex | Yes | Yes | Yes | Yes | Yes | Explicit dispatch by default; experimental async `SubagentStop` opt-in | Optional async `SubagentStop` |
|
|
6
6
|
| Claude Code | Yes | Yes | Yes | Yes | Yes | Explicit fallback; hook when validated | Optional `SubagentStop` |
|
|
7
7
|
| OpenCode | Yes | Yes | Yes | Yes | Yes | Explicit dispatch | No adapter hook |
|
|
8
8
|
| Antigravity | Yes | Yes | Yes | Yes | Yes | Explicit dispatch | No adapter hook |
|
|
9
9
|
| Hermes Agent | Yes | Yes | Yes | Yes | Yes | Explicit dispatch | No adapter hook |
|
|
10
|
+
| GitHub Copilot | Yes | Yes | Yes | Yes | Yes | Explicit dispatch | No adapter hook; desktop lifecycle validated |
|
|
10
11
|
|
|
11
12
|
All adapters consume the same role IDs and project-control records. Runtime
|
|
12
13
|
model/effort application is limited to the facilities each host exposes; the
|
package/docs/configuration.md
CHANGED
|
@@ -24,8 +24,9 @@ When enabled, Evaluator+ is automatically dispatched by the Orchestrator after
|
|
|
24
24
|
Triad reaches Reviewer approval. It receives a fresh post-run packet and cannot
|
|
25
25
|
change the closed Triad result. Set `enabled` to `false` to disable this default.
|
|
26
26
|
|
|
27
|
-
The runtime adapter is selected per installed control workspace (`--host`).
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
27
|
+
The runtime adapter is selected once per installed control workspace (`--host`).
|
|
28
|
+
All roles in that run use that adapter; Triad+ does not orchestrate roles across
|
|
29
|
+
different hosts. The team file records role-level models and effort, but an
|
|
30
|
+
adapter writes those into host-native profiles only where the selected host
|
|
31
|
+
supports that facility. A blank model means the host default. Never put tokens,
|
|
32
|
+
API keys, or private deployment data in this file.
|
package/docs/npx-installation.md
CHANGED
|
@@ -35,7 +35,8 @@ the control workspace `AGENTS.md`; existing instructions are preserved. Review
|
|
|
35
35
|
`doctor` output when host-level instructions impose a fixed identity, because a
|
|
36
36
|
higher-priority host policy can prevent the configured Orchestrator identity.
|
|
37
37
|
|
|
38
|
-
Supported hosts: `codex`, `opencode`, `claude-code`, `antigravity`, `hermes
|
|
38
|
+
Supported hosts: `codex`, `opencode`, `claude-code`, `antigravity`, `hermes`, and
|
|
39
|
+
the `copilot` adapter.
|
|
39
40
|
Use `--global` to install a host-level entry point where desired. The installer
|
|
40
41
|
refuses overwrites. If the control path is recognizably a product Git repository,
|
|
41
42
|
it stops unless `--allow-product-repo` is explicitly supplied after review.
|
|
@@ -26,10 +26,23 @@ PRD/card e definizione dei gate. “I test passano” detto dall’agente è una
|
|
|
26
26
|
l’output del verifier è evidence derivata dall’ambiente. Evidence fallita o
|
|
27
27
|
invalida non può essere trattata come pass.
|
|
28
28
|
|
|
29
|
+
Per Codex la modalità `auto` usa intenzionalmente il dispatch esplicito. Il
|
|
30
|
+
percorso asincrono `SubagentStop` resta disponibile solo come opt-in
|
|
31
|
+
sperimentale: gli hook producono evidence, mentre l’Orchestrator mantiene
|
|
32
|
+
l’autorità sull’avanzamento del loop.
|
|
33
|
+
|
|
34
|
+
L’adapter GitHub Copilot usa custom agent di progetto e la skill `triad`, con
|
|
35
|
+
dispatch esplicito della verification e senza hook di lifecycle. Lo smoke della
|
|
36
|
+
desktop app ha validato contesti distinti per i ruoli e la continuazione
|
|
37
|
+
unattended, oltre ai controlli CLI di asset e doctor.
|
|
38
|
+
|
|
29
39
|
Il Reviewer riceve card, diff, report Developer, rilievi precedenti ed evidence.
|
|
30
40
|
`rework` torna al Developer con una correzione delimitata; `blocked` richiede
|
|
31
41
|
all’Orchestrator di escalare la decisione. Le push normali possono essere autonome
|
|
32
|
-
dopo i goal dichiarati.
|
|
42
|
+
dopo i goal dichiarati. La consegna formale al proprietario è un gate distinto:
|
|
43
|
+
registra push finale, eventuale valutazione, handoff, stato finale della run e
|
|
44
|
+
prova pratica prima di dichiarare il progetto consegnato. Avvio e stop della demo
|
|
45
|
+
restano del proprietario.
|
|
33
46
|
|
|
34
47
|
## Evaluator+
|
|
35
48
|
|
|
@@ -48,3 +61,8 @@ eventuali report Evaluator+ e handoff in un workspace di controllo separato. Non
|
|
|
48
61
|
inserire token o segreti. Gli hook sono un’ottimizzazione, non autorità: il
|
|
49
62
|
dispatch esplicito della verification resta sempre disponibile. Vedi la
|
|
50
63
|
[matrice di compatibilità](compatibility.md).
|
|
64
|
+
|
|
65
|
+
Per ogni demo configurata, registra nel progetto e nell’handoff comando, URL
|
|
66
|
+
locale, modalità di accesso remoto e URL remoto. `localhost` è solo locale: non
|
|
67
|
+
va indicato a chi prova da remoto. Avvia il servizio soltanto su richiesta del
|
|
68
|
+
proprietario e verifica l’URL remoto dichiarato prima di comunicarlo.
|
package/docs/operating-guide.md
CHANGED
|
@@ -27,10 +27,23 @@ hash. A developer saying tests pass is a claim; verifier output is
|
|
|
27
27
|
environment-derived evidence. Failed or invalid evidence cannot be treated as a
|
|
28
28
|
pass.
|
|
29
29
|
|
|
30
|
+
Codex is intentionally an exception to automatic hook selection: its `auto`
|
|
31
|
+
mode uses explicit dispatch. The async `SubagentStop` route remains an
|
|
32
|
+
experimental opt-in; this does not change the rule that hooks produce evidence
|
|
33
|
+
and the Orchestrator governs progress.
|
|
34
|
+
|
|
35
|
+
The GitHub Copilot adapter uses project custom agents and the `triad` skill,
|
|
36
|
+
with explicit verification dispatch and no lifecycle hook. Its desktop-app
|
|
37
|
+
smoke has validated distinct role contexts and unattended continuation in
|
|
38
|
+
addition to the CLI asset and doctor checks.
|
|
39
|
+
|
|
30
40
|
The Reviewer sees the card, diff, Developer report, previous findings, and
|
|
31
41
|
verifier evidence. `rework` returns a bounded finding to Developer; `blocked`
|
|
32
42
|
asks the Orchestrator to escalate the stated decision. Normal pushes may happen
|
|
33
|
-
autonomously once declared goals pass.
|
|
43
|
+
autonomously once declared goals pass. A final owner delivery is a separate
|
|
44
|
+
closure gate: it records the final push, optional evaluation, handoff, final run
|
|
45
|
+
record, and practical test before the project is called delivered. Demo start and
|
|
46
|
+
stop remain owner-controlled.
|
|
34
47
|
|
|
35
48
|
## Evaluator+
|
|
36
49
|
|
|
@@ -48,3 +61,8 @@ immutable PRD, cards, assignments, evidence, review reports, optional Evaluator+
|
|
|
48
61
|
reports, and handoff in a separate project-control workspace. Never place tokens
|
|
49
62
|
or secrets there. Hooks are an optimization, not authority; explicit verification
|
|
50
63
|
is always the fallback. See the [compatibility matrix](compatibility.md).
|
|
64
|
+
|
|
65
|
+
For a configured demo service, record its command, local URL, remote-access mode,
|
|
66
|
+
and remote URL in the project and handoff. `localhost` is local-only; do not give
|
|
67
|
+
it to a remote tester as a reachable endpoint. Start the service only on the
|
|
68
|
+
owner's request and validate any declared remote URL before presenting it.
|
package/docs/runtimes.md
CHANGED
|
@@ -5,11 +5,12 @@ host-specific entry points and configuration behavior.
|
|
|
5
5
|
|
|
6
6
|
| Runtime | Prerequisite | Entry point | Verification | Hook limitation |
|
|
7
7
|
| --- | --- | --- | --- | --- |
|
|
8
|
-
| Codex | Codex CLI | `/prompts:triad` | Explicit dispatch; async hook
|
|
8
|
+
| Codex | Codex CLI | `/prompts:triad` | Explicit dispatch by default; experimental async hook opt-in | Hook is optional, requires trusted configuration, and is not selected by `auto`. |
|
|
9
9
|
| Claude Code | Claude Code CLI | `/triad` | Explicit dispatch; hook when validated | Hook is optional and requires trusted configuration. |
|
|
10
10
|
| OpenCode | OpenCode | `/triad` | Explicit dispatch | No Triad lifecycle hook. |
|
|
11
11
|
| Antigravity | Antigravity | `/triad` | Explicit dispatch | No Triad lifecycle hook. |
|
|
12
12
|
| Hermes Agent | Hermes Agent | `/triad` | Explicit dispatch | No Triad lifecycle hook. |
|
|
13
|
+
| GitHub Copilot | GitHub Copilot CLI and desktop app | `/triad` when the project skill is exposed, otherwise `/agent` → `triad-orchestrator` | Explicit dispatch | No adapter hook; desktop app supports the complete validated lifecycle. |
|
|
13
14
|
|
|
14
15
|
Install with `npx triad-plus init --host <runtime> --control <path>`; use
|
|
15
16
|
`--global` when you want host-level command assets. `doctor` reports the runtime
|
|
@@ -23,6 +24,28 @@ See the concise host guides for [Codex](codex-replication.md),
|
|
|
23
24
|
[Claude Code](claude-code-replication.md), [OpenCode](opencode-replication.md),
|
|
24
25
|
and [Antigravity](antigravity-replication.md).
|
|
25
26
|
|
|
27
|
+
## GitHub Copilot
|
|
28
|
+
|
|
29
|
+
The Copilot adapter uses the documented custom-agent and agent-skill primitives.
|
|
30
|
+
Install it with:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npx triad-plus init --host copilot --control /absolute/path/to/triad-control --global
|
|
34
|
+
npx triad-plus doctor --host copilot --control /absolute/path/to/triad-control
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Project agents are generated under `.github/agents/` and the Triad skill under
|
|
38
|
+
`.github/skills/triad/`; `--global` also installs the corresponding assets under
|
|
39
|
+
`~/.copilot/`. Open the control workspace in the Copilot desktop app. Use
|
|
40
|
+
`/triad <absolute-prd-path>` when the skill is available as a command; otherwise
|
|
41
|
+
select `triad-orchestrator` through `/agent` and provide the same request.
|
|
42
|
+
Verification is explicit by default and no Copilot lifecycle hook is required.
|
|
43
|
+
|
|
44
|
+
The desktop app has been validated with distinct Orchestrator, Developer, and
|
|
45
|
+
Reviewer contexts through a complete unattended Triad run. The adapter uses
|
|
46
|
+
explicit verification and has no lifecycle hook; the CLI and asset paths are
|
|
47
|
+
validated independently as well.
|
|
48
|
+
|
|
26
49
|
## OpenCode
|
|
27
50
|
|
|
28
51
|
Use the interactive OpenCode TUI for complete multi-step Triad runs. OpenCode
|
package/docs/verification.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Verification and evidence
|
|
2
2
|
|
|
3
|
+
## Repository skill bindings
|
|
4
|
+
|
|
5
|
+
When a target repository declares a skill router, the Orchestrator binds the
|
|
6
|
+
router, routed task skills, and completion skill to the Developer assignment as
|
|
7
|
+
worktree-relative paths and SHA-256 values. `triad-verify` checks that every
|
|
8
|
+
declared skill exists inside the declared worktree and still matches its bound
|
|
9
|
+
hash, then records the result in environment-derived verification evidence.
|
|
10
|
+
|
|
11
|
+
This proves the exact repository skill policy available to the attempt; it does
|
|
12
|
+
not claim to observe an LLM's private reasoning. Developer, Reviewer, and
|
|
13
|
+
Evaluator+ must separately report their use of the same bound skills, so a
|
|
14
|
+
missing or inconsistent attestation is visible to the Orchestrator.
|
|
15
|
+
|
|
3
16
|
An agent-reported claim is not the same as verification evidence. A Developer can
|
|
4
17
|
report the commands it ran; `triad-verify` independently observes declared
|
|
5
18
|
required `control-plane` gates and writes atomic evidence.
|
|
@@ -15,3 +28,15 @@ it does not itself approve, rework, or transition a run.
|
|
|
15
28
|
|
|
16
29
|
Evidence files and logs are diagnostics. Users normally need only the
|
|
17
30
|
Orchestrator's summary and the Reviewer verdict.
|
|
31
|
+
|
|
32
|
+
## Codex dispatch modes
|
|
33
|
+
|
|
34
|
+
Codex uses `explicit_dispatch` by default, including when a compatible async
|
|
35
|
+
`SubagentStop` hook is installed. This is the directly auditable path used for
|
|
36
|
+
normal unattended progress. The async hook remains supported as an experimental
|
|
37
|
+
opt-in with `requested_mode=async_hook`; it is selected only when the requested
|
|
38
|
+
hook is valid and available. If that requested hook is unavailable, capability
|
|
39
|
+
detection fails safe to `explicit_dispatch` and records the reason.
|
|
40
|
+
|
|
41
|
+
Hooks may produce evidence; the Orchestrator governs progress. The hook never
|
|
42
|
+
dispatches a Reviewer, selects a card, reopens a run, or performs repair.
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
# Triad Codex hook adapter
|
|
2
2
|
|
|
3
|
-
This adapter is for Codex CLI 0.148.0 or newer.
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
This adapter is for Codex CLI 0.148.0 or newer. Its normal verification route is
|
|
4
|
+
explicit dispatch by the Orchestrator. It also provides an experimental,
|
|
5
|
+
opt-in asynchronous command hook that can dispatch the Node verification runner
|
|
6
|
+
when a subagent whose type is exactly `triad_developer` stops. The hook writes
|
|
7
|
+
immutable evidence and has no authority to approve, reject, or transition a
|
|
8
|
+
feature card.
|
|
7
9
|
|
|
8
10
|
## Install only after verifying the local Codex schema
|
|
9
11
|
|
|
@@ -44,18 +46,24 @@ The JSON payload needs an `agent_id` matching an active assignment under
|
|
|
44
46
|
assignment and emits only structured evidence. The orchestrator still validates
|
|
45
47
|
the evidence against the active candidate before changing state.
|
|
46
48
|
|
|
47
|
-
##
|
|
49
|
+
## Default and experimental routes
|
|
48
50
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
+
Keep `project.control_plane.dispatch_mode: auto` for the normal Codex path and
|
|
52
|
+
run:
|
|
51
53
|
|
|
52
54
|
```bash
|
|
53
55
|
node /absolute/path/to/triad-plus-engineering-loop/runtime/triad-runtime-capabilities.mjs \
|
|
54
56
|
--hook-config /absolute/path/to/installed-triad-hooks.json
|
|
55
57
|
```
|
|
56
58
|
|
|
57
|
-
Store the JSON output at `.loop/runtime/capabilities.json`.
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
59
|
+
Store the JSON output at `.loop/runtime/capabilities.json`. For Codex, `auto`
|
|
60
|
+
always selects `explicit_dispatch`, including when the async hook is detected as
|
|
61
|
+
available. This is the default because the live Codex collaboration path did
|
|
62
|
+
not reliably emit `SubagentStop` during the validation runs.
|
|
63
|
+
|
|
64
|
+
To deliberately test the experimental route, pass
|
|
65
|
+
`--requested-mode async_hook`. A compatible Codex version and a valid trusted
|
|
66
|
+
hook configuration select `async_hook`; an unavailable or invalid hook falls
|
|
67
|
+
back to `explicit_dispatch` with a diagnostic reason. The Orchestrator follows
|
|
68
|
+
the snapshot and re-detects only before a new assignment or on resume after
|
|
69
|
+
runtime configuration changes.
|
package/package.json
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
"schema_version": 1,
|
|
4
4
|
"id": "codex",
|
|
5
5
|
"binary": "codex",
|
|
6
|
+
"verification": { "default_mode": "explicit_dispatch", "experimental_modes": ["async_hook"] },
|
|
6
7
|
"lifecycle": { "kind": "SubagentStop", "agent_type": "triad_developer", "requires_async": true, "minimum_version": "0.148.0" }
|
|
7
8
|
},
|
|
8
9
|
"opencode": { "schema_version": 1, "id": "opencode", "binary": "opencode", "lifecycle": null },
|
|
@@ -13,5 +14,12 @@
|
|
|
13
14
|
"lifecycle": { "kind": "SubagentStop", "agent_type": "triad-developer", "requires_async": false }
|
|
14
15
|
},
|
|
15
16
|
"antigravity": { "schema_version": 1, "id": "antigravity", "binary": "agy", "lifecycle": null },
|
|
16
|
-
"hermes": { "schema_version": 1, "id": "hermes", "binary": "hermes", "lifecycle": null }
|
|
17
|
+
"hermes": { "schema_version": 1, "id": "hermes", "binary": "hermes", "lifecycle": null },
|
|
18
|
+
"copilot": {
|
|
19
|
+
"schema_version": 1,
|
|
20
|
+
"id": "copilot",
|
|
21
|
+
"binary": "copilot",
|
|
22
|
+
"verification": { "default_mode": "explicit_dispatch" },
|
|
23
|
+
"lifecycle": null
|
|
24
|
+
}
|
|
17
25
|
}
|
|
@@ -104,12 +104,40 @@ const hookAvailable = lifecycleAvailable && hook.configured;
|
|
|
104
104
|
const explicitAvailable = Boolean(hostVersion && nodeVersion);
|
|
105
105
|
let selectedMode = "unavailable";
|
|
106
106
|
let reason = "verification_runtime_unavailable";
|
|
107
|
-
|
|
107
|
+
const defaultMode = adapter.verification?.default_mode ?? null;
|
|
108
|
+
const experimentalModes = new Set(adapter.verification?.experimental_modes ?? []);
|
|
109
|
+
const experimentalHookRequested = requestedMode === "async_hook" && experimentalModes.has("async_hook");
|
|
110
|
+
|
|
111
|
+
// An adapter may declare a safer automatic mode. This keeps runtime policy in
|
|
112
|
+
// adapter metadata instead of adding host-specific branches to the Core.
|
|
113
|
+
if (requestedMode === "auto" && defaultMode) {
|
|
114
|
+
if (defaultMode === "explicit_dispatch" && explicitAvailable) {
|
|
115
|
+
selectedMode = "explicit_dispatch";
|
|
116
|
+
reason = `${adapter.id}_default_explicit_dispatch`;
|
|
117
|
+
} else if (defaultMode === "async_hook" && hookAvailable) {
|
|
118
|
+
selectedMode = "async_hook";
|
|
119
|
+
reason = `${adapter.id}_default_async_hook`;
|
|
120
|
+
} else if (defaultMode === "hook_dispatch" && hookAvailable) {
|
|
121
|
+
selectedMode = "hook_dispatch";
|
|
122
|
+
reason = `${adapter.id}_default_hook_dispatch`;
|
|
123
|
+
} else if (explicitAvailable) {
|
|
124
|
+
selectedMode = "explicit_dispatch";
|
|
125
|
+
reason = `${adapter.id}_default_${defaultMode}_unavailable_using_explicit_dispatch`;
|
|
126
|
+
}
|
|
127
|
+
} else if (experimentalHookRequested && hookAvailable) {
|
|
128
|
+
selectedMode = "async_hook";
|
|
129
|
+
reason = "experimental_async_hook_requested";
|
|
130
|
+
} else if (experimentalHookRequested && explicitAvailable) {
|
|
131
|
+
selectedMode = "explicit_dispatch";
|
|
132
|
+
reason = "requested_async_hook_unavailable_using_explicit_dispatch";
|
|
133
|
+
} else if (hookAvailable && (requestedMode === "auto" || requestedMode === "async_hook" || requestedMode === "hook_dispatch") && (!lifecycle?.requires_async || requestedMode === "async_hook")) {
|
|
108
134
|
selectedMode = lifecycle.requires_async ? "async_hook" : "hook_dispatch";
|
|
109
135
|
reason = lifecycle.requires_async ? "validated_async_hook_available" : "validated_hook_dispatch_available";
|
|
110
136
|
} else if (explicitAvailable) {
|
|
111
137
|
selectedMode = "explicit_dispatch";
|
|
112
|
-
reason =
|
|
138
|
+
reason = requestedMode === "explicit_dispatch"
|
|
139
|
+
? "explicit_dispatch_requested"
|
|
140
|
+
: ["async_hook", "hook_dispatch"].includes(requestedMode)
|
|
113
141
|
? "requested_hook_unavailable_using_explicit_dispatch"
|
|
114
142
|
: "explicit_dispatch_available";
|
|
115
143
|
}
|
package/runtime/triad-verify.mjs
CHANGED
|
@@ -27,6 +27,26 @@ async function sha256File(value) {
|
|
|
27
27
|
return sha256(await readFile(value));
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
async function validateRepositorySkills(required, worktree) {
|
|
31
|
+
if (required === undefined) return { declared: false, skills: [] };
|
|
32
|
+
if (!Array.isArray(required) || required.length === 0) throw new Error("repository skill binding must declare at least one skill");
|
|
33
|
+
const root = await realpath(worktree);
|
|
34
|
+
const skills = [];
|
|
35
|
+
for (const item of required) {
|
|
36
|
+
if (!item || typeof item.path !== "string" || typeof item.sha256 !== "string") {
|
|
37
|
+
throw new Error("repository skill binding entries require path and sha256");
|
|
38
|
+
}
|
|
39
|
+
const candidate = path.resolve(root, item.path);
|
|
40
|
+
if (!candidate.startsWith(`${root}${path.sep}`)) throw new Error("repository skill path escapes worktree");
|
|
41
|
+
try { await access(candidate); }
|
|
42
|
+
catch { throw new Error(`repository skill missing: ${item.path}`); }
|
|
43
|
+
const actual = await sha256File(candidate);
|
|
44
|
+
if (actual !== item.sha256) throw new Error(`repository skill hash mismatch: ${item.path}`);
|
|
45
|
+
skills.push({ path: item.path, sha256: actual });
|
|
46
|
+
}
|
|
47
|
+
return { declared: true, skills };
|
|
48
|
+
}
|
|
49
|
+
|
|
30
50
|
function triggerFrom(payload) {
|
|
31
51
|
return {
|
|
32
52
|
event: payload.event ?? payload.hook_event_name ?? "manual",
|
|
@@ -100,6 +120,7 @@ async function main() {
|
|
|
100
120
|
await access(cardPath);
|
|
101
121
|
if ((await sha256File(prdPath)) !== assignment.expected_prd_sha256) throw new Error("PRD baseline hash mismatch");
|
|
102
122
|
if ((await sha256File(cardPath)) !== assignment.expected_card_sha256) throw new Error("feature card hash mismatch");
|
|
123
|
+
const repositorySkills = await validateRepositorySkills(assignment.required_repository_skills, worktree);
|
|
103
124
|
const before = await calculateCandidateFingerprint(worktree);
|
|
104
125
|
const branch = await worktreeBranch(worktree);
|
|
105
126
|
if (assignment.expected_branch && assignment.expected_branch !== branch) throw new Error("worktree branch does not match assignment");
|
|
@@ -128,6 +149,7 @@ async function main() {
|
|
|
128
149
|
candidate_fingerprint: before.value,
|
|
129
150
|
branch,
|
|
130
151
|
},
|
|
152
|
+
repository_skills: repositorySkills,
|
|
131
153
|
gates,
|
|
132
154
|
required_gates_passed: requiredGatesPassed,
|
|
133
155
|
status: candidateChanged ? "invalidated" : requiredGatesPassed ? "pass" : "fail",
|