triad-plus 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/CODE_OF_CONDUCT.md +9 -0
- package/CONTRIBUTING.md +44 -0
- package/LICENSE +21 -0
- package/README.md +80 -0
- package/SECURITY.md +12 -0
- package/adapters/antigravity/.agents/agents/triad-developer/agent.md +11 -0
- package/adapters/antigravity/.agents/agents/triad-evaluator/agent.md +11 -0
- package/adapters/antigravity/.agents/agents/triad-orchestrator/agent.md +19 -0
- package/adapters/antigravity/.agents/agents/triad-reviewer/agent.md +10 -0
- package/adapters/antigravity/.agents/skills/triad/SKILL.md +30 -0
- package/adapters/antigravity/.agents/skills/triad/agents/openai.yaml +4 -0
- package/adapters/antigravity/README.md +26 -0
- package/adapters/antigravity/install.sh +72 -0
- package/adapters/antigravity/runtime.json +6 -0
- package/adapters/claude-code/.claude/agents/triad-developer.md +19 -0
- package/adapters/claude-code/.claude/agents/triad-evaluator.md +17 -0
- package/adapters/claude-code/.claude/agents/triad-reviewer.md +14 -0
- package/adapters/claude-code/.claude/commands/triad.md +26 -0
- package/adapters/claude-code/README.md +60 -0
- package/adapters/claude-code/install.sh +75 -0
- package/adapters/claude-code/runtime.json +10 -0
- package/adapters/codex/README.md +39 -0
- package/adapters/codex/install.sh +78 -0
- package/adapters/codex/prompts/triad.md +25 -0
- package/adapters/codex/runtime.json +11 -0
- package/adapters/hermes/README.md +31 -0
- package/adapters/hermes/install.sh +45 -0
- package/adapters/hermes/runtime.json +7 -0
- package/adapters/hermes/skills/triad/SKILL.md +36 -0
- package/adapters/hermes/skills/triad/agents/openai.yaml +4 -0
- package/adapters/opencode/.opencode/agents/triad-developer.md +32 -0
- package/adapters/opencode/.opencode/agents/triad-evaluator.md +29 -0
- package/adapters/opencode/.opencode/agents/triad-orchestrator.md +60 -0
- package/adapters/opencode/.opencode/agents/triad-reviewer.md +32 -0
- package/adapters/opencode/.opencode/commands/triad.md +24 -0
- package/adapters/opencode/README.md +111 -0
- package/adapters/opencode/install.sh +75 -0
- package/adapters/opencode/runtime.json +6 -0
- package/adapters/registry.mjs +224 -0
- package/assets/triad-plus-engineering-loop-icon.svg +16 -0
- package/bin/triad-plus.js +311 -0
- package/docs/antigravity-replication.md +9 -0
- package/docs/architecture.md +24 -0
- package/docs/claude-code-replication.md +9 -0
- package/docs/codex-replication.md +10 -0
- package/docs/compatibility.md +13 -0
- package/docs/configuration.md +27 -0
- package/docs/evaluator-plus.md +13 -0
- package/docs/getting-started.md +33 -0
- package/docs/npx-installation.md +44 -0
- package/docs/opencode-replication.md +8 -0
- package/docs/operating-guide.it.md +49 -0
- package/docs/operating-guide.md +49 -0
- package/docs/runtimes.md +24 -0
- package/docs/troubleshooting.md +22 -0
- package/docs/verification.md +17 -0
- package/integrations/claude-code/README.md +25 -0
- package/integrations/claude-code/hooks.json +17 -0
- package/integrations/codex/README.md +61 -0
- package/integrations/codex/hooks.json +20 -0
- package/package.json +51 -0
- package/runtime/legacy-adapters.json +17 -0
- package/runtime/lib/evidence.mjs +31 -0
- package/runtime/lib/fingerprint.mjs +52 -0
- package/runtime/lib/gates.mjs +89 -0
- package/runtime/lib/process.mjs +36 -0
- package/runtime/triad-runtime-capabilities.mjs +131 -0
- package/runtime/triad-verify.mjs +152 -0
- package/schemas/evaluator-plus-result.schema.json +17 -0
- package/schemas/runtime-capabilities.schema.json +15 -0
- package/schemas/verification-evidence.schema.json +21 -0
- package/skills/triad-loop-bootstrap/SKILL.md +43 -0
- package/skills/triad-loop-bootstrap/agents/openai.yaml +4 -0
- package/skills/triad-loop-bootstrap/assets/loop-template/decision-policy.md +22 -0
- package/skills/triad-loop-bootstrap/assets/loop-template/feature-card.template.md +32 -0
- package/skills/triad-loop-bootstrap/assets/loop-template/feature-plan.template.md +13 -0
- package/skills/triad-loop-bootstrap/assets/loop-template/handoff-report.template.md +60 -0
- package/skills/triad-loop-bootstrap/assets/loop-template/quality-gates.yaml +20 -0
- package/skills/triad-loop-bootstrap/assets/loop-template/review-report.template.md +32 -0
- package/skills/triad-loop-bootstrap/assets/loop-template/run-state.yaml +22 -0
- package/skills/triad-loop-bootstrap/assets/loop-template/runtime/assignments/assignment.template.json +21 -0
- package/skills/triad-loop-bootstrap/assets/loop-template/runtime/capabilities.template.json +27 -0
- package/skills/triad-loop-bootstrap/assets/loop-template/work-queue.yaml +28 -0
- package/skills/triad-loop-bootstrap/assets/project.yaml +55 -0
- package/skills/triad-loop-developer/SKILL.md +22 -0
- package/skills/triad-loop-developer/agents/openai.yaml +4 -0
- package/skills/triad-loop-evaluator/SKILL.md +24 -0
- package/skills/triad-loop-evaluator/agents/openai.yaml +4 -0
- package/skills/triad-loop-orchestrator/SKILL.md +54 -0
- package/skills/triad-loop-orchestrator/agents/openai.yaml +4 -0
- package/skills/triad-loop-reviewer/SKILL.md +25 -0
- package/skills/triad-loop-reviewer/agents/openai.yaml +4 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Getting started
|
|
2
|
+
|
|
3
|
+
Triad+ keeps project-control records outside product repositories. Choose an empty
|
|
4
|
+
directory for that workspace and a runtime available on your machine.
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
npx triad-plus init --host codex --control "$PWD/triad-control" --global
|
|
8
|
+
npx triad-plus doctor --host codex --control "$PWD/triad-control"
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
The interactive setup asks for language, owner address, neutral role names,
|
|
12
|
+
personas, models, and whether optional Evaluator+ is enabled. It writes only
|
|
13
|
+
after you type `install`.
|
|
14
|
+
|
|
15
|
+
For non-interactive setup, pass a reviewed team file:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npx triad-plus init --host opencode --control "$PWD/triad-control" --team-config ./team.json
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Open `triad-control` in the selected host and invoke its native Triad entry
|
|
22
|
+
point with an absolute PRD path. The Orchestrator snapshots the PRD, asks only
|
|
23
|
+
for missing measurable facts, and shows feature cards before implementation.
|
|
24
|
+
|
|
25
|
+
For the first run, use a small PRD with one observable acceptance criterion and
|
|
26
|
+
one deterministic command such as a focused unit test. The expected path is:
|
|
27
|
+
|
|
28
|
+
```text
|
|
29
|
+
Orchestrator → Developer → verifier evidence → Reviewer → approved | rework | blocked
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
The host agent governs the conversation. Triad+ does not start product services
|
|
33
|
+
or demonstrations unless you explicitly request them.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Install Triad+
|
|
2
|
+
|
|
3
|
+
Triad+ installs a project-control workspace, not application code. Keep it
|
|
4
|
+
outside product repositories whenever possible.
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
npx triad-plus
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
The interactive setup selects a host, optional user-level command, language,
|
|
11
|
+
owner address, role display names/personas, models, and whether the optional
|
|
12
|
+
Evaluator+ is enabled. It changes files only after `install` is typed.
|
|
13
|
+
|
|
14
|
+
For repeatable setup:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npx triad-plus init --host codex --control /path/to/project-control --global
|
|
18
|
+
npx triad-plus doctor --host codex --control /path/to/project-control
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Supported hosts: `codex`, `opencode`, `claude-code`, `antigravity`, `hermes`.
|
|
22
|
+
Use `--global` to install a host-level entry point where desired. The installer
|
|
23
|
+
refuses overwrites. If the control path is recognizably a product Git repository,
|
|
24
|
+
it stops unless `--allow-product-repo` is explicitly supplied after review.
|
|
25
|
+
|
|
26
|
+
The saved `.triad-plus/team.json` separates stable role IDs from display names,
|
|
27
|
+
personas, models, and supported effort/options. Existing schema-version-1 team
|
|
28
|
+
files remain valid. Core roles are always enabled; Evaluator+ is enabled only
|
|
29
|
+
when `roles.evaluator.enabled` is `true`.
|
|
30
|
+
|
|
31
|
+
| Role | Responsibility |
|
|
32
|
+
| --- | --- |
|
|
33
|
+
| Orchestrator | Maintains goal/context and decides the next step. |
|
|
34
|
+
| Developer | Changes the artifact to meet the declared card. |
|
|
35
|
+
| Reviewer | Finds defects and returns approved, rework, or blocked. |
|
|
36
|
+
| Evaluator+ | Optionally and freshly judges an approved result after the run. |
|
|
37
|
+
|
|
38
|
+
Open the control workspace and invoke the host-native command with an absolute
|
|
39
|
+
PRD path. The Orchestrator presents feature cards before implementation, then
|
|
40
|
+
delegates normal development and review. Use the optional evaluator form exposed
|
|
41
|
+
by the host only after Triad is approved: invoke the native Triad command with
|
|
42
|
+
`--evaluator` (for example `/triad --evaluator`) and the Orchestrator will either
|
|
43
|
+
run the configured Evaluator+ or state that it is disabled. Users never create
|
|
44
|
+
evaluation packets, report paths, or evidence directories manually.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# OpenCode adapter
|
|
2
|
+
|
|
3
|
+
Install with `npx triad-plus init --host opencode --control <path>`. Open the
|
|
4
|
+
control workspace and run `/triad <absolute-prd-path>`. Project-local agent and
|
|
5
|
+
command assets carry the configured role models where OpenCode supports them.
|
|
6
|
+
|
|
7
|
+
Verification uses explicit Orchestrator dispatch. Evaluator+ is optional and may
|
|
8
|
+
be invoked only after a Reviewer-approved result; it cannot reopen that run.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Guida operativa Triad+
|
|
2
|
+
|
|
3
|
+
Triad+ è un metodo operativo per un coding-agent host. Non avvia daemon,
|
|
4
|
+
scheduler, database di workflow o una macchina a stati autonoma.
|
|
5
|
+
|
|
6
|
+
## Il metodo
|
|
7
|
+
|
|
8
|
+
```text
|
|
9
|
+
ready → in_progress → verifying → in_review → approved
|
|
10
|
+
├── rework → in_progress
|
|
11
|
+
└── blocked
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
L’Orchestrator decide il prossimo passo e le escalation. Il Developer esegue un
|
|
15
|
+
tentativo delimitato. Il Reviewer decide `approved`, `rework` oppure `blocked`.
|
|
16
|
+
|
|
17
|
+
Prima del lavoro l’Orchestrator congela il PRD, dichiara card misurabili,
|
|
18
|
+
repository/worktree/branch e quality gate deterministici. Mostra il piano al
|
|
19
|
+
proprietario, poi delega normalmente sviluppo e review.
|
|
20
|
+
|
|
21
|
+
Dopo il completamento del Developer avviene la verifica: tramite hook validato
|
|
22
|
+
quando disponibile, altrimenti con dispatch esplicito dell’Orchestrator. Il
|
|
23
|
+
verifier esegue solo comandi `control-plane` dichiarati. L’evidence atomica lega
|
|
24
|
+
assignment ID/hash, run, branch del worktree, fingerprint del candidato, hash di
|
|
25
|
+
PRD/card e definizione dei gate. “I test passano” detto dall’agente è una claim;
|
|
26
|
+
l’output del verifier è evidence derivata dall’ambiente. Evidence fallita o
|
|
27
|
+
invalida non può essere trattata come pass.
|
|
28
|
+
|
|
29
|
+
Il Reviewer riceve card, diff, report Developer, rilievi precedenti ed evidence.
|
|
30
|
+
`rework` torna al Developer con una correzione delimitata; `blocked` richiede
|
|
31
|
+
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.
|
|
33
|
+
|
|
34
|
+
## Evaluator+
|
|
35
|
+
|
|
36
|
+
Evaluator+ è opzionale e fuori dal loop produttivo. Dopo `approved`, osserva solo
|
|
37
|
+
goal, target di accettazione, candidato finale ed evidence del verifier. Produce
|
|
38
|
+
`PASS`, `FAIL` oppure `INDETERMINATE`. Un `FAIL` non modifica la run chiusa e non
|
|
39
|
+
avvia rework. Vedi [Evaluator+](evaluator-plus.md).
|
|
40
|
+
|
|
41
|
+
## Decisioni umane e record
|
|
42
|
+
|
|
43
|
+
Triad+ interpella il proprietario solo per criteri di successo mancanti, cambi di
|
|
44
|
+
scope/policy materiali, ambiguità non risolvibili, fallimenti ripetuti o demo
|
|
45
|
+
richieste. Conserva PRD immutabile, card, assignment, evidence, report di review,
|
|
46
|
+
eventuali report Evaluator+ e handoff in un workspace di controllo separato. Non
|
|
47
|
+
inserire token o segreti. Gli hook sono un’ottimizzazione, non autorità: il
|
|
48
|
+
dispatch esplicito della verification resta sempre disponibile. Vedi la
|
|
49
|
+
[matrice di compatibilità](compatibility.md).
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Triad+ operating guide
|
|
2
|
+
|
|
3
|
+
Triad+ is an operating method for a coding-agent host. It does not run a daemon,
|
|
4
|
+
scheduler, workflow database, or autonomous state controller.
|
|
5
|
+
|
|
6
|
+
## The method
|
|
7
|
+
|
|
8
|
+
```text
|
|
9
|
+
ready → in_progress → verifying → in_review → approved
|
|
10
|
+
├── rework → in_progress
|
|
11
|
+
└── blocked
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
The Orchestrator owns the next step and escalation. The Developer owns a bounded
|
|
15
|
+
implementation attempt. The Reviewer owns its `approved`, `rework`, or `blocked`
|
|
16
|
+
verdict.
|
|
17
|
+
|
|
18
|
+
Before work, the Orchestrator snapshots the PRD, declares measurable feature
|
|
19
|
+
cards, target repositories/worktrees/branches, and deterministic quality gates.
|
|
20
|
+
It shows the plan to the owner, then delegates ordinary development and review.
|
|
21
|
+
|
|
22
|
+
After Developer completion, verification happens through a validated host hook
|
|
23
|
+
when available, or explicit Orchestrator dispatch. The verifier executes only
|
|
24
|
+
declared `control-plane` commands. Atomic evidence binds assignment ID/hash, run,
|
|
25
|
+
worktree branch, candidate fingerprint, PRD/card hashes, and gate definition
|
|
26
|
+
hash. A developer saying tests pass is a claim; verifier output is
|
|
27
|
+
environment-derived evidence. Failed or invalid evidence cannot be treated as a
|
|
28
|
+
pass.
|
|
29
|
+
|
|
30
|
+
The Reviewer sees the card, diff, Developer report, previous findings, and
|
|
31
|
+
verifier evidence. `rework` returns a bounded finding to Developer; `blocked`
|
|
32
|
+
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.
|
|
34
|
+
|
|
35
|
+
## Evaluator+
|
|
36
|
+
|
|
37
|
+
Evaluator+ is optional and outside the production loop. After `approved`, it
|
|
38
|
+
inspects only the goal, acceptance target, final candidate, and verifier evidence.
|
|
39
|
+
Its report is `PASS`, `FAIL`, or `INDETERMINATE`. A failure does not modify the
|
|
40
|
+
closed run or start repair. See [Evaluator+](evaluator-plus.md).
|
|
41
|
+
|
|
42
|
+
## Human decisions and records
|
|
43
|
+
|
|
44
|
+
Triad+ asks the owner only for missing success criteria, material scope/policy
|
|
45
|
+
changes, unresolved ambiguity, repeated failure, or a requested demo. Keep the
|
|
46
|
+
immutable PRD, cards, assignments, evidence, review reports, optional Evaluator+
|
|
47
|
+
reports, and handoff in a separate project-control workspace. Never place tokens
|
|
48
|
+
or secrets there. Hooks are an optimization, not authority; explicit verification
|
|
49
|
+
is always the fallback. See the [compatibility matrix](compatibility.md).
|
package/docs/runtimes.md
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Runtime adapters
|
|
2
|
+
|
|
3
|
+
Every adapter installs the same role skills and verifier runtime, while retaining
|
|
4
|
+
host-specific entry points and configuration behavior.
|
|
5
|
+
|
|
6
|
+
| Runtime | Prerequisite | Entry point | Verification | Hook limitation |
|
|
7
|
+
| --- | --- | --- | --- | --- |
|
|
8
|
+
| Codex | Codex CLI | `/prompts:triad` | Explicit dispatch; async hook when validated | Hook is optional and requires trusted configuration. |
|
|
9
|
+
| Claude Code | Claude Code CLI | `/triad` | Explicit dispatch; hook when validated | Hook is optional and requires trusted configuration. |
|
|
10
|
+
| OpenCode | OpenCode | `/triad` | Explicit dispatch | No Triad lifecycle hook. |
|
|
11
|
+
| Antigravity | Antigravity | `/triad` | Explicit dispatch | No Triad lifecycle hook. |
|
|
12
|
+
| Hermes Agent | Hermes Agent | `/triad` | Explicit dispatch | No Triad lifecycle hook. |
|
|
13
|
+
|
|
14
|
+
Install with `npx triad-plus init --host <runtime> --control <path>`; use
|
|
15
|
+
`--global` when you want host-level command assets. `doctor` reports the runtime
|
|
16
|
+
binary, adapter assets, verifier, team file, and optional Evaluator+ state.
|
|
17
|
+
|
|
18
|
+
Hermes installs user skills into the active Hermes profile. Its role invocations
|
|
19
|
+
must bind terminal operations to the declared worktree; this is handled by its
|
|
20
|
+
adapter instructions. It is supported, not experimental.
|
|
21
|
+
|
|
22
|
+
See the concise host guides for [Codex](codex-replication.md),
|
|
23
|
+
[Claude Code](claude-code-replication.md), [OpenCode](opencode-replication.md),
|
|
24
|
+
and [Antigravity](antigravity-replication.md).
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Troubleshooting
|
|
2
|
+
|
|
3
|
+
Run doctor first:
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npx triad-plus doctor --host <runtime> --control /path/to/triad-control
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
`not installed` means the selected adapter assets are absent from that control
|
|
10
|
+
workspace. `not installed or version unavailable` for a host means its binary is
|
|
11
|
+
not on PATH or otherwise cannot answer `--version`. Re-run the appropriate host
|
|
12
|
+
installer or fix the host environment; Triad+ does not require every runtime to
|
|
13
|
+
be present.
|
|
14
|
+
|
|
15
|
+
If a hook is unavailable, use explicit verification dispatch. Hooks are never a
|
|
16
|
+
requirement for a normal Triad run.
|
|
17
|
+
|
|
18
|
+
If a verifier result is `invalid_context`, compare the assignment, PRD/card/gate
|
|
19
|
+
baselines, worktree, branch, and candidate. Do not treat it as a passing test.
|
|
20
|
+
|
|
21
|
+
If Evaluator+ is unavailable, check `roles.evaluator.enabled` in the team file.
|
|
22
|
+
An Evaluator+ failure is post-run information, not an automatic repair request.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Verification and evidence
|
|
2
|
+
|
|
3
|
+
An agent-reported claim is not the same as verification evidence. A Developer can
|
|
4
|
+
report the commands it ran; `triad-verify` independently observes declared
|
|
5
|
+
required `control-plane` gates and writes atomic evidence.
|
|
6
|
+
|
|
7
|
+
Before gates run, the verifier validates the active assignment, PRD/card/gate
|
|
8
|
+
hashes, worktree, expected branch, and candidate fingerprint. It records the
|
|
9
|
+
assignment ID/hash and run ID, executes deterministic commands with a bounded
|
|
10
|
+
timeout, and invalidates evidence if the candidate changes while gates run.
|
|
11
|
+
|
|
12
|
+
Only `control-plane` gate executors are supported in v1. A required unsupported
|
|
13
|
+
executor fails closed. Verifier evidence informs the Reviewer and Orchestrator;
|
|
14
|
+
it does not itself approve, rework, or transition a run.
|
|
15
|
+
|
|
16
|
+
Evidence files and logs are diagnostics. Users normally need only the
|
|
17
|
+
Orchestrator's summary and the Reviewer verdict.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Triad Claude Code hook adapter
|
|
2
|
+
|
|
3
|
+
Claude Code exposes `SubagentStop` with the developer's `agent_id` and
|
|
4
|
+
`agent_type`. The optional command hook in `hooks.json` filters exactly
|
|
5
|
+
`triad-developer` and invokes the Node verifier. It is a synchronous hook
|
|
6
|
+
dispatch: it writes evidence but never owns a Triad state transition.
|
|
7
|
+
|
|
8
|
+
The file is a configuration fragment, not a settings file to copy blindly.
|
|
9
|
+
Replace `<TRIAD_RUNTIME>` and `<TRIAD_CONTROL_ROOT>` with canonical absolute
|
|
10
|
+
paths in the actual Claude settings syntax. Confirm its presence with `/hooks`
|
|
11
|
+
and validate agent configuration with `claude plugin validate .claude/agents`
|
|
12
|
+
where the installed CLI supports it.
|
|
13
|
+
|
|
14
|
+
With `dispatch_mode: auto`, run:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
node .triad-runtime/triad-runtime-capabilities.mjs \
|
|
18
|
+
--host claude-code \
|
|
19
|
+
--hook-config /absolute/path/to/installed-claude-settings.json
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
An installed `SubagentStop` hook selects `hook_dispatch`. If Claude Code or its
|
|
23
|
+
hook is absent, Triad selects `explicit_dispatch`: the orchestrator runs the
|
|
24
|
+
same verifier after developer completion. Neither mode permits developer claims
|
|
25
|
+
to substitute for verification evidence.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"purpose": "Triad verification dispatch only; this hook writes evidence and never changes card state.",
|
|
3
|
+
"hooks": {
|
|
4
|
+
"SubagentStop": [
|
|
5
|
+
{
|
|
6
|
+
"matcher": "^triad-developer$",
|
|
7
|
+
"hooks": [
|
|
8
|
+
{
|
|
9
|
+
"type": "command",
|
|
10
|
+
"command": "node <TRIAD_RUNTIME>/triad-verify.mjs --project <TRIAD_CONTROL_ROOT>",
|
|
11
|
+
"timeout": 900
|
|
12
|
+
}
|
|
13
|
+
]
|
|
14
|
+
}
|
|
15
|
+
]
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Triad Codex hook adapter
|
|
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.
|
|
7
|
+
|
|
8
|
+
## Install only after verifying the local Codex schema
|
|
9
|
+
|
|
10
|
+
The included `hooks.json` is a versioned configuration fragment, not a global
|
|
11
|
+
configuration file to copy blindly. Replace these placeholders with canonical
|
|
12
|
+
absolute paths in the configuration syntax accepted by the installed CLI:
|
|
13
|
+
|
|
14
|
+
- `<TRIAD_RUNTIME>`: the cloned repository's `runtime` directory;
|
|
15
|
+
- `<TRIAD_CONTROL_ROOT>`: the project-control repository containing `.loop/`.
|
|
16
|
+
|
|
17
|
+
Before enabling it, verify all of the following:
|
|
18
|
+
|
|
19
|
+
1. `codex --version` is at least `0.148.0`.
|
|
20
|
+
2. The local CLI accepts the `SubagentStop` async command-hook schema.
|
|
21
|
+
3. The project-local hook source is trusted by the Codex installation.
|
|
22
|
+
4. Developer assignments use `agent_type: triad_developer` and are written
|
|
23
|
+
before the subagent starts.
|
|
24
|
+
5. Gate commands are in the hashed, trusted `.loop/quality-gates.yaml`.
|
|
25
|
+
|
|
26
|
+
Do not use this fragment for `mcp_tool` execution. MCP hooks are synchronous and
|
|
27
|
+
optional; long verification remains in the async command runner. A missing,
|
|
28
|
+
aborted, or unsupported hook must be detected by the orchestrator watchdog and
|
|
29
|
+
recorded as `verification_hook_missing`, `developer_aborted`, or
|
|
30
|
+
`verification_timeout`; it must never silently create a passing result.
|
|
31
|
+
|
|
32
|
+
## Direct, auditable fallback
|
|
33
|
+
|
|
34
|
+
Until the lifecycle hook is supported and verified, dispatch the same runner
|
|
35
|
+
explicitly after developer completion:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
node /absolute/path/to/triad-plus-engineering-loop/runtime/triad-verify.mjs \
|
|
39
|
+
--project /absolute/path/to/project-control < hook-payload.json
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The JSON payload needs an `agent_id` matching an active assignment under
|
|
43
|
+
`.loop/runtime/assignments/`. The runner resolves all semantic data from that
|
|
44
|
+
assignment and emits only structured evidence. The orchestrator still validates
|
|
45
|
+
the evidence against the active candidate before changing state.
|
|
46
|
+
|
|
47
|
+
## Let Triad choose the route
|
|
48
|
+
|
|
49
|
+
Do not select this route by memory. Keep
|
|
50
|
+
`project.control_plane.dispatch_mode: auto` and run:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
node /absolute/path/to/triad-plus-engineering-loop/runtime/triad-runtime-capabilities.mjs \
|
|
54
|
+
--hook-config /absolute/path/to/installed-triad-hooks.json
|
|
55
|
+
```
|
|
56
|
+
|
|
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.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"minimum_codex_cli_version": "0.148.0",
|
|
3
|
+
"purpose": "Triad external verification dispatch only; this hook never changes card state.",
|
|
4
|
+
"hooks": {
|
|
5
|
+
"SubagentStop": [
|
|
6
|
+
{
|
|
7
|
+
"matcher": "^triad_developer$",
|
|
8
|
+
"hooks": [
|
|
9
|
+
{
|
|
10
|
+
"type": "command",
|
|
11
|
+
"command": "node <TRIAD_RUNTIME>/triad-verify.mjs --project <TRIAD_CONTROL_ROOT>",
|
|
12
|
+
"async": true,
|
|
13
|
+
"timeout": 900,
|
|
14
|
+
"statusMessage": "Running Triad verification"
|
|
15
|
+
}
|
|
16
|
+
]
|
|
17
|
+
}
|
|
18
|
+
]
|
|
19
|
+
}
|
|
20
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "triad-plus",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A lightweight, evidence-backed engineering loop for coding agents.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"triad-plus": "bin/triad-plus.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"bin/",
|
|
12
|
+
"adapters/",
|
|
13
|
+
"integrations/",
|
|
14
|
+
"runtime/",
|
|
15
|
+
"schemas/",
|
|
16
|
+
"skills/",
|
|
17
|
+
"docs/",
|
|
18
|
+
"assets/",
|
|
19
|
+
"README.md",
|
|
20
|
+
"LICENSE",
|
|
21
|
+
"CHANGELOG.md",
|
|
22
|
+
"CONTRIBUTING.md",
|
|
23
|
+
"SECURITY.md",
|
|
24
|
+
"CODE_OF_CONDUCT.md"
|
|
25
|
+
],
|
|
26
|
+
"engines": {
|
|
27
|
+
"node": ">=20"
|
|
28
|
+
},
|
|
29
|
+
"scripts": {
|
|
30
|
+
"test": "node tests/runtime-forward-test.mjs && node tests/cli-install-test.mjs",
|
|
31
|
+
"pack:check": "npm pack --dry-run"
|
|
32
|
+
},
|
|
33
|
+
"repository": {
|
|
34
|
+
"type": "git",
|
|
35
|
+
"url": "git+https://github.com/delbono-smeup/triad-plus-engineering-loop.git"
|
|
36
|
+
},
|
|
37
|
+
"bugs": {
|
|
38
|
+
"url": "https://github.com/delbono-smeup/triad-plus-engineering-loop/issues"
|
|
39
|
+
},
|
|
40
|
+
"homepage": "https://github.com/delbono-smeup/triad-plus-engineering-loop#readme",
|
|
41
|
+
"keywords": [
|
|
42
|
+
"agent-skills",
|
|
43
|
+
"codex",
|
|
44
|
+
"claude-code",
|
|
45
|
+
"opencode",
|
|
46
|
+
"antigravity",
|
|
47
|
+
"engineering-loop",
|
|
48
|
+
"code-review",
|
|
49
|
+
"verification"
|
|
50
|
+
]
|
|
51
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"codex": {
|
|
3
|
+
"schema_version": 1,
|
|
4
|
+
"id": "codex",
|
|
5
|
+
"binary": "codex",
|
|
6
|
+
"lifecycle": { "kind": "SubagentStop", "agent_type": "triad_developer", "requires_async": true, "minimum_version": "0.148.0" }
|
|
7
|
+
},
|
|
8
|
+
"opencode": { "schema_version": 1, "id": "opencode", "binary": "opencode", "lifecycle": null },
|
|
9
|
+
"claude-code": {
|
|
10
|
+
"schema_version": 1,
|
|
11
|
+
"id": "claude-code",
|
|
12
|
+
"binary": "claude",
|
|
13
|
+
"lifecycle": { "kind": "SubagentStop", "agent_type": "triad-developer", "requires_async": false }
|
|
14
|
+
},
|
|
15
|
+
"antigravity": { "schema_version": 1, "id": "antigravity", "binary": "agy", "lifecycle": null },
|
|
16
|
+
"hermes": { "schema_version": 1, "id": "hermes", "binary": "hermes", "lifecycle": null }
|
|
17
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { mkdir, rename, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
const TOKEN_PATTERN = /(?:api[_-]?key|token|secret|password|authorization)\s*(?:=|:|\s)\s*(?:Bearer\s+)?[^\s'"`]{8,}/gi;
|
|
5
|
+
const MAX_LOG_BYTES = 64 * 1024;
|
|
6
|
+
|
|
7
|
+
export function redactAndCap(value) {
|
|
8
|
+
const text = String(value ?? "").replace(TOKEN_PATTERN, "[REDACTED]");
|
|
9
|
+
const bytes = Buffer.from(text);
|
|
10
|
+
if (bytes.length <= MAX_LOG_BYTES) return { text, truncated: false };
|
|
11
|
+
return {
|
|
12
|
+
text: bytes.subarray(0, MAX_LOG_BYTES).toString("utf8") + "\n[TRUNCATED]",
|
|
13
|
+
truncated: true,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function writeLog(logDirectory, gateId, stream, value) {
|
|
18
|
+
await mkdir(logDirectory, { recursive: true });
|
|
19
|
+
const safeId = gateId.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
20
|
+
const relative = path.join("logs", `${safeId}.${stream}.log`);
|
|
21
|
+
const result = redactAndCap(value);
|
|
22
|
+
await writeFile(path.join(path.dirname(logDirectory), relative), result.text, "utf8");
|
|
23
|
+
return { ref: relative, truncated: result.truncated };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function writeAtomicJson(targetPath, value) {
|
|
27
|
+
await mkdir(path.dirname(targetPath), { recursive: true });
|
|
28
|
+
const temporaryPath = `${targetPath}.tmp`;
|
|
29
|
+
await writeFile(temporaryPath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
30
|
+
await rename(temporaryPath, targetPath);
|
|
31
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { readFile, realpath, stat } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { runProcess } from "./process.mjs";
|
|
5
|
+
|
|
6
|
+
const SENSITIVE_PATH = /(^|\/)(?:\.env(?:\..*)?|.*\.pem|.*\.key|credentials(?:\..*)?)$/i;
|
|
7
|
+
const IGNORED_PATH = /(^|\/)(?:node_modules|\.git|dist|build|coverage)(\/|$)/;
|
|
8
|
+
|
|
9
|
+
function digest(value) {
|
|
10
|
+
return createHash("sha256").update(value).digest("hex");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function parseLines(value) {
|
|
14
|
+
return value.split("\n").map((line) => line.trim()).filter(Boolean);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async function gitLines(worktree, args) {
|
|
18
|
+
const result = await runProcess("git", args, { cwd: worktree, timeoutMs: 15_000 });
|
|
19
|
+
if (result.exitCode !== 0) throw new Error(`git ${args.join(" ")} failed`);
|
|
20
|
+
return parseLines(result.stdout);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function worktreeBranch(worktree) {
|
|
24
|
+
const root = await realpath(worktree);
|
|
25
|
+
return (await gitLines(root, ["branch", "--show-current"]))[0] ?? "DETACHED";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function calculateCandidateFingerprint(worktree) {
|
|
29
|
+
const root = await realpath(worktree);
|
|
30
|
+
const head = (await gitLines(root, ["rev-parse", "HEAD"]))[0] ?? "NO_HEAD";
|
|
31
|
+
const changed = new Set([
|
|
32
|
+
...(await gitLines(root, ["diff", "--name-only"])),
|
|
33
|
+
...(await gitLines(root, ["diff", "--cached", "--name-only"])),
|
|
34
|
+
...(await gitLines(root, ["ls-files", "--others", "--exclude-standard"])),
|
|
35
|
+
]);
|
|
36
|
+
const files = [];
|
|
37
|
+
for (const relativePath of [...changed].sort()) {
|
|
38
|
+
if (SENSITIVE_PATH.test(relativePath) || IGNORED_PATH.test(relativePath)) continue;
|
|
39
|
+
const absolutePath = path.resolve(root, relativePath);
|
|
40
|
+
if (!absolutePath.startsWith(`${root}${path.sep}`)) throw new Error(`unsafe changed path: ${relativePath}`);
|
|
41
|
+
let contentHash = "DELETED";
|
|
42
|
+
try {
|
|
43
|
+
const metadata = await stat(absolutePath);
|
|
44
|
+
if (metadata.isFile()) contentHash = digest(await readFile(absolutePath));
|
|
45
|
+
} catch (error) {
|
|
46
|
+
if (error.code !== "ENOENT") throw error;
|
|
47
|
+
}
|
|
48
|
+
files.push({ path: relativePath, sha256: contentHash });
|
|
49
|
+
}
|
|
50
|
+
const canonical = JSON.stringify({ git_head: head, files });
|
|
51
|
+
return { algorithm: "sha256", value: digest(canonical), git_head: head, files };
|
|
52
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { redactAndCap, writeLog } from "./evidence.mjs";
|
|
4
|
+
import { runProcess } from "./process.mjs";
|
|
5
|
+
|
|
6
|
+
function scalar(value) {
|
|
7
|
+
const trimmed = value.trim();
|
|
8
|
+
if (trimmed === "true") return true;
|
|
9
|
+
if (trimmed === "false") return false;
|
|
10
|
+
if (/^\d+$/.test(trimmed)) return Number(trimmed);
|
|
11
|
+
if (trimmed === "[]") return [];
|
|
12
|
+
return trimmed.replace(/^['"]|['"]$/g, "");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function unquoteWhole(value) {
|
|
16
|
+
const trimmed = value.trim();
|
|
17
|
+
if ((trimmed.startsWith("\"") && trimmed.endsWith("\"")) || (trimmed.startsWith("'") && trimmed.endsWith("'"))) {
|
|
18
|
+
return trimmed.slice(1, -1);
|
|
19
|
+
}
|
|
20
|
+
return trimmed;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Supports the deliberately small, list-of-maps YAML contract used by quality-gates.yaml.
|
|
24
|
+
export function parseQualityGates(source) {
|
|
25
|
+
try {
|
|
26
|
+
const parsed = JSON.parse(source);
|
|
27
|
+
return parsed.gates ?? [];
|
|
28
|
+
} catch {}
|
|
29
|
+
const gates = [];
|
|
30
|
+
let current = null;
|
|
31
|
+
for (const rawLine of source.split("\n")) {
|
|
32
|
+
const line = rawLine.replace(/\s+#.*$/, "");
|
|
33
|
+
const match = line.match(/^\s*(?:-\s+)?([a-z_]+):\s*(.*?)\s*$/i);
|
|
34
|
+
if (!match) continue;
|
|
35
|
+
const [, key, value] = match;
|
|
36
|
+
const parsedValue = key === "command" || key === "description" ? unquoteWhole(value) : scalar(value);
|
|
37
|
+
if (line.trimStart().startsWith("- ")) {
|
|
38
|
+
if (current) gates.push(current);
|
|
39
|
+
current = { [key]: parsedValue };
|
|
40
|
+
} else if (current) {
|
|
41
|
+
current[key] = parsedValue;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (current) gates.push(current);
|
|
45
|
+
return gates;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function loadTrustedGates(gatesPath, expectedHash) {
|
|
49
|
+
const source = await readFile(gatesPath, "utf8");
|
|
50
|
+
const actualHash = createHash("sha256").update(source).digest("hex");
|
|
51
|
+
if (!expectedHash || actualHash !== expectedHash) {
|
|
52
|
+
return { valid: false, actualHash, gates: [] };
|
|
53
|
+
}
|
|
54
|
+
return { valid: true, actualHash, gates: parseQualityGates(source) };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function executeGates(gates, worktree, logDirectory) {
|
|
58
|
+
const results = [];
|
|
59
|
+
for (const gate of gates) {
|
|
60
|
+
const required = gate.required !== false;
|
|
61
|
+
const executor = gate.executor ?? "control-plane";
|
|
62
|
+
if (executor !== "control-plane") {
|
|
63
|
+
results.push({ id: gate.id ?? "unknown", required, executor, status: "unsupported_executor", reason: `executor_${executor}_is_not_supported_in_v1` });
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (!gate.id || !gate.command || /^REPLACE_ME/.test(gate.command)) {
|
|
67
|
+
results.push({ id: gate.id ?? "unknown", required, executor, status: "invalid_gate", reason: "missing_trusted_command" });
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
const startedAt = Date.now();
|
|
71
|
+
const result = await runProcess(gate.command, [], { cwd: worktree, shell: true, timeoutMs: (gate.timeout_seconds ?? 600) * 1000 });
|
|
72
|
+
const stdout = redactAndCap(result.stdout);
|
|
73
|
+
const stderr = redactAndCap(result.stderr);
|
|
74
|
+
const stdoutLog = await writeLog(logDirectory, gate.id, "stdout", result.stdout);
|
|
75
|
+
const stderrLog = await writeLog(logDirectory, gate.id, "stderr", result.stderr);
|
|
76
|
+
results.push({
|
|
77
|
+
id: gate.id,
|
|
78
|
+
required,
|
|
79
|
+
executor,
|
|
80
|
+
status: result.timedOut ? "timeout" : result.exitCode === 0 ? "pass" : "fail",
|
|
81
|
+
exit_code: result.exitCode,
|
|
82
|
+
duration_ms: Date.now() - startedAt,
|
|
83
|
+
stdout_ref: stdoutLog.ref,
|
|
84
|
+
stderr_ref: stderrLog.ref,
|
|
85
|
+
output_truncated: stdout.truncated || stderr.truncated,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
return results;
|
|
89
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import process from "node:process";
|
|
3
|
+
|
|
4
|
+
export function runProcess(command, args = [], options = {}) {
|
|
5
|
+
const { cwd, timeoutMs = 600_000, shell = false, killGraceMs = 2_000 } = options;
|
|
6
|
+
return new Promise((resolve) => {
|
|
7
|
+
const detached = process.platform !== "win32";
|
|
8
|
+
const child = spawn(command, args, { cwd, shell, detached, stdio: ["ignore", "pipe", "pipe"] });
|
|
9
|
+
let stdout = "";
|
|
10
|
+
let stderr = "";
|
|
11
|
+
let timedOut = false;
|
|
12
|
+
let forceTimer;
|
|
13
|
+
const timer = setTimeout(() => {
|
|
14
|
+
timedOut = true;
|
|
15
|
+
signalProcess(child, "SIGTERM", detached);
|
|
16
|
+
forceTimer = setTimeout(() => signalProcess(child, "SIGKILL", detached), killGraceMs);
|
|
17
|
+
}, timeoutMs);
|
|
18
|
+
child.stdout.on("data", (chunk) => { stdout += chunk; });
|
|
19
|
+
child.stderr.on("data", (chunk) => { stderr += chunk; });
|
|
20
|
+
child.on("error", (error) => { stderr += `${error.message}\n`; });
|
|
21
|
+
child.on("close", (exitCode, signal) => {
|
|
22
|
+
clearTimeout(timer);
|
|
23
|
+
clearTimeout(forceTimer);
|
|
24
|
+
resolve({ exitCode: exitCode ?? 1, signal, timedOut, stdout, stderr });
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function signalProcess(child, signal, detached) {
|
|
30
|
+
try {
|
|
31
|
+
if (detached && child.pid) process.kill(-child.pid, signal);
|
|
32
|
+
else child.kill(signal);
|
|
33
|
+
} catch {
|
|
34
|
+
// The process may have exited between the timeout and the escalation.
|
|
35
|
+
}
|
|
36
|
+
}
|