triad-plus 1.2.0 → 1.3.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.
Files changed (37) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +8 -5
  3. package/adapters/antigravity/.agents/agents/triad-orchestrator/agent.md +7 -5
  4. package/adapters/antigravity/.agents/skills/triad/SKILL.md +6 -5
  5. package/adapters/claude-code/.claude/commands/triad.md +6 -5
  6. package/adapters/codex/README.md +6 -4
  7. package/adapters/codex/prompts/triad.md +27 -9
  8. package/adapters/codex/runtime.json +4 -0
  9. package/adapters/hermes/skills/triad/SKILL.md +6 -5
  10. package/adapters/opencode/.opencode/agents/triad-developer.md +4 -0
  11. package/adapters/opencode/.opencode/agents/triad-evaluator.md +8 -5
  12. package/adapters/opencode/.opencode/agents/triad-orchestrator.md +7 -5
  13. package/adapters/opencode/.opencode/agents/triad-reviewer.md +4 -0
  14. package/adapters/opencode/.opencode/commands/triad.md +6 -5
  15. package/bin/triad-plus.js +55 -13
  16. package/docs/codex-replication.md +8 -4
  17. package/docs/compatibility.md +1 -1
  18. package/docs/configuration.md +6 -5
  19. package/docs/operating-guide.it.md +14 -1
  20. package/docs/operating-guide.md +14 -1
  21. package/docs/runtimes.md +1 -1
  22. package/docs/verification.md +25 -0
  23. package/integrations/codex/README.md +20 -12
  24. package/package.json +1 -1
  25. package/runtime/legacy-adapters.json +1 -0
  26. package/runtime/triad-runtime-capabilities.mjs +30 -2
  27. package/runtime/triad-verify.mjs +22 -0
  28. package/skills/triad-loop-bootstrap/SKILL.md +3 -1
  29. package/skills/triad-loop-bootstrap/assets/loop-template/feature-card.template.md +6 -0
  30. package/skills/triad-loop-bootstrap/assets/loop-template/handoff-report.template.md +8 -0
  31. package/skills/triad-loop-bootstrap/assets/loop-template/run-state.yaml +8 -0
  32. package/skills/triad-loop-bootstrap/assets/loop-template/runtime/assignments/assignment.template.json +10 -0
  33. package/skills/triad-loop-bootstrap/assets/project.yaml +10 -0
  34. package/skills/triad-loop-developer/SKILL.md +12 -0
  35. package/skills/triad-loop-evaluator/SKILL.md +12 -0
  36. package/skills/triad-loop-orchestrator/SKILL.md +65 -11
  37. package/skills/triad-loop-reviewer/SKILL.md +13 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.3.0 — 2026-08-26
4
+
5
+ - Keep the declared card chain unattended from Developer completion through
6
+ verification, review, and the next dependency-satisfied card.
7
+ - Bind repository skill files and hashes to attempts as verifier evidence, and
8
+ make role activations observable to the owner.
9
+ - Require a formal owner delivery closure: final push evidence, optional
10
+ Evaluator+ report, handoff, run-record update, and practical-test guidance.
11
+ - Make local versus remote demo access explicit; `localhost` is never reported
12
+ as a remote endpoint.
13
+ - Use explicit verification dispatch by default on Codex; retain the async
14
+ `SubagentStop` hook as an experimental opt-in path.
15
+
16
+ ## Unreleased
17
+
18
+ _No changes beyond the 1.3.0 review candidate._
19
+
3
20
  ## 1.2.0 — 2026-08-25
4
21
 
5
22
  - Add safe dry-run/apply upgrades for existing project-control workspaces.
package/README.md CHANGED
@@ -16,7 +16,7 @@ implementation, review, and orchestration.
16
16
  | | What changes |
17
17
  | --- | --- |
18
18
  | **Independent review** | A Reviewer examines the candidate and verifier evidence instead of asking the Developer to grade its own work. |
19
- | **Different models per role** | Choose a runtime, model, persona, and supported effort for each role. |
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
21
  | **Works across coding agents** | Use Codex, Claude Code, OpenCode, Antigravity, or Hermes Agent through peer adapters. |
22
22
 
@@ -138,7 +138,7 @@ It sees only the goal, quality target, final candidate, and verifier evidence.
138
138
 
139
139
  | Runtime | Orchestrator | Developer | Reviewer | Evaluator+ | Verification dispatch |
140
140
  | --- | --- | --- | --- | --- | --- |
141
- | Codex | Yes | Yes | Yes | Yes | Explicit fallback; validated async hook when available |
141
+ | Codex | Yes | Yes | Yes | Yes | Explicit dispatch by default; experimental async hook opt-in |
142
142
  | Claude Code | Yes | Yes | Yes | Yes | Explicit fallback; validated hook when available |
143
143
  | OpenCode | Yes | Yes | Yes | Yes | Explicit dispatch |
144
144
  | Antigravity | Yes | Yes | Yes | Yes | Explicit dispatch |
@@ -147,9 +147,12 @@ It sees only the goal, quality target, final candidate, and verifier evidence.
147
147
  ## Configuration and verification
148
148
 
149
149
  The installer writes `.triad-plus/team.json`, which separates stable role IDs
150
- from user-facing names, personas, models, and supported effort/options. Roles
151
- can use different models and, where the adapter supports it, different hosts.
152
- Read [configuration](docs/configuration.md) and [runtime details](docs/runtimes.md).
150
+ from user-facing names, personas, models, and supported effort/options. A control
151
+ workspace selects one host adapter for the whole run; its roles can use different
152
+ models where that host supports them. To use another host, create another control
153
+ workspace with that adapter. Triad+ does not dispatch different roles across
154
+ different hosts in one run. Read [configuration](docs/configuration.md) and
155
+ [runtime details](docs/runtimes.md).
153
156
 
154
157
  `triad-verify` validates assignment/candidate binding, runs declared
155
158
  control-plane gates, detects mutation, and writes evidence. It supports the
@@ -19,11 +19,13 @@ non-empty display name, use `Triad Orchestrator`; never present a hidden
19
19
  intermediary or another Triad role to the owner. You may report delegated roles'
20
20
  outputs, but never claim their identity.
21
21
 
22
- After loading this configuration, the first owner-facing message of every Triad+
23
- run must begin with a concise introduction: "I am <displayName>, the Triad+
24
- Orchestrator for this run." Localize it to the configured interaction language,
25
- then state in one sentence whether the run is new or resumed and what input was
26
- received. Do this before delegating, discussing artifacts, or asking questions.
22
+ If the current Triad+ invocation has not yet introduced the Orchestrator, its
23
+ first owner-facing message is a presentation, not a generic acknowledgement or
24
+ bootstrap report. Before any other owner-facing content, begin with a
25
+ first-person sentence that includes `<displayName>` and "Triad+ Orchestrator",
26
+ localized to the configured interaction language; then state whether the run is
27
+ new or resumed and what input was received. If the entry point already made that
28
+ presentation for this invocation, do not repeat it.
27
29
 
28
30
  Detect the runtime with `.triad-runtime/triad-runtime-capabilities.mjs --host
29
31
  antigravity`. Use explicit verification dispatch unless the recorded capability
@@ -18,11 +18,12 @@ non-empty display name, use `Triad Orchestrator`; never present a hidden
18
18
  intermediary or another Triad role to the owner. You may report delegated roles'
19
19
  outputs, but never claim their identity.
20
20
 
21
- After loading this configuration, the first owner-facing message of every Triad+
22
- run must begin with a concise introduction: "I am <displayName>, the Triad+
23
- Orchestrator for this run." Localize it to the configured interaction language,
24
- then state in one sentence whether the run is new or resumed and what input was
25
- received. Do this before delegating, discussing artifacts, or asking questions.
21
+ The first owner-facing message of a Triad+ invocation is a presentation, not a
22
+ generic acknowledgement or bootstrap report. Before any other owner-facing
23
+ content, begin with a first-person sentence that includes `<displayName>` and
24
+ "Triad+ Orchestrator", localized to the configured interaction language; then
25
+ state whether the run is new or resumed and what input was received. Do not
26
+ repeat this presentation when an already-introduced run loads its role skill.
26
27
 
27
28
  Before work, compare every role model that the host exposes with the recorded
28
29
  contract. If Antigravity does not expose a model identity, say that it cannot be
@@ -26,11 +26,12 @@ non-empty display name, use `Triad Orchestrator`; never present a hidden
26
26
  intermediary or another Triad role to the owner. You may report delegated roles'
27
27
  outputs, but never claim their identity.
28
28
 
29
- After loading this configuration, the first owner-facing message of every Triad+
30
- run must begin with a concise introduction: "I am <displayName>, the Triad+
31
- Orchestrator for this run." Localize it to the configured interaction language,
32
- then state in one sentence whether the run is new or resumed and what input was
33
- received. Do this before delegating, discussing artifacts, or asking questions.
29
+ The first owner-facing message of a Triad+ invocation is a presentation, not a
30
+ generic acknowledgement or bootstrap report. Before any other owner-facing
31
+ content, begin with a first-person sentence that includes `<displayName>` and
32
+ "Triad+ Orchestrator", localized to the configured interaction language; then
33
+ state whether the run is new or resumed and what input was received. Do not
34
+ repeat this presentation when an already-introduced run loads its role skill.
34
35
 
35
36
  Delegate implementation to `triad-developer` and review to `triad-reviewer`.
36
37
  After Triad approval, automatically invoke fresh `triad-evaluator` when
@@ -33,7 +33,9 @@ Before it starts work, configure the four named role profiles described in
33
33
  does not edit profile or model configuration, because their availability and
34
34
  model routing are host-owner decisions.
35
35
 
36
- At bootstrap and resume, the Orchestrator runs the runtime capability detector.
37
- It chooses the configured Codex asynchronous hook only when that hook is really
38
- available; otherwise it performs explicit verification dispatch. On Codex CLI
39
- 0.142, explicit dispatch is the safe expected route.
36
+ At bootstrap and resume, the Orchestrator runs the runtime capability detector
37
+ with the project's `control_plane.dispatch_mode`. Codex `auto` selects explicit
38
+ verification dispatch even when an async hook is available. The async
39
+ `SubagentStop` route remains an experimental opt-in (`async_hook`) and falls
40
+ back to explicit dispatch when unavailable. On Codex CLI 0.142, explicit
41
+ dispatch is the safe expected route.
@@ -8,10 +8,13 @@ $ARGUMENTS
8
8
 
9
9
  Act as the Triad Orchestrator. Load `triad-loop-bootstrap` for a new project or
10
10
  `triad-loop-orchestrator` for an initialized project. At bootstrap and resume,
11
- run `.triad-runtime/triad-runtime-capabilities.mjs --adapter .triad-runtime/adapter.json` and record the
12
- result. Follow its selected verification mode: use `async_hook` only when the
13
- installed Codex lifecycle hook has been configured and verified; otherwise
14
- explicitly invoke the verifier after Developer completion.
11
+ read `project.control_plane.dispatch_mode` (defaulting to `auto`) and pass it as
12
+ `--requested-mode` to `.triad-runtime/triad-runtime-capabilities.mjs --adapter
13
+ .triad-runtime/adapter.json`; when `.codex/hooks.json` exists, pass it as
14
+ `--hook-config .codex/hooks.json`. Record the result. Codex `auto` selects
15
+ `explicit_dispatch`; use `async_hook` only when `dispatch_mode: async_hook` was
16
+ explicitly configured and the installed Codex lifecycle hook has been validated.
17
+ Otherwise explicitly invoke the verifier after Developer completion.
15
18
 
16
19
  If `.triad-plus/team.json` exists, load it before replying. Use its interaction
17
20
  language, owner address, display names, personas, and model contract in communication;
@@ -26,11 +29,13 @@ non-empty display name, use `Triad Orchestrator`; never present a hidden
26
29
  intermediary or another Triad role to the owner. You may report delegated roles'
27
30
  outputs, but never claim their identity.
28
31
 
29
- After loading this configuration, the first owner-facing message of every Triad+
30
- run must begin with a concise introduction: "I am <displayName>, the Triad+
31
- Orchestrator for this run." Localize it to the configured interaction language,
32
- then state in one sentence whether the run is new or resumed and what input was
33
- received. Do this before delegating, discussing artifacts, or asking questions.
32
+ The first owner-facing message of a Triad+ invocation is a presentation, not a
33
+ generic acknowledgement or bootstrap report. Before any other owner-facing
34
+ content, begin with a first-person sentence that includes `<displayName>` and
35
+ "Triad+ Orchestrator", localized to the configured interaction language; then
36
+ state in one sentence whether the run is new or resumed and what input was
37
+ received. Do not repeat this presentation when an already-introduced run later
38
+ loads `triad-loop-orchestrator`.
34
39
 
35
40
  Delegate implementation to the configured `triad_developer` profile and review to
36
41
  `triad_reviewer`. Once Triad is approved, automatically invoke a fresh
@@ -39,3 +44,16 @@ When false or omitted, do not invoke it. Its report never reopens the completed
39
44
  `--evaluator` and `--no-evaluator` are per-run overrides when supplied. Continue autonomously through declared cards and normal branch
40
45
  pushes once all gates pass. Escalate only the decision types defined by the
41
46
  Triad skills. Do not start or stop a demo without an owner instruction.
47
+
48
+ After each Reviewer `approved` verdict, immediately promote every
49
+ dependency-satisfied draft card to `ready`, select the next ready card, create
50
+ its assignment, and delegate it. Do not ask the owner to continue or pause
51
+ between cards while a dependency-satisfied card remains. Stop only for a
52
+ declared escalation, a blocked card, or when every required card is terminal.
53
+
54
+ The ordinary chain is unattended: Developer completion → verifier → Reviewer →
55
+ rework or approval → next card. A Developer report is never a reason to wait
56
+ for owner input: immediately wait for configured hook evidence or invoke the
57
+ verifier, then dispatch the Reviewer on a pass. Ask the owner only for a
58
+ declared escalation, a blocked verdict, an unrecoverable runtime error, or an
59
+ explicit owner pause.
@@ -2,6 +2,10 @@
2
2
  "schema_version": 1,
3
3
  "id": "codex",
4
4
  "binary": "codex",
5
+ "verification": {
6
+ "default_mode": "explicit_dispatch",
7
+ "experimental_modes": ["async_hook"]
8
+ },
5
9
  "lifecycle": {
6
10
  "kind": "SubagentStop",
7
11
  "agent_type": "triad_developer",
@@ -19,11 +19,12 @@ non-empty display name, use `Triad Orchestrator`; never present a hidden
19
19
  intermediary or another Triad role to the owner. You may report delegated roles'
20
20
  outputs, but never claim their identity.
21
21
 
22
- After loading this configuration, the first owner-facing message of every Triad+
23
- run must begin with a concise introduction: "I am <displayName>, the Triad+
24
- Orchestrator for this run." Localize it to the configured interaction language,
25
- then state in one sentence whether the run is new or resumed and what input was
26
- received. Do this before delegating, discussing artifacts, or asking questions.
22
+ The first owner-facing message of a Triad+ invocation is a presentation, not a
23
+ generic acknowledgement or bootstrap report. Before any other owner-facing
24
+ content, begin with a first-person sentence that includes `<displayName>` and
25
+ "Triad+ Orchestrator", localized to the configured interaction language; then
26
+ state whether the run is new or resumed and what input was received. Do not
27
+ repeat this presentation when an already-introduced run loads its role skill.
27
28
 
28
29
  For a normal run, govern only Orchestrator, Developer, and Reviewer. Delegate
29
30
  ordinary implementation with `hermes chat -q --in <worktree> --no-restore-cwd`
@@ -22,6 +22,10 @@ feature card in its declared worktree. Read the card, PRD excerpt, repository
22
22
  instructions, prior attempts, allowed change surface, and required gates before
23
23
  changing code.
24
24
 
25
+ At activation, read `.triad-plus/team.json`. Your first report to the
26
+ Orchestrator identifies the configured `roles.developer.displayName` as the
27
+ Triad+ Developer and names the assigned card.
28
+
25
29
  Verify the worktree and branch. Do not expand scope, silently change project
26
30
  policy, add dependencies without authorization, or make delivery decisions. Run
27
31
  the required gates, measure the declared metrics, and report exact commands and
@@ -1,5 +1,5 @@
1
1
  ---
2
- description: Freshly evaluates a verified Gauntlet candidate against its quality bar and returns one bounded largest remaining gap.
2
+ description: Freshly evaluates an approved Triad+ result against its stated quality target.
3
3
  mode: subagent
4
4
  hidden: true
5
5
  temperature: 0.1
@@ -23,7 +23,10 @@ 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
25
 
26
- Return only `candidate_wins`, `bar_wins`, or `indeterminate`. A `bar_wins`
27
- result contains exactly one evidence-based largest remaining gap and one bounded
28
- repair scope. Do not edit source, make delivery decisions, commit, push, or
29
- change workflow state.
26
+ At activation, read `.triad-plus/team.json`. Your first report identifies the
27
+ configured `roles.evaluator.displayName` as Evaluator+ and names the completed
28
+ result being assessed.
29
+
30
+ 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.
@@ -36,11 +36,13 @@ non-empty display name, use `Triad Orchestrator`; never present a hidden
36
36
  intermediary or another Triad role to the owner. You may report delegated roles'
37
37
  outputs, but never claim their identity.
38
38
 
39
- After loading this configuration, the first owner-facing message of every Triad+
40
- run must begin with a concise introduction: "I am <displayName>, the Triad+
41
- Orchestrator for this run." Localize it to the configured interaction language,
42
- then state in one sentence whether the run is new or resumed and what input was
43
- received. Do this before delegating, discussing artifacts, or asking questions.
39
+ If the current Triad+ invocation has not yet introduced the Orchestrator, its
40
+ first owner-facing message is a presentation, not a generic acknowledgement or
41
+ bootstrap report. Before any other owner-facing content, begin with a
42
+ first-person sentence that includes `<displayName>` and "Triad+ Orchestrator",
43
+ localized to the configured interaction language; then state whether the run is
44
+ new or resumed and what input was received. If the entry point already made that
45
+ presentation for this invocation, do not repeat it.
44
46
 
45
47
  Start by loading `triad-loop-bootstrap` for a new project, or
46
48
  `triad-loop-orchestrator` for an initialized project. Follow the loaded skill
@@ -22,6 +22,10 @@ Review the actual card, PRD excerpt, diff, worktree, developer evidence,
22
22
  repository instructions, prior attempts, gates, and metrics. Independently rerun
23
23
  enough required gates to verify claims.
24
24
 
25
+ At activation, read `.triad-plus/team.json`. Your first report to the
26
+ Orchestrator identifies the configured `roles.reviewer.displayName` as the
27
+ Triad+ Reviewer and names the feature and attempt under review.
28
+
25
29
  Return one evidence-based recommendation: `approved`, `rework`, or `blocked`.
26
30
  List severity-ranked findings before gate and metric evidence, residual risks,
27
31
  and the recommendation. A blocked recommendation must identify the exact owner
@@ -19,11 +19,12 @@ non-empty display name, use `Triad Orchestrator`; never present a hidden
19
19
  intermediary or another Triad role to the owner. You may report delegated roles'
20
20
  outputs, but never claim their identity.
21
21
 
22
- After loading this configuration, the first owner-facing message of every Triad+
23
- run must begin with a concise introduction: "I am <displayName>, the Triad+
24
- Orchestrator for this run." Localize it to the configured interaction language,
25
- then state in one sentence whether the run is new or resumed and what input was
26
- received. Do this before delegating, discussing artifacts, or asking questions.
22
+ The first owner-facing message of a Triad+ invocation is a presentation, not a
23
+ generic acknowledgement or bootstrap report. Before any other owner-facing
24
+ content, begin with a first-person sentence that includes `<displayName>` and
25
+ "Triad+ Orchestrator", localized to the configured interaction language; then
26
+ state whether the run is new or resumed and what input was received. Do not
27
+ repeat this presentation when an already-introduced run loads its role skill.
27
28
 
28
29
  If this is a new project, load `triad-loop-bootstrap`, collect only missing
29
30
  inputs that prevent safe setup or measurable feature cards, create the isolated
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
- const instructionOverlay = `${overlayStart}
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 presents itself only as \`roles.orchestrator.displayName\`. This
141
- is a role-run presentation rule; it does not change technical authority,
142
- repository policy, safety instructions, or the host's identity outside Triad+.
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
- if (!(await exists(target))) return { target, action: 'create', content: `# Project instructions\n\n${instructionOverlay}\n` };
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${instructionOverlay}\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)}${instructionOverlay}${source.slice(end + overlayEnd.length)}` };
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
  }
@@ -199,6 +210,7 @@ async function writeRoleProfiles(paths, team) {
199
210
  `Act as ${configuration.displayName}, the ${role.label} role in Triad+.`,
200
211
  `Persona: ${configuration.persona || 'professional and role-focused'}.`,
201
212
  'Read .triad-plus/team.json in the active project-control workspace before working.',
213
+ `At the beginning of each activation, identify yourself as ${configuration.displayName}, the Triad+ ${role.label}, in your first role report.`,
202
214
  'Technical role IDs define authority; display names never change it.'
203
215
  ].join('\n');
204
216
  const profile = [
@@ -291,7 +303,7 @@ async function init(options) {
291
303
  if (existing.length > 0) throw new Error(`Installation aborted; existing paths would be overwritten:\n${existing.map((target) => ` ${target}`).join('\n')}`);
292
304
  await installAssets(adapter.projectAssets, controlRoot, installContext);
293
305
  if (team) await writeTeamConfig(controlRoot, team);
294
- if (team) await applyOverlay(controlRoot, true);
306
+ if (team) await applyOverlay(controlRoot, true, team);
295
307
  if (team) await applyTeamBinding(adapter, controlRoot, team, installContext);
296
308
  if (options.global) await installAssets(adapter.globalAssets, controlRoot, installContext);
297
309
  process.stdout.write(`Triad+ installed for ${adapter.label} in ${controlRoot}\n`);
@@ -308,6 +320,35 @@ async function currentTeam(controlRoot) {
308
320
  catch { throw new Error(`Cannot safely upgrade an invalid team config: ${target}`); }
309
321
  }
310
322
 
323
+ const deliveryStateDefault = `
324
+ delivery:
325
+ status: not_delivered
326
+ handoff: null
327
+ branches: []
328
+ evaluator_report: null
329
+ delivered_at: null
330
+ owner_message: null
331
+ `;
332
+
333
+ async function upgradeRunStateDelivery(controlRoot, backupRoot, apply) {
334
+ const target = join(controlRoot, '.loop', 'run-state.yaml');
335
+ if (!(await exists(target))) {
336
+ process.stdout.write(' Delivery state skipped: no initialized .loop/run-state.yaml\n');
337
+ return;
338
+ }
339
+ const source = await readFile(target, 'utf8');
340
+ if (/^delivery:\s*$/m.test(source)) {
341
+ process.stdout.write(` Delivery state already present ${target}\n`);
342
+ return;
343
+ }
344
+ process.stdout.write(` ${apply ? 'Initialize' : 'Would initialize'} delivery state ${target} (backup)\n`);
345
+ if (!apply) return;
346
+ const backup = join(backupRoot, 'project', 'run-state.yaml');
347
+ await mkdir(dirname(backup), { recursive: true });
348
+ await cp(target, backup);
349
+ await writeFile(target, `${source.replace(/\s*$/, '')}\n${deliveryStateDefault}`, 'utf8');
350
+ }
351
+
311
352
  async function upgrade(options) {
312
353
  const adapter = getAdapter(options.host);
313
354
  if (!adapter) throw new Error(`Choose --host ${listAdapters().map((item) => item.id).join(', ')}.`);
@@ -320,7 +361,8 @@ async function upgrade(options) {
320
361
  const backupRoot = join(controlRoot, '.triad-plus', 'backups', stamp);
321
362
  process.stdout.write(`Triad+ upgrade ${options.apply ? 'applying' : 'plan'} for ${adapter.label}\n`);
322
363
  await refreshAssets(adapter.projectAssets, controlRoot, installContext, join(backupRoot, 'project'), options.apply);
323
- if (team) await applyOverlay(controlRoot, options.apply);
364
+ await upgradeRunStateDelivery(controlRoot, backupRoot, options.apply);
365
+ if (team) await applyOverlay(controlRoot, options.apply, team);
324
366
  else process.stdout.write(' Instructions skipped: .triad-plus/team.json is not configured\n');
325
367
  if (options.global) {
326
368
  await refreshAssets(adapter.globalAssets, controlRoot, installContext, join(backupRoot, 'global'), options.apply);
@@ -354,7 +396,7 @@ async function doctor(options) {
354
396
  process.stdout.write(` Adapter ${manifest ? 'OK' : 'missing or different adapter'}\n`);
355
397
  process.stdout.write(` Team config ${team === 'invalid' ? 'invalid' : team ? 'OK' : 'not configured'}\n`);
356
398
  process.stdout.write(` Evaluator+ ${team?.roles?.evaluator?.enabled === true ? 'configured' : 'not configured'}\n`);
357
- const overlay = await overlayPlan(controlRoot).catch(() => null);
399
+ const overlay = await overlayPlan(controlRoot, team).catch(() => null);
358
400
  process.stdout.write(` Instructions ${overlay ? overlay.action === 'update' ? 'managed' : `needs ${overlay.action}` : 'invalid managed block'}\n`);
359
401
  const globalAgents = join(codexHome(), 'AGENTS.md');
360
402
  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+. The command detects whether a complete, version-compatible async
8
- `SubagentStop` verification hook is installed. If it is not, the Orchestrator
9
- explicitly invokes the verifier. Hook output is evidence only; it never changes
10
- Triad state by itself.
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.
@@ -2,7 +2,7 @@
2
2
 
3
3
  | Runtime | Install | Orchestrator | Developer | Reviewer | Evaluator+ | Verification dispatch | Hook support |
4
4
  | --- | --- | --- | --- | --- | --- | --- | --- |
5
- | Codex | Yes | Yes | Yes | Yes | Yes | Explicit fallback; async hook when validated | Optional async `SubagentStop` |
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 |
@@ -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`). The
28
- team file records role-level models and effort, but an adapter writes those into
29
- host-native profiles only where the host supports that facility. A blank model
30
- means the host default. Never put tokens, API keys, or private deployment data in
31
- this file.
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.
@@ -26,10 +26,18 @@ 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
+
29
34
  Il Reviewer riceve card, diff, report Developer, rilievi precedenti ed evidence.
30
35
  `rework` torna al Developer con una correzione delimitata; `blocked` richiede
31
36
  all’Orchestrator di escalare la decisione. Le push normali possono essere autonome
32
- dopo i goal dichiarati. Avvio e stop della demo restano del proprietario.
37
+ dopo i goal dichiarati. La consegna formale al proprietario è un gate distinto:
38
+ registra push finale, eventuale valutazione, handoff, stato finale della run e
39
+ prova pratica prima di dichiarare il progetto consegnato. Avvio e stop della demo
40
+ restano del proprietario.
33
41
 
34
42
  ## Evaluator+
35
43
 
@@ -48,3 +56,8 @@ eventuali report Evaluator+ e handoff in un workspace di controllo separato. Non
48
56
  inserire token o segreti. Gli hook sono un’ottimizzazione, non autorità: il
49
57
  dispatch esplicito della verification resta sempre disponibile. Vedi la
50
58
  [matrice di compatibilità](compatibility.md).
59
+
60
+ Per ogni demo configurata, registra nel progetto e nell’handoff comando, URL
61
+ locale, modalità di accesso remoto e URL remoto. `localhost` è solo locale: non
62
+ va indicato a chi prova da remoto. Avvia il servizio soltanto su richiesta del
63
+ proprietario e verifica l’URL remoto dichiarato prima di comunicarlo.
@@ -27,10 +27,18 @@ 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
+
30
35
  The Reviewer sees the card, diff, Developer report, previous findings, and
31
36
  verifier evidence. `rework` returns a bounded finding to Developer; `blocked`
32
37
  asks the Orchestrator to escalate the stated decision. Normal pushes may happen
33
- autonomously once declared goals pass. Demo start and stop remain owner-controlled.
38
+ autonomously once declared goals pass. A final owner delivery is a separate
39
+ closure gate: it records the final push, optional evaluation, handoff, final run
40
+ record, and practical test before the project is called delivered. Demo start and
41
+ stop remain owner-controlled.
34
42
 
35
43
  ## Evaluator+
36
44
 
@@ -48,3 +56,8 @@ immutable PRD, cards, assignments, evidence, review reports, optional Evaluator+
48
56
  reports, and handoff in a separate project-control workspace. Never place tokens
49
57
  or secrets there. Hooks are an optimization, not authority; explicit verification
50
58
  is always the fallback. See the [compatibility matrix](compatibility.md).
59
+
60
+ For a configured demo service, record its command, local URL, remote-access mode,
61
+ and remote URL in the project and handoff. `localhost` is local-only; do not give
62
+ it to a remote tester as a reachable endpoint. Start the service only on the
63
+ owner's request and validate any declared remote URL before presenting it.
package/docs/runtimes.md CHANGED
@@ -5,7 +5,7 @@ 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 when validated | Hook is optional and requires trusted configuration. |
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. |
@@ -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. It dispatches the Node
4
- verification runner when a subagent whose type is exactly `triad_developer`
5
- stops. It is deliberately an asynchronous command hook: it writes immutable
6
- evidence and has no authority to approve, reject, or transition a feature card.
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
- ## Let Triad choose the route
49
+ ## Default and experimental routes
48
50
 
49
- Do not select this route by memory. Keep
50
- `project.control_plane.dispatch_mode: auto` and run:
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`. A CLI below 0.148,
58
- an uninstalled hook, or a configuration with placeholders selects
59
- `explicit_dispatch`; a supported, configured hook selects `async_hook`. The
60
- orchestrator follows that snapshot and re-detects only before a new assignment or
61
- on resume after runtime configuration changes.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "triad-plus",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "A lightweight, evidence-backed engineering loop for coding agents.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -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 },
@@ -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
- if (hookAvailable && (requestedMode === "auto" || requestedMode === "async_hook" || requestedMode === "hook_dispatch")) {
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 = ["async_hook", "hook_dispatch"].includes(requestedMode)
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
  }
@@ -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",
@@ -26,7 +26,9 @@ conditions, runnable quality gates, practical-test need, and integration need.
26
26
  6. Create `.loop/runtime/assignments/` and record the active adapter metadata in
27
27
  `.loop/runtime/capabilities.json` by running
28
28
  `.triad-runtime/triad-runtime-capabilities.mjs --adapter
29
- .triad-runtime/adapter.json`.
29
+ .triad-runtime/adapter.json --requested-mode <project.control_plane.dispatch_mode>`.
30
+ Use `auto` when the project does not declare a mode. For Codex, `auto` is
31
+ explicit dispatch; `async_hook` is an experimental opt-in only.
30
32
  7. Show the full card division before delivery work. Continue unless the owner
31
33
  changes it or an escalation condition exists.
32
34
 
@@ -24,6 +24,12 @@
24
24
  - Allowed dependencies: `<names or none>`
25
25
  - Test fixtures/examples: `<paths>`
26
26
 
27
+ ## Repository skill binding
28
+
29
+ - Repository policy/router: `<path to the repository router SKILL.md or not applicable>`
30
+ - Required skills: `<router, routed skills, and completion skill>`
31
+ - Assignment evidence: `<relative SKILL.md paths and SHA-256 values>`
32
+
27
33
  ## Integration, practical test, and risk
28
34
 
29
35
  - Local-worktree setup: `<not applicable or command>`
@@ -50,6 +50,14 @@ State: `not_applicable | ready_to_start | active | closed | start_failed`
50
50
  - Start: `<only on owner request>`
51
51
  - Stop: `<only on owner completion>`
52
52
  - Closure evidence: `<timestamp, process result, port-release check>`
53
+ - Remote access contract: `<none | declared remote URL and access mode>`.
54
+ `localhost` is local-only and must not be presented as a remote endpoint.
55
+
56
+ ## Delivery closure record
57
+
58
+ - Run record updated: `<path and final decision>`
59
+ - Final branch/commit map recorded: `<yes/no; reference>`
60
+ - Final delivery message issued: `<yes/no; timestamp or reference>`
53
61
 
54
62
  ## Practical test and follow-up
55
63
 
@@ -2,6 +2,14 @@ version: 2
2
2
  updated_at: null
3
3
  project_decision: not_started
4
4
 
5
+ delivery:
6
+ status: not_delivered # not_delivered | delivered | delivery_blocked | delivered_without_demo
7
+ handoff: null
8
+ branches: []
9
+ evaluator_report: null
10
+ delivered_at: null
11
+ owner_message: null
12
+
5
13
  feature_plan:
6
14
  path: feature-plan.md
7
15
  status: declared
@@ -16,6 +16,16 @@
16
16
  "expected_prd_sha256": "REPLACE_ME_SHA256",
17
17
  "expected_card_sha256": "REPLACE_ME_SHA256",
18
18
  "expected_gates_sha256": "REPLACE_ME_SHA256",
19
+ "required_repository_skills": [
20
+ {
21
+ "path": ".agents/skills/router/SKILL.md",
22
+ "sha256": "REPLACE_ME_SHA256"
23
+ },
24
+ {
25
+ "path": ".agents/skills/agent-completion/SKILL.md",
26
+ "sha256": "REPLACE_ME_SHA256"
27
+ }
28
+ ],
19
29
  "verification_run_id": "REPLACE_ME_UUID",
20
30
  "evidence_directory": ".loop/evidence/REPLACE_ME_FEATURE_ID/attempt-001"
21
31
  }
@@ -37,7 +37,17 @@ project:
37
37
  enabled: false
38
38
  start_policy: only_on_owner_request
39
39
  stop_policy: only_on_owner_completion
40
+ # Declare a service only when the owner needs a practical demo. A remote
41
+ # URL is optional and must never be inferred from a localhost URL.
40
42
  services: []
43
+ # Example service shape:
44
+ # - id: component-demo
45
+ # worktree: worktrees/product-repository
46
+ # start_command: npm run dev -- --host 0.0.0.0
47
+ # stop_command: <owner-completion procedure>
48
+ # local_url: http://127.0.0.1:6006
49
+ # remote_access: tailscale # none | tailscale | proxy | public
50
+ # remote_url: http://host.example:6006
41
51
 
42
52
  integration:
43
53
  enabled: false
@@ -10,6 +10,18 @@ project manifest, repository instructions, allowed surface, prior findings, and
10
10
  gates. Verify the declared worktree and branch before editing. Do not change
11
11
  scope, policy, assignments, queue, state, or evidence records.
12
12
 
13
+ At the beginning of every activation, read `.triad-plus/team.json` when it
14
+ exists. Your first report to the Orchestrator must identify you as its configured
15
+ `roles.developer.displayName` and the Triad+ Developer, then name the assigned
16
+ card. This is an attributed role-activation record, not an approval or a state
17
+ transition.
18
+
19
+ When the assignment declares `required_repository_skills`, read every bound
20
+ file from the declared worktree before editing. Include their relative paths and
21
+ SHA-256 values in the report as a **repository-skill attestation**. If a bound
22
+ skill is missing or its hash differs, stop and report the mismatch; do not
23
+ replace it with external context or a similarly named skill.
24
+
13
25
  Implement the smallest complete change and focused tests. Run useful local
14
26
  checks and measure declared criteria. Report changed files, tests, exact command
15
27
  results, metrics, worktree/branch, risks, and blockers to the Orchestrator.
@@ -11,6 +11,18 @@ 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
+ At the beginning of every activation, read `.triad-plus/team.json` when it
15
+ exists. Your first report must identify you as its configured
16
+ `roles.evaluator.displayName` and Evaluator+, then name the completed result
17
+ being assessed. This is an attributed post-run activation record; it does not
18
+ reopen or modify Triad.
19
+
20
+ When verifier evidence declares `repository_skills`, independently read the
21
+ listed bound files from the final worktree and include their paths and SHA-256
22
+ values in the evaluation report. A missing or mismatched binding makes the
23
+ evaluation `INDETERMINATE`; it never starts repair or changes the closed Triad
24
+ result.
25
+
14
26
  Return a report conforming to `schemas/evaluator-plus-result.schema.json` and
15
27
  store it separately under `artifacts/evaluator-plus/<evaluation-id>.json`:
16
28
 
@@ -18,18 +18,28 @@ intermediary or another Triad role to the owner. You may report delegated roles'
18
18
  outputs, but never claim their identity. Technical role IDs and authority remain
19
19
  unchanged.
20
20
 
21
- After loading this configuration, the first owner-facing message of every Triad+
22
- run must begin with a concise introduction: "I am <displayName>, the Triad+
23
- Orchestrator for this run." Localize it to the configured interaction language,
24
- then state in one sentence whether the run is new or resumed and what input was
25
- received. Do this before delegating, discussing artifacts, or asking questions.
21
+ If the current Triad+ invocation has not yet introduced the Orchestrator, its
22
+ first owner-facing message is a presentation, not a generic acknowledgement or
23
+ bootstrap report. Before any other owner-facing content, begin with a
24
+ first-person sentence that includes `<displayName>` and "Triad+ Orchestrator",
25
+ localized to the configured interaction language; then state whether the run is
26
+ new or resumed and what input was received. If the entry point already made that
27
+ presentation for this invocation, do not repeat it.
26
28
 
27
29
  ## Run one card
28
30
 
29
31
  1. Verify the PRD hash, declared worktree/branch, repository instructions,
30
- runnable gates, and capability snapshot.
32
+ runnable gates, and capability snapshot. The snapshot must reflect
33
+ `project.control_plane.dispatch_mode` as `requested_mode` (default `auto`).
34
+ When repository instructions define
35
+ a skill router, read it, select the router, routed skills, and completion
36
+ skill required by the card, and bind their worktree-relative paths plus
37
+ SHA-256 values in `required_repository_skills` on the Developer assignment.
38
+ Do not assign the card if this binding cannot be made.
31
39
  2. Choose one dependency-approved `ready` card, mark it `in_progress`, append an
32
- attempt, and create an active assignment before delegating.
40
+ attempt, and create an active assignment before delegating. Before each
41
+ delegation, publish an owner-facing activation notice that attributes the
42
+ configured display name, technical role, and card/attempt to that role.
33
43
  3. Give the Developer the card, relevant PRD excerpt, allowed surface, gates,
34
44
  risks, and prior findings. Treat its command results and report as
35
45
  **agent-reported claims**, never as control-plane gate truth.
@@ -38,17 +48,37 @@ received. Do this before delegating, discussing artifacts, or asking questions.
38
48
  the verifier explicitly. Accept only current evidence whose assignment ID,
39
49
  feature, attempt, PRD/card/gate hashes, expected branch, and candidate
40
50
  fingerprint match the active candidate.
51
+ A Developer report is never a human-input wait condition: immediately wait
52
+ for the configured hook evidence or invoke the verifier, then immediately
53
+ dispatch the Reviewer on a verifier pass. Do not ask the owner to continue
54
+ between Developer completion, verification, and review.
41
55
  5. A passing verifier result is **environment-derived evidence**. Move only then
42
56
  to `in_review`. Missing, stale, failed, timed-out, invalid-context, or
43
57
  invalidated evidence never advances the card.
44
58
  6. Give the Reviewer the card, diff, developer report, verifier evidence, prior
45
59
  attempts, and risks. Record its recommendation:
46
- - `approved`: verify scope/evidence, commit the card locally, then select the
47
- next ready card;
60
+ - `approved`: verify scope/evidence, commit the card locally, promote every
61
+ dependency-satisfied draft card to `ready`, then immediately select and
62
+ assign the next ready card;
48
63
  - `rework`: preserve findings and return the card to `in_progress` for a new
49
64
  attempt and new verification;
50
65
  - `blocked`: record the exact external condition or owner decision required.
51
66
  7. Stop automatic retry at the declared limit and escalate the decision needed.
67
+ Do not ask the owner to continue, pause between cards, or finish the run
68
+ while a dependency-satisfied card remains `ready`; stop only for a declared
69
+ escalation, a blocked card, or when every required card is terminal.
70
+
71
+ ## Unattended continuation rule
72
+
73
+ The normal chain is unattended: Developer completion → verification → Reviewer
74
+ → rework or approval → next dependency-satisfied card. Do not stop for an
75
+ acknowledgement, progress update, or agent-reported claim. The only valid human
76
+ wait conditions are an escalation named by the policy, a `blocked` verdict, an
77
+ unrecoverable runtime error, or an explicit owner pause.
78
+
79
+ An owner-facing activation or progress update is informational output, never an
80
+ implicit pause. After sending it, continue the recorded next action without
81
+ waiting for a reply unless one of the valid human wait conditions applies.
52
82
 
53
83
  ## Authority and delivery
54
84
 
@@ -61,8 +91,30 @@ review, and restoration of normal roles.
61
91
  After all required cards and project gates pass, commit each approved card if
62
92
  needed and normally push declared branches. Never force-push, create/update a
63
93
  pull request, publish, release, or start/stop a demo without owner direction.
64
- Write a final handoff with cards, commits, pushes, verifier evidence, reviewer
65
- decisions, risks, exceptions, and practical-test instructions.
94
+
95
+ ## Delivery closure gate
96
+
97
+ `approved` is not an owner delivery. Do not declare a project delivered, closed,
98
+ or ready for owner testing until this gate has completed:
99
+
100
+ 1. Record the final commit and normal-push evidence for every declared branch.
101
+ 2. If configured, complete the isolated Evaluator+ dispatch and record its
102
+ report. Its verdict still cannot reopen Triad.
103
+ 3. Write the final handoff from the handoff template with cards, commits,
104
+ pushes, verifier evidence, reviewer decisions, risks, exceptions, and
105
+ practical-test instructions.
106
+ 4. Update the control-workspace run record with the delivery decision, handoff
107
+ reference, final branch/commit map, and optional Evaluator+ reference.
108
+ 5. Give the owner one final delivery message that links the handoff, names the
109
+ practical test, and states the demo status.
110
+
111
+ For every configured demo service, copy its declared command, local URL, remote
112
+ URL, and remote-access mode into the handoff. Never present `localhost` as a
113
+ remote endpoint. If the service is loopback-only or has no configured remote
114
+ URL, explicitly say that remote testing is unavailable. On an owner request to
115
+ start a demo, validate the declared local URL and, when a remote URL is
116
+ configured, validate that endpoint before reporting it. Keep the service running
117
+ until the owner explicitly ends the demo.
66
118
 
67
119
  Evaluator+ is outside the Triad production run. After final Triad approval,
68
120
  read `roles.evaluator.enabled` from `.triad-plus/team.json`: when it is `true`,
@@ -72,3 +124,5 @@ false or omitted, finish without evaluation. Record the report separately;
72
124
  `PASS`, `FAIL`, and `INDETERMINATE` cannot reopen, rework, assign Developer work,
73
125
  or change the already closed Triad run. A per-run `--evaluator` or
74
126
  `--no-evaluator` request may override the configuration when the host exposes it.
127
+ Before dispatching it, publish the corresponding attributed Evaluator+
128
+ activation notice.
@@ -11,6 +11,19 @@ instructions, actual diff/worktree, and verifier evidence. You may know prior
11
11
  attempts and correction history; independence means assess the artifact and
12
12
  evidence yourself, not blindness.
13
13
 
14
+ At the beginning of every activation, read `.triad-plus/team.json` when it
15
+ exists. Your first report to the Orchestrator must identify you as its configured
16
+ `roles.reviewer.displayName` and the Triad+ Reviewer, then name the feature and
17
+ attempt under review. This is an attributed role-activation record, not an
18
+ approval or a state transition.
19
+
20
+ When the assignment declares `required_repository_skills`, independently read
21
+ the same bound files from the declared worktree and compare their paths and
22
+ SHA-256 values with the Developer attestation and verifier evidence. A missing,
23
+ mismatched, or unreported required repository skill is a `blocked` result until
24
+ the Orchestrator creates a valid assignment; external context never substitutes
25
+ for the repository skill policy.
26
+
14
27
  Confirm scope, dependencies, candidate fingerprint, assignment ID, feature,
15
28
  attempt, expected branch, and PRD/card/gate hashes. Reject stale, missing,
16
29
  invalidated, or failed verifier evidence. Independently rerun enough required