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.
Files changed (93) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/CODE_OF_CONDUCT.md +9 -0
  3. package/CONTRIBUTING.md +44 -0
  4. package/LICENSE +21 -0
  5. package/README.md +80 -0
  6. package/SECURITY.md +12 -0
  7. package/adapters/antigravity/.agents/agents/triad-developer/agent.md +11 -0
  8. package/adapters/antigravity/.agents/agents/triad-evaluator/agent.md +11 -0
  9. package/adapters/antigravity/.agents/agents/triad-orchestrator/agent.md +19 -0
  10. package/adapters/antigravity/.agents/agents/triad-reviewer/agent.md +10 -0
  11. package/adapters/antigravity/.agents/skills/triad/SKILL.md +30 -0
  12. package/adapters/antigravity/.agents/skills/triad/agents/openai.yaml +4 -0
  13. package/adapters/antigravity/README.md +26 -0
  14. package/adapters/antigravity/install.sh +72 -0
  15. package/adapters/antigravity/runtime.json +6 -0
  16. package/adapters/claude-code/.claude/agents/triad-developer.md +19 -0
  17. package/adapters/claude-code/.claude/agents/triad-evaluator.md +17 -0
  18. package/adapters/claude-code/.claude/agents/triad-reviewer.md +14 -0
  19. package/adapters/claude-code/.claude/commands/triad.md +26 -0
  20. package/adapters/claude-code/README.md +60 -0
  21. package/adapters/claude-code/install.sh +75 -0
  22. package/adapters/claude-code/runtime.json +10 -0
  23. package/adapters/codex/README.md +39 -0
  24. package/adapters/codex/install.sh +78 -0
  25. package/adapters/codex/prompts/triad.md +25 -0
  26. package/adapters/codex/runtime.json +11 -0
  27. package/adapters/hermes/README.md +31 -0
  28. package/adapters/hermes/install.sh +45 -0
  29. package/adapters/hermes/runtime.json +7 -0
  30. package/adapters/hermes/skills/triad/SKILL.md +36 -0
  31. package/adapters/hermes/skills/triad/agents/openai.yaml +4 -0
  32. package/adapters/opencode/.opencode/agents/triad-developer.md +32 -0
  33. package/adapters/opencode/.opencode/agents/triad-evaluator.md +29 -0
  34. package/adapters/opencode/.opencode/agents/triad-orchestrator.md +60 -0
  35. package/adapters/opencode/.opencode/agents/triad-reviewer.md +32 -0
  36. package/adapters/opencode/.opencode/commands/triad.md +24 -0
  37. package/adapters/opencode/README.md +111 -0
  38. package/adapters/opencode/install.sh +75 -0
  39. package/adapters/opencode/runtime.json +6 -0
  40. package/adapters/registry.mjs +224 -0
  41. package/assets/triad-plus-engineering-loop-icon.svg +16 -0
  42. package/bin/triad-plus.js +311 -0
  43. package/docs/antigravity-replication.md +9 -0
  44. package/docs/architecture.md +24 -0
  45. package/docs/claude-code-replication.md +9 -0
  46. package/docs/codex-replication.md +10 -0
  47. package/docs/compatibility.md +13 -0
  48. package/docs/configuration.md +27 -0
  49. package/docs/evaluator-plus.md +13 -0
  50. package/docs/getting-started.md +33 -0
  51. package/docs/npx-installation.md +44 -0
  52. package/docs/opencode-replication.md +8 -0
  53. package/docs/operating-guide.it.md +49 -0
  54. package/docs/operating-guide.md +49 -0
  55. package/docs/runtimes.md +24 -0
  56. package/docs/troubleshooting.md +22 -0
  57. package/docs/verification.md +17 -0
  58. package/integrations/claude-code/README.md +25 -0
  59. package/integrations/claude-code/hooks.json +17 -0
  60. package/integrations/codex/README.md +61 -0
  61. package/integrations/codex/hooks.json +20 -0
  62. package/package.json +51 -0
  63. package/runtime/legacy-adapters.json +17 -0
  64. package/runtime/lib/evidence.mjs +31 -0
  65. package/runtime/lib/fingerprint.mjs +52 -0
  66. package/runtime/lib/gates.mjs +89 -0
  67. package/runtime/lib/process.mjs +36 -0
  68. package/runtime/triad-runtime-capabilities.mjs +131 -0
  69. package/runtime/triad-verify.mjs +152 -0
  70. package/schemas/evaluator-plus-result.schema.json +17 -0
  71. package/schemas/runtime-capabilities.schema.json +15 -0
  72. package/schemas/verification-evidence.schema.json +21 -0
  73. package/skills/triad-loop-bootstrap/SKILL.md +43 -0
  74. package/skills/triad-loop-bootstrap/agents/openai.yaml +4 -0
  75. package/skills/triad-loop-bootstrap/assets/loop-template/decision-policy.md +22 -0
  76. package/skills/triad-loop-bootstrap/assets/loop-template/feature-card.template.md +32 -0
  77. package/skills/triad-loop-bootstrap/assets/loop-template/feature-plan.template.md +13 -0
  78. package/skills/triad-loop-bootstrap/assets/loop-template/handoff-report.template.md +60 -0
  79. package/skills/triad-loop-bootstrap/assets/loop-template/quality-gates.yaml +20 -0
  80. package/skills/triad-loop-bootstrap/assets/loop-template/review-report.template.md +32 -0
  81. package/skills/triad-loop-bootstrap/assets/loop-template/run-state.yaml +22 -0
  82. package/skills/triad-loop-bootstrap/assets/loop-template/runtime/assignments/assignment.template.json +21 -0
  83. package/skills/triad-loop-bootstrap/assets/loop-template/runtime/capabilities.template.json +27 -0
  84. package/skills/triad-loop-bootstrap/assets/loop-template/work-queue.yaml +28 -0
  85. package/skills/triad-loop-bootstrap/assets/project.yaml +55 -0
  86. package/skills/triad-loop-developer/SKILL.md +22 -0
  87. package/skills/triad-loop-developer/agents/openai.yaml +4 -0
  88. package/skills/triad-loop-evaluator/SKILL.md +24 -0
  89. package/skills/triad-loop-evaluator/agents/openai.yaml +4 -0
  90. package/skills/triad-loop-orchestrator/SKILL.md +54 -0
  91. package/skills/triad-loop-orchestrator/agents/openai.yaml +4 -0
  92. package/skills/triad-loop-reviewer/SKILL.md +25 -0
  93. 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).
@@ -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
+ }