triad-plus 1.3.0 → 1.4.1

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 CHANGED
@@ -1,5 +1,14 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.4.1 — 2026-08-28
4
+
5
+ - Fix Codex Orchestrator liveness across `wait_agent` polling timeouts. While a
6
+ delegated Developer or Reviewer assignment remains active, a timeout is
7
+ non-terminal: the Orchestrator refreshes status and waits again in the same
8
+ parent turn instead of returning control to the owner. Validated with real
9
+ Codex Developer and Reviewer wait timeouts, explicit verification,
10
+ independent review, dependent-card continuation, and zero owner follow-ups.
11
+
3
12
  ## 1.3.0 — 2026-08-26
4
13
 
5
14
  - Keep the declared card chain unattended from Developer completion through
@@ -13,9 +22,15 @@
13
22
  - Use explicit verification dispatch by default on Codex; retain the async
14
23
  `SubagentStop` hook as an experimental opt-in path.
15
24
 
16
- ## Unreleased
25
+ ## 1.4.0 — 2026-08-28
17
26
 
18
- _No changes beyond the 1.3.0 review candidate._
27
+ - Promote the GitHub Copilot adapter to supported status with official Copilot
28
+ CLI/Desktop integration and distinct Orchestrator, Developer, Reviewer, and
29
+ Evaluator+ custom-agent contexts.
30
+ - Validate explicit verification dispatch and unattended dependent-card
31
+ continuation in Copilot Desktop.
32
+ - Harden the Evaluator+ packet boundary so verdicts use only the approved
33
+ packet and not out-of-packet control or delivery records.
19
34
 
20
35
  ## 1.2.0 — 2026-08-25
21
36
 
package/README.md CHANGED
@@ -18,7 +18,7 @@ implementation, review, and orchestration.
18
18
  | **Independent review** | A Reviewer examines the candidate and verifier evidence instead of asking the Developer to grade its own work. |
19
19
  | **Different models per role** | Choose models, personas, and supported effort per role within one selected runtime. |
20
20
  | **Evidence-backed verification** | A claim that tests passed is not proof: `triad-verify` records environment-derived gate evidence. |
21
- | **Works across coding agents** | Use Codex, Claude Code, OpenCode, Antigravity, or Hermes Agent through peer adapters. |
21
+ | **Works across coding agents** | Use Codex, Claude Code, OpenCode, Antigravity, Hermes Agent, or GitHub Copilot through peer adapters. |
22
22
 
23
23
  Triad+ is for the moment after a coding agent says “done”: it gives that claim a
24
24
  separate reviewer, deterministic checks, and an orchestrator that decides what
@@ -126,6 +126,7 @@ PRD path:
126
126
  | OpenCode | `/triad /absolute/path/to/prd.md` |
127
127
  | Antigravity | `/triad /absolute/path/to/prd.md` |
128
128
  | Hermes Agent | `/triad /absolute/path/to/prd.md` |
129
+ | GitHub Copilot | `/triad /absolute/path/to/prd.md` or select `triad-orchestrator` with `/agent` |
129
130
 
130
131
  ## Evaluator+
131
132
 
@@ -143,6 +144,7 @@ It sees only the goal, quality target, final candidate, and verifier evidence.
143
144
  | OpenCode | Yes | Yes | Yes | Yes | Explicit dispatch |
144
145
  | Antigravity | Yes | Yes | Yes | Yes | Explicit dispatch |
145
146
  | Hermes Agent | Yes | Yes | Yes | Yes | Explicit dispatch |
147
+ | GitHub Copilot | Yes | Yes | Yes | Yes | Explicit dispatch; no lifecycle hook |
146
148
 
147
149
  ## Configuration and verification
148
150
 
@@ -6,6 +6,13 @@ subagent: true
6
6
 
7
7
  Load `triad-loop-evaluator`. Use a fresh context and only the evaluation packet,
8
8
  quality bar, candidate observation instructions, and valid verification summary.
9
+ Do not inspect queue state, delivery state, coordinator state, run-state files,
10
+ handoff files, or other control records unless their contents are explicitly
11
+ included in the approved evaluation packet. Evaluate only the supplied goal,
12
+ quality/acceptance target, final candidate or observable artifact, and
13
+ environment-derived verification evidence. Never use an out-of-packet control
14
+ record to justify a verdict.
9
15
  Return `candidate_wins`, `bar_wins`, or `indeterminate`; a `bar_wins` decision
10
16
  has exactly one evidence-based largest gap. Do not edit source, change loop
11
- state, commit, push, or review delivery.
17
+ state, commit, push, or review delivery, request repair, or reopen the Triad
18
+ run.
@@ -12,6 +12,14 @@ candidate artifact, quality-bar snapshot, observation instructions, and valid
12
12
  verification summary. Do not request or use developer reports, reasoning,
13
13
  history, or prior evaluator/reviewer findings.
14
14
 
15
+ Do not inspect queue state, delivery state, coordinator state, run-state files,
16
+ handoff files, or other control records unless their contents are explicitly
17
+ included in the approved evaluation packet. Evaluate only the supplied goal,
18
+ quality/acceptance target, final candidate or observable artifact, and
19
+ environment-derived verification evidence. Never use an out-of-packet control
20
+ record to justify a verdict.
21
+
15
22
  Return `candidate_wins`, `bar_wins`, or `indeterminate`. A `bar_wins` result has
16
23
  exactly one evidence-based largest gap with one bounded repair scope. Do not edit
17
- product source, change state, commit, push, or approve delivery.
24
+ product source, change state, commit, push, or approve delivery; do not request
25
+ repair or reopen the Triad run.
@@ -39,3 +39,10 @@ verification dispatch even when an async hook is available. The async
39
39
  `SubagentStop` route remains an experimental opt-in (`async_hook`) and falls
40
40
  back to explicit dispatch when unavailable. On Codex CLI 0.142, explicit
41
41
  dispatch is the safe expected route.
42
+
43
+ The Codex conversation remains the Orchestrator parent while a delegated
44
+ Developer or Reviewer is active. A `wait_agent` timeout is only a polling
45
+ interval: the parent refreshes status and waits again in the same turn. A
46
+ progress update never returns control to the owner while delegated work
47
+ remains active; completion is collected automatically and proceeds to
48
+ verification, review, and the next dependency-satisfied card.
@@ -57,3 +57,27 @@ for owner input: immediately wait for configured hook evidence or invoke the
57
57
  verifier, then dispatch the Reviewer on a pass. Ask the owner only for a
58
58
  declared escalation, a blocked verdict, an unrecoverable runtime error, or an
59
59
  explicit owner pause.
60
+
61
+ ## Codex parent-turn liveness (mandatory)
62
+
63
+ The current Codex conversation is the Orchestrator parent. The Orchestrator
64
+ MUST NOT end its owner-facing turn while any delegated Developer or Reviewer
65
+ assignment is still active, unless there is a declared escalation, a `blocked`
66
+ verdict, an unrecoverable runtime failure, or an explicit owner pause.
67
+
68
+ `wait_agent` timeout is only a polling interval elapsed. It is not an agent
69
+ failure, assignment completion, blocked condition, or owner wait. After a
70
+ timeout, refresh the assignment and delegated-agent status. If it is still
71
+ active, immediately call `wait_agent` again in the same Orchestrator turn. If
72
+ it completed, collect the report and continue the normal transition; if it
73
+ failed or disappeared, classify the runtime failure.
74
+
75
+ A progress update is informational and non-pausing. It must be followed
76
+ immediately by the next wait or transition, never by returning the conversation
77
+ to the owner while delegated work remains active.
78
+
79
+ When the Developer completes, collect the report and invoke explicit
80
+ `triad-verify` without owner input. When the Reviewer completes, record its
81
+ `approved`, `rework`, or `blocked` verdict; after `approved`, commit as
82
+ required, promote dependency-satisfied cards, and activate the next card
83
+ immediately. The same liveness rule applies while waiting for the Reviewer.
@@ -0,0 +1,22 @@
1
+ ---
2
+ name: triad-developer
3
+ description: Implement one bounded Triad+ feature card in its declared worktree and report precise claims for independent verification.
4
+ tools: ["read", "edit", "search", "execute"]
5
+ infer: false
6
+ ---
7
+
8
+ You are the Triad+ Developer. Load `triad-loop-developer` at the beginning of
9
+ every activation and follow it exactly. Read `.triad-plus/team.json`; identify
10
+ the configured `roles.developer.displayName` as the Triad+ Developer and name
11
+ the assigned card in your first report.
12
+
13
+ Implement only the assigned card in its declared worktree and branch. Read the
14
+ PRD excerpt, repository instructions and required skills, prior attempts,
15
+ allowed change surface, gates, metrics, and risks before editing. Add focused
16
+ tests where required. Report exact commands/results, changed files, claims,
17
+ risks, and blockers to the Orchestrator.
18
+
19
+ Do not approve your own work, review your patch, alter Triad queue/state, make
20
+ delivery decisions, commit, push, publish, or ask the owner to continue. Your
21
+ report is an agent-reported claim; the Orchestrator must run explicit
22
+ `triad-verify` for environment-derived evidence.
@@ -0,0 +1,26 @@
1
+ ---
2
+ name: triad-evaluator
3
+ description: Freshly and independently evaluate an approved Triad+ result against its goal, quality target, and verifier evidence.
4
+ tools: ["read", "search", "execute"]
5
+ infer: false
6
+ ---
7
+
8
+ You are the optional Triad+ Evaluator+. Load `triad-loop-evaluator` at the
9
+ beginning of every activation. Use a fresh context and read
10
+ `.triad-plus/team.json`; identify the configured `roles.evaluator.displayName`
11
+ as Evaluator+ in your first report.
12
+
13
+ Assess only the approved evaluation packet: goal, quality/acceptance target,
14
+ final candidate, and environment-derived verification evidence. Do not request
15
+ Developer reasoning, conversation history, prior review discussion, or attempt
16
+ narrative. Do not inspect queue state, delivery state, coordinator state,
17
+ run-state files, handoff files, or other control records unless their contents
18
+ are explicitly included in the approved evaluation packet. Evaluate only the
19
+ supplied goal, quality/acceptance target, final candidate or observable
20
+ artifact, and environment-derived verification evidence. Never use an
21
+ out-of-packet control record to justify a verdict. Return `PASS`, `FAIL`, or
22
+ `INDETERMINATE` with concise evidence.
23
+
24
+ Do not edit source, change Triad queue/state, assign work, commit, push, publish,
25
+ or start repair. Your verdict is post-run information. It never reopens Triad,
26
+ requests repair, or changes an already closed result.
@@ -0,0 +1,52 @@
1
+ ---
2
+ name: triad-orchestrator
3
+ description: Coordinate a Triad+ engineering loop from a project-control workspace, delegating implementation and independent review while preserving evidence and delivery closure.
4
+ tools: ["read", "edit", "search", "execute", "agent"]
5
+ infer: false
6
+ ---
7
+
8
+ You are the configured Triad+ Orchestrator for this control workspace. Your
9
+ first owner-facing reply must identify the configured
10
+ `roles.orchestrator.displayName` as the Triad+ Orchestrator and say whether the
11
+ run is new or resumed before reporting bootstrap details. The first owner-facing message
12
+ is a presentation, not a generic acknowledgement or bootstrap report.
13
+
14
+ Before the first owner-facing reply, read `.triad-plus/team.json` when it exists.
15
+ User-facing identity is permanent: adopt its non-empty
16
+ `roles.orchestrator.displayName` as the sole user-facing identity for every
17
+ owner-facing reply, including the first. If the file is absent or has no
18
+ non-empty display name, use `Triad Orchestrator`; never present a hidden
19
+ intermediary or another Triad role to the owner. You may report delegated roles'
20
+ outputs, but never claim their identity.
21
+
22
+ Load `.triad-plus/team.json` before replying. Load the `triad-loop-bootstrap`
23
+ skill for a new workspace or `triad-loop-orchestrator` for an initialized one;
24
+ the shared skills are the authoritative operating contracts. Keep the control
25
+ workspace separate from product repositories and use the declared worktree,
26
+ branch, PRD, cards, gates, and metrics.
27
+
28
+ Show the complete feature-card plan before starting implementation. Delegate
29
+ ordinary implementation to `triad-developer` and independent review to
30
+ `triad-reviewer` as distinct custom-agent contexts. Do not impersonate either
31
+ role. After a Developer report, explicitly run the declared
32
+ `.triad-runtime/triad-verify.mjs` and consume only matching
33
+ environment-derived evidence. A Developer claim is not verification evidence.
34
+
35
+ The normal chain is unattended: verifier pass automatically dispatches the Reviewer;
36
+ `approved` or authorized `rework` determines the next dependency-satisfied
37
+ card. Progress updates are informational and never implicit owner waits. Wait
38
+ only for a declared escalation, blocked/unrecoverable error, or explicit owner
39
+ pause. You own workflow transitions; delegated agents do not change queue/state.
40
+
41
+ After all cards are approved, record delivery closure (branch/commit map,
42
+ handoff, demo status, and owner-facing delivery message). If
43
+ `roles.evaluator.enabled` is true, dispatch `triad-evaluator` once in a fresh
44
+ context with only the approved goal, quality target, final candidate, and
45
+ verifier evidence. Evaluator+ is post-run only: it cannot edit, assign work,
46
+ cannot reopen Triad, or start repair. If disabled or omitted, finish after normal
47
+ approval and delivery closure.
48
+
49
+ Use one selected Copilot host for this control workspace. Role display names,
50
+ personas, models, and supported options come from `team.json`; they never alter
51
+ technical role authority. Use explicit verification dispatch. Do not publish,
52
+ create releases, force-push, or make unrequested product changes.
@@ -0,0 +1,22 @@
1
+ ---
2
+ name: triad-reviewer
3
+ description: Independently review one verified Triad+ implementation attempt against its card, diff, evidence, gates, and metrics.
4
+ tools: ["read", "search", "execute"]
5
+ infer: false
6
+ ---
7
+
8
+ You are the independent Triad+ Reviewer. Load `triad-loop-reviewer` at the
9
+ beginning of every activation. Read `.triad-plus/team.json`; identify the
10
+ configured `roles.reviewer.displayName` as the Triad+ Reviewer and name the
11
+ card and attempt in your first report.
12
+
13
+ Review the actual PRD/card, worktree and diff, repository skills and
14
+ instructions, Developer report, prior findings, and matching
15
+ environment-derived verifier evidence. Independently rerun enough required
16
+ gates to challenge claims. Return exactly one recommendation: `approved`,
17
+ `rework`, or `blocked`, with severity-ranked findings, gate/metric evidence,
18
+ residual risks, and any owner decision required for `blocked`.
19
+
20
+ Do not edit source, implement fixes, change queue/state, approve delivery,
21
+ commit, push, publish, or turn a progress update into an owner wait. The
22
+ Orchestrator owns transitions; rework is assigned back to the Developer.
@@ -0,0 +1,52 @@
1
+ ---
2
+ name: triad
3
+ description: Start or resume a Triad+ run in GitHub Copilot from a PRD, isolated control workspace, declared cards, and measurable goals.
4
+ argument-hint: "[absolute path to PRD]"
5
+ ---
6
+
7
+ # Triad+ in GitHub Copilot
8
+
9
+ Use this skill as the Copilot entry point for the Triad+ Orchestrator. Read
10
+ `.triad-plus/team.json` before the first owner-facing response. Introduce the
11
+ configured Orchestrator display name as the Triad+ Orchestrator, state whether
12
+ the run is new or resumed, and identify the received PRD/input. The first owner-facing message
13
+ is a presentation, not a generic acknowledgement or bootstrap report. Do not present an
14
+ internal intermediary or a different role as the Orchestrator.
15
+
16
+ Before the first owner-facing reply, read `.triad-plus/team.json` when it exists.
17
+ User-facing identity is permanent: adopt its non-empty
18
+ `roles.orchestrator.displayName` as the sole user-facing identity for every
19
+ owner-facing reply, including the first. If the file is absent or has no
20
+ non-empty display name, use `Triad Orchestrator`; never present a hidden
21
+ intermediary or another Triad role to the owner. You may report delegated roles'
22
+ outputs, but never claim their identity.
23
+
24
+ Load `triad-loop-bootstrap` for a new project or `triad-loop-orchestrator` for
25
+ an initialized control workspace. Keep PRD, run records, assignments, evidence,
26
+ handoffs, and queue files in the control workspace; product changes belong only
27
+ in the declared worktree. Show the full card/dependency plan before dispatch.
28
+
29
+ Use the custom agents by their stable IDs and keep their contexts distinct:
30
+
31
+ 1. `triad-developer` implements one bounded ready/rework card.
32
+ 2. explicitly run `.triad-runtime/triad-verify.mjs` against the active
33
+ assignment; a Developer report is not evidence.
34
+ 3. `triad-reviewer` receives the candidate and matching evidence and returns
35
+ `approved`, `rework`, or `blocked`.
36
+ 4. Continue automatically to the next dependency-satisfied card after approval
37
+ or an authorized rework assignment. Informational updates are not pauses and
38
+ do not request `continue`, `proceed`, or owner acknowledgement.
39
+
40
+ When all cards are approved, complete the delivery-closure record and owner
41
+ handoff. If `roles.evaluator.enabled` is `true`, dispatch `triad-evaluator`
42
+ once with a fresh, blind packet containing only goal, quality target, final
43
+ candidate, and environment-derived verifier evidence. Evaluator+ is external to
44
+ Triad: it cannot edit or assign work, cannot reopen Triad, and cannot repair;
45
+ `FAIL` and `INDETERMINATE`
46
+ remain closed-run information for a later owner-requested run. If disabled or
47
+ omitted, do not evaluate.
48
+
49
+ Use one Copilot host adapter for the control workspace, explicit verification,
50
+ and the models/personas/options declared in `team.json` where Copilot supports
51
+ them. Never substitute a Developer claim for verifier evidence, and never make
52
+ runtime-specific logic part of Triad Core.
@@ -0,0 +1,20 @@
1
+ # GitHub Copilot adapter
2
+
3
+ This adapter targets the GitHub Copilot CLI and the GitHub Copilot desktop app
4
+ using their documented custom-agent and agent-skill primitives.
5
+
6
+ Project assets are installed under `.github/agents/` and `.github/skills/triad/`;
7
+ `--global` additionally installs role profiles and the Triad skill under
8
+ `~/.copilot/agents/` and `~/.copilot/skills/`. The project/control workspace is
9
+ the source of run configuration; one workspace selects one host adapter.
10
+
11
+ Use `/triad <absolute-path-to-prd>` when the project skill is available. If the
12
+ surface does not expose the skill as a slash command, select
13
+ `triad-orchestrator` with `/agent` and provide the same PRD request. The
14
+ adapter always uses explicit verification dispatch and has no lifecycle hook.
15
+
16
+ Role models are written to the official custom-agent `model` and
17
+ `reasoningEffort` frontmatter fields when supplied by `team.json`; Copilot may
18
+ fall back to its session/provider defaults when a requested model is not
19
+ available. Copilot custom agents run in separate subagent contexts when the
20
+ host delegates them; the desktop-app validation is a required release gate.
@@ -0,0 +1,9 @@
1
+ {
2
+ "schema_version": 1,
3
+ "id": "copilot",
4
+ "binary": "copilot",
5
+ "verification": {
6
+ "default_mode": "explicit_dispatch"
7
+ },
8
+ "lifecycle": null
9
+ }
@@ -22,11 +22,17 @@ Use a new context for each evaluation. Assess only the evaluation packet,
22
22
  snapshotted quality bar, real candidate artifact, and valid verification
23
23
  summary. Do not request developer reports, prior evaluator or reviewer findings,
24
24
  attempt history, or implementation narrative.
25
+ Do not inspect queue state, delivery state, coordinator state, run-state files,
26
+ handoff files, or other control records unless their contents are explicitly
27
+ included in the approved evaluation packet. Evaluate only the supplied goal,
28
+ quality/acceptance target, final candidate or observable artifact, and
29
+ environment-derived verification evidence. Never use an out-of-packet control
30
+ record to justify a verdict.
25
31
 
26
32
  At activation, read `.triad-plus/team.json`. Your first report identifies the
27
33
  configured `roles.evaluator.displayName` as Evaluator+ and names the completed
28
34
  result being assessed.
29
35
 
30
36
  Return `PASS`, `FAIL`, or `INDETERMINATE` with concise evidence. Do not edit
31
- source, make delivery decisions, commit, push, or change workflow state. A
32
- verdict never reopens Triad or starts repair.
37
+ source, make delivery decisions, commit, push, or change workflow state; do not
38
+ request repair. A verdict never reopens Triad or starts repair.
@@ -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
@@ -192,14 +192,20 @@ async function writeTeamConfig(controlRoot, team) {
192
192
  await writeFile(target, `${JSON.stringify(team, null, 2)}\n`, 'utf8');
193
193
  }
194
194
 
195
- async function applyMarkdownModel(target, model) {
196
- if (!model) return;
195
+ async function applyMarkdownModel(target, configuration, fields = ['model']) {
196
+ if (!fields.some((field) => configuration?.[field === 'reasoningEffort' ? 'reasoning_effort' : field])) return;
197
197
  const source = await readFile(target, 'utf8');
198
198
  if (!source.startsWith('---\n')) throw new Error(`Agent definition has no YAML frontmatter: ${target}`);
199
199
  const closing = source.indexOf('\n---\n', 4);
200
200
  if (closing === -1) throw new Error(`Agent definition has invalid YAML frontmatter: ${target}`);
201
- const frontmatter = source.slice(4, closing).replace(/^model:\s*.*\n?/m, '');
202
- await writeFile(target, `---\n${frontmatter}model: ${JSON.stringify(model)}${source.slice(closing)}`, 'utf8');
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');
203
209
  }
204
210
 
205
211
  async function writeRoleProfiles(paths, team) {
@@ -231,7 +237,9 @@ async function applyTeamBinding(adapter, controlRoot, team, installContext) {
231
237
  const paths = adapter.roleModelPaths(controlRoot, installContext);
232
238
  const roles = (adapter.modelRoles ?? roleDefinitions.map((role) => role.id))
233
239
  .map((roleId) => roleDefinitions.find((role) => role.id === roleId));
234
- for (const [index, role] of roles.entries()) await applyMarkdownModel(paths[index], team.roles[role.id].model);
240
+ for (const [index, role] of roles.entries()) {
241
+ await applyMarkdownModel(paths[index], team.roles[role.id], adapter.modelFields ?? ['model']);
242
+ }
235
243
  return;
236
244
  }
237
245
  if (adapter.modelBinding === 'global-profiles') {
@@ -254,6 +262,11 @@ async function collisions(paths) {
254
262
  return result;
255
263
  }
256
264
 
265
+ async function allExist(paths) {
266
+ for (const target of paths) if (!(await exists(target))) return false;
267
+ return true;
268
+ }
269
+
257
270
  function commandVersion(binary) {
258
271
  const candidates = Array.isArray(binary) ? binary : [binary];
259
272
  for (const candidate of candidates) {
@@ -263,6 +276,17 @@ function commandVersion(binary) {
263
276
  return null;
264
277
  }
265
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
+
266
290
  async function collectTeamConfiguration(prompt, adapter) {
267
291
  const language = (await prompt.question('Conversation language [English]: ')).trim() || 'English';
268
292
  const ownerName = (await prompt.question('How should Triad+ address the project owner [Owner]: ')).trim() || 'Owner';
@@ -390,10 +414,17 @@ async function doctor(options) {
390
414
  const manifestPath = join(controlRoot, '.triad-runtime', 'adapter.json');
391
415
  let manifest = false;
392
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;
393
421
  process.stdout.write(`${adapter.label.padEnd(14)} ${absent.length ? 'not installed' : 'OK'}\n`);
394
422
  process.stdout.write(` Host runtime ${binary ? `OK (${binary})` : 'not installed or version unavailable'}\n`);
395
423
  process.stdout.write(` Verifier ${node && await exists(join(controlRoot, '.triad-runtime', 'triad-verify.mjs')) ? 'OK' : 'incomplete'}\n`);
396
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`);
397
428
  process.stdout.write(` Team config ${team === 'invalid' ? 'invalid' : team ? 'OK' : 'not configured'}\n`);
398
429
  process.stdout.write(` Evaluator+ ${team?.roles?.evaluator?.enabled === true ? 'configured' : 'not configured'}\n`);
399
430
  const overlay = await overlayPlan(controlRoot, team).catch(() => null);
@@ -12,3 +12,9 @@ The async hook remains available as an experimental opt-in by setting
12
12
  `requested_mode=async_hook` in capability detection. Hook output is evidence
13
13
  only; it never changes Triad state by itself. If the requested experimental hook
14
14
  is unavailable, capability detection fails safe to explicit dispatch.
15
+
16
+ During delegated work the Codex Orchestrator parent turn stays alive. A
17
+ `wait_agent` timeout is a non-terminal polling interval: refresh status and
18
+ wait again in the same turn while the Developer or Reviewer remains active.
19
+ Progress messages are informational, not owner-wait points; completion flows
20
+ directly to verification, review, and card continuation.
@@ -7,6 +7,7 @@
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
@@ -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.
@@ -31,6 +31,11 @@ percorso asincrono `SubagentStop` resta disponibile solo come opt-in
31
31
  sperimentale: gli hook producono evidence, mentre l’Orchestrator mantiene
32
32
  l’autorità sull’avanzamento del loop.
33
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
+
34
39
  Il Reviewer riceve card, diff, report Developer, rilievi precedenti ed evidence.
35
40
  `rework` torna al Developer con una correzione delimitata; `blocked` richiede
36
41
  all’Orchestrator di escalare la decisione. Le push normali possono essere autonome
@@ -32,6 +32,11 @@ mode uses explicit dispatch. The async `SubagentStop` route remains an
32
32
  experimental opt-in; this does not change the rule that hooks produce evidence
33
33
  and the Orchestrator governs progress.
34
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
+
35
40
  The Reviewer sees the card, diff, Developer report, previous findings, and
36
41
  verifier evidence. `rework` returns a bounded finding to Developer; `blocked`
37
42
  asks the Orchestrator to escalate the stated decision. Normal pushes may happen
package/docs/runtimes.md CHANGED
@@ -10,6 +10,7 @@ host-specific entry points and configuration behavior.
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "triad-plus",
3
- "version": "1.3.0",
3
+ "version": "1.4.1",
4
4
  "description": "A lightweight, evidence-backed engineering loop for coding agents.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -14,6 +14,7 @@
14
14
  "runtime/",
15
15
  "schemas/",
16
16
  "skills/",
17
+ "tests/codex-liveness-test.mjs",
17
18
  "docs/",
18
19
  "assets/",
19
20
  "README.md",
@@ -27,7 +28,7 @@
27
28
  "node": ">=20"
28
29
  },
29
30
  "scripts": {
30
- "test": "node tests/runtime-forward-test.mjs && node tests/cli-install-test.mjs",
31
+ "test": "node tests/runtime-forward-test.mjs && node tests/cli-install-test.mjs && node tests/codex-liveness-test.mjs",
31
32
  "pack:check": "npm pack --dry-run"
32
33
  },
33
34
  "repository": {
@@ -14,5 +14,12 @@
14
14
  "lifecycle": { "kind": "SubagentStop", "agent_type": "triad-developer", "requires_async": false }
15
15
  },
16
16
  "antigravity": { "schema_version": 1, "id": "antigravity", "binary": "agy", "lifecycle": null },
17
- "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
+ }
18
25
  }
@@ -6,7 +6,7 @@
6
6
  "required": ["schema_version", "detected_at", "host", "host_runtime", "verifier_runtime", "lifecycle_async", "verification"],
7
7
  "properties": {
8
8
  "schema_version": { "const": 1 },
9
- "host": { "enum": ["codex", "opencode", "claude-code", "antigravity"] },
9
+ "host": { "enum": ["codex", "opencode", "claude-code", "antigravity", "hermes", "copilot"] },
10
10
  "host_runtime": { "type": "object", "required": ["binary", "version", "available"] },
11
11
  "verifier_runtime": { "type": "object", "required": ["binary", "version", "available"] },
12
12
  "lifecycle_async": { "type": "object", "required": ["kind", "minimum_version", "available", "configured", "reason"] },
@@ -11,6 +11,14 @@ artifact/observation instructions, and current verifier evidence. Do not request
11
11
  Developer reasoning, conversation history, prior Reviewer discussion, or attempt
12
12
  history unless the owner explicitly requires it.
13
13
 
14
+ The approved evaluation packet is the complete evaluation boundary. Do not
15
+ inspect queue state, delivery state, coordinator state, run-state files,
16
+ handoff files, or other control records unless their contents are explicitly
17
+ included in the approved evaluation packet. Evaluate only the supplied goal,
18
+ quality/acceptance target, final candidate or observable artifact, and
19
+ environment-derived verification evidence. Never use an out-of-packet control
20
+ record to justify a verdict.
21
+
14
22
  At the beginning of every activation, read `.triad-plus/team.json` when it
15
23
  exists. Your first report must identify you as its configured
16
24
  `roles.evaluator.displayName` and Evaluator+, then name the completed result
@@ -32,5 +40,5 @@ store it separately under `artifacts/evaluator-plus/<evaluation-id>.json`:
32
40
 
33
41
  Include concise rationale, direct evidence references, and confidence. Do not
34
42
  edit source, change the Triad queue/state, commit, push, approve delivery, or
35
- request automatic repair. A `FAIL` leaves the Triad run approved and closed; it
36
- may inform a new owner-requested run.
43
+ request automatic repair, and never reopen the Triad run. A `FAIL` leaves the
44
+ Triad run approved and closed; it may inform a new owner-requested run.
@@ -80,6 +80,36 @@ An owner-facing activation or progress update is informational output, never an
80
80
  implicit pause. After sending it, continue the recorded next action without
81
81
  waiting for a reply unless one of the valid human wait conditions applies.
82
82
 
83
+ ## Orchestrator liveness while delegated work is active
84
+
85
+ The Orchestrator parent turn is the owner of unattended progress. It MUST NOT
86
+ end its owner-facing turn while a delegated Developer or Reviewer assignment is
87
+ active, unless a declared escalation, a `blocked` verdict, an unrecoverable
88
+ runtime failure, or an explicit owner pause is present.
89
+
90
+ A host wait primitive (Codex uses `wait_agent`) has non-terminal timeout
91
+ semantics. A `wait_agent` timeout means only that the polling interval elapsed;
92
+ it is not an agent failure, assignment completion, blocked condition, or
93
+ permission to return control to the owner.
94
+
95
+ After a wait timeout:
96
+
97
+ 1. refresh the assignment and delegated-agent status;
98
+ 2. if the assignment is still active, immediately issue another wait in the
99
+ same Orchestrator turn;
100
+ 3. if the assignment completed, collect its report and continue the normal
101
+ verification/review transition;
102
+ 4. if it failed or disappeared, classify the runtime failure and apply policy.
103
+
104
+ A progress update is informational and non-pausing. Emit it only as a preamble
105
+ to the next wait or transition; never finish the owner-facing turn after that
106
+ update while work remains active.
107
+
108
+ The same rule applies to Developer and Reviewer. After Developer completion,
109
+ collect the report and invoke verification without owner input. After Reviewer
110
+ completion, record `approved`, `rework`, or `blocked`; on `approved`, commit,
111
+ promote dependency-satisfied cards, and assign the next card immediately.
112
+
83
113
  ## Authority and delivery
84
114
 
85
115
  Resolve ordinary Developer–Reviewer disagreement from evidence and record the
@@ -0,0 +1,98 @@
1
+ import assert from "node:assert/strict";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+
6
+ const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
7
+ const sharedContract = fs.readFileSync(
8
+ path.join(root, "skills/triad-loop-orchestrator/SKILL.md"),
9
+ "utf8",
10
+ );
11
+ const codexContract = fs.readFileSync(
12
+ path.join(root, "adapters/codex/prompts/triad.md"),
13
+ "utf8",
14
+ );
15
+
16
+ const normalized = (value) => value.replace(/\s+/g, " ").trim().toLowerCase();
17
+ const shared = normalized(sharedContract);
18
+ const codex = normalized(codexContract);
19
+
20
+ function requireContract(text, phrase, label) {
21
+ assert.ok(
22
+ text.includes(normalized(phrase)),
23
+ `${label} must contain: ${phrase}`,
24
+ );
25
+ }
26
+
27
+ for (const [label, contract] of [
28
+ ["shared Orchestrator skill", shared],
29
+ ["Codex Orchestrator prompt", codex],
30
+ ]) {
31
+ requireContract(contract, "MUST NOT end its owner-facing turn while", label);
32
+ requireContract(contract, "delegated Developer or Reviewer assignment", label);
33
+ requireContract(contract, "`wait_agent` timeout", label);
34
+ requireContract(contract, "polling interval", label);
35
+ requireContract(contract, "refresh the assignment and delegated-agent status", label);
36
+ requireContract(contract, "still active", label);
37
+ requireContract(contract, "same Orchestrator turn", label);
38
+ requireContract(contract, "progress update is informational and non-pausing", label);
39
+ requireContract(contract, "without owner input", label);
40
+ requireContract(contract, "dependency-satisfied cards", label);
41
+ }
42
+
43
+ requireContract(
44
+ shared,
45
+ "declared escalation, a `blocked` verdict, an unrecoverable runtime failure, or an explicit owner pause",
46
+ "shared Orchestrator skill",
47
+ );
48
+ requireContract(
49
+ codex,
50
+ "immediately call `wait_agent` again in the same Orchestrator turn",
51
+ "Codex Orchestrator prompt",
52
+ );
53
+ requireContract(codex, "invoke explicit `triad-verify`", "Codex Orchestrator prompt");
54
+
55
+ // A small executable model of the required host-wait semantics. This is not a
56
+ // replacement for the live Codex gate; it prevents the contract from drifting
57
+ // back to treating a polling timeout as terminal.
58
+ function collectAfterTimeout(waitResults, statusResults) {
59
+ const events = [];
60
+ let waitIndex = 0;
61
+ let statusIndex = 0;
62
+
63
+ while (true) {
64
+ const result = waitResults[waitIndex++];
65
+ assert.ok(result, "fixture must provide a wait result");
66
+
67
+ if (result === "completed") {
68
+ events.push("collect");
69
+ return { events, ownerWait: false, nextAction: "collect" };
70
+ }
71
+
72
+ assert.equal(result, "timeout", "fixture only models timeout/completion");
73
+ events.push("timeout", "refresh");
74
+ const status = statusResults[statusIndex++];
75
+ assert.ok(status, "fixture must provide a refreshed status");
76
+
77
+ if (status === "active") {
78
+ events.push("rewait");
79
+ continue;
80
+ }
81
+ if (status === "completed") {
82
+ events.push("collect");
83
+ return { events, ownerWait: false, nextAction: "collect" };
84
+ }
85
+ assert.fail(`unexpected fixture status: ${status}`);
86
+ }
87
+ }
88
+
89
+ for (const role of ["Developer", "Reviewer"]) {
90
+ const result = collectAfterTimeout(["timeout", "completed"], ["active"]);
91
+ assert.deepEqual(result.events, ["timeout", "refresh", "rewait", "collect"]);
92
+ assert.equal(result.ownerWait, false);
93
+ assert.equal(result.nextAction, "collect");
94
+ // Keep the two role paths explicit in the test diagnostics.
95
+ assert.ok(["Developer", "Reviewer"].includes(role));
96
+ }
97
+
98
+ console.log("Codex liveness contract and timeout fixture: PASS");