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,131 @@
1
+ #!/usr/bin/env node
2
+ import { readFile } from "node:fs/promises";
3
+ import { spawnSync } from "node:child_process";
4
+ import path from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+
7
+ const argv = process.argv.slice(2);
8
+ const option = (name) => {
9
+ const index = argv.indexOf(name);
10
+ return index >= 0 ? argv[index + 1] : null;
11
+ };
12
+
13
+ function versionParts(value) {
14
+ const match = String(value).match(/(\d+)\.(\d+)\.(\d+)/);
15
+ return match ? match.slice(1).map(Number) : null;
16
+ }
17
+
18
+ function atLeast(actual, minimum) {
19
+ const a = versionParts(actual);
20
+ const b = versionParts(minimum);
21
+ if (!a || !b) return false;
22
+ for (let index = 0; index < a.length; index += 1) {
23
+ if (a[index] > b[index]) return true;
24
+ if (a[index] < b[index]) return false;
25
+ }
26
+ return true;
27
+ }
28
+
29
+ function expandHome(candidate) {
30
+ return candidate.startsWith("~/") ? path.join(process.env.HOME ?? "", candidate.slice(2)) : candidate;
31
+ }
32
+
33
+ function commandVersion(binary, overriddenOutput) {
34
+ const candidates = Array.isArray(binary) ? binary : [binary];
35
+ for (const candidate of candidates) {
36
+ const result = overriddenOutput ? { status: 0, stdout: overriddenOutput } : spawnSync(expandHome(candidate), ["--version"], { encoding: "utf8" });
37
+ if (result.status === 0) return { binary: expandHome(candidate), version: versionParts(result.stdout)?.join(".") ?? null };
38
+ }
39
+ return { binary: expandHome(candidates[0]), version: null };
40
+ }
41
+
42
+ function validHookRule(rule, lifecycle) {
43
+ if (!rule || typeof rule !== "object") return false;
44
+ let matcher;
45
+ try {
46
+ matcher = new RegExp(rule.matcher ?? "");
47
+ } catch {
48
+ return false;
49
+ }
50
+ if (!matcher.test(lifecycle.agent_type)) return false;
51
+ const hooks = Array.isArray(rule.hooks) ? rule.hooks : [];
52
+ return hooks.some((hook) => {
53
+ const command = String(hook?.command ?? "");
54
+ return hook?.type === "command" &&
55
+ !command.includes("<TRIAD_") &&
56
+ command.includes("triad-verify.mjs") &&
57
+ command.includes("--project") &&
58
+ (!lifecycle.requires_async || hook.async === true);
59
+ });
60
+ }
61
+
62
+ async function loadAdapter() {
63
+ const adapterPath = option("--adapter");
64
+ if (adapterPath) return JSON.parse(await readFile(path.resolve(adapterPath), "utf8"));
65
+ const adapters = JSON.parse(await readFile(new URL("./legacy-adapters.json", import.meta.url), "utf8"));
66
+ const id = option("--host") ?? "codex";
67
+ const adapter = adapters[id];
68
+ if (!adapter) throw new Error(`unsupported adapter: ${id}`);
69
+ return adapter;
70
+ }
71
+
72
+ async function hookConfiguration(pathname, lifecycle) {
73
+ if (!lifecycle) return { configured: false, reason: "adapter_has_no_lifecycle_hook", minimum: null };
74
+ if (!pathname) return { configured: false, reason: "hook_configuration_not_declared", minimum: lifecycle.minimum_version ?? null };
75
+ try {
76
+ const source = await readFile(pathname, "utf8");
77
+ const parsed = JSON.parse(source);
78
+ const rules = parsed?.hooks?.[lifecycle.kind];
79
+ if (!Array.isArray(rules) || !rules.some((rule) => validHookRule(rule, lifecycle))) {
80
+ return { configured: false, reason: "hook_configuration_has_no_usable_triad_rule", minimum: parsed.minimum_version ?? parsed.minimum_codex_cli_version ?? lifecycle.minimum_version ?? null };
81
+ }
82
+ return { configured: true, reason: null, minimum: parsed.minimum_version ?? parsed.minimum_codex_cli_version ?? lifecycle.minimum_version ?? null };
83
+ } catch (error) {
84
+ return { configured: false, reason: `hook_configuration_unreadable:${error.code ?? "error"}`, minimum: lifecycle.minimum_version ?? null };
85
+ }
86
+ }
87
+
88
+ const requestedMode = option("--requested-mode") ?? "auto";
89
+ const versionOutput = option("--version-output");
90
+ const adapter = await loadAdapter();
91
+ if (adapter?.schema_version !== 1 || typeof adapter.id !== "string" || typeof adapter.binary !== "string") {
92
+ throw new Error("adapter metadata must contain schema_version 1, id, and binary");
93
+ }
94
+ const lifecycle = adapter.lifecycle ?? null;
95
+ const hostRuntime = commandVersion(option("--host-bin") ?? adapter.binary_candidates ?? adapter.binary, versionOutput);
96
+ const hostBinary = hostRuntime.binary;
97
+ const hostVersion = hostRuntime.version;
98
+ const nodeBinary = option("--node-bin") ?? "node";
99
+ const nodeRuntime = commandVersion(nodeBinary);
100
+ const nodeVersion = nodeRuntime.version;
101
+ const hook = await hookConfiguration(option("--hook-config"), lifecycle);
102
+ const lifecycleAvailable = Boolean(lifecycle && hostVersion && (!hook.minimum || atLeast(hostVersion, hook.minimum)));
103
+ const hookAvailable = lifecycleAvailable && hook.configured;
104
+ const explicitAvailable = Boolean(hostVersion && nodeVersion);
105
+ let selectedMode = "unavailable";
106
+ let reason = "verification_runtime_unavailable";
107
+ if (hookAvailable && (requestedMode === "auto" || requestedMode === "async_hook" || requestedMode === "hook_dispatch")) {
108
+ selectedMode = lifecycle.requires_async ? "async_hook" : "hook_dispatch";
109
+ reason = lifecycle.requires_async ? "validated_async_hook_available" : "validated_hook_dispatch_available";
110
+ } else if (explicitAvailable) {
111
+ selectedMode = "explicit_dispatch";
112
+ reason = ["async_hook", "hook_dispatch"].includes(requestedMode)
113
+ ? "requested_hook_unavailable_using_explicit_dispatch"
114
+ : "explicit_dispatch_available";
115
+ }
116
+
117
+ process.stdout.write(`${JSON.stringify({
118
+ schema_version: 1,
119
+ detected_at: new Date().toISOString(),
120
+ host: adapter.id,
121
+ host_runtime: { binary: hostBinary, version: hostVersion, available: Boolean(hostVersion) },
122
+ verifier_runtime: { binary: nodeBinary, version: nodeVersion, available: Boolean(nodeVersion) },
123
+ lifecycle_async: {
124
+ kind: lifecycle?.kind ?? null,
125
+ minimum_version: hook.minimum,
126
+ available: lifecycleAvailable,
127
+ configured: hook.configured,
128
+ reason: hook.reason
129
+ },
130
+ verification: { requested_mode: requestedMode, selected_mode: selectedMode, reason }
131
+ }, null, 2)}\n`);
@@ -0,0 +1,152 @@
1
+ #!/usr/bin/env node
2
+ import { createHash, randomUUID } from "node:crypto";
3
+ import { access, readFile, realpath } from "node:fs/promises";
4
+ import path from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { writeAtomicJson } from "./lib/evidence.mjs";
7
+ import { calculateCandidateFingerprint, worktreeBranch } from "./lib/fingerprint.mjs";
8
+ import { executeGates, loadTrustedGates } from "./lib/gates.mjs";
9
+
10
+ const argv = process.argv.slice(2);
11
+ const option = (name) => {
12
+ const index = argv.indexOf(name);
13
+ return index >= 0 ? argv[index + 1] : null;
14
+ };
15
+
16
+ async function readStdin() {
17
+ let input = "";
18
+ for await (const chunk of process.stdin) input += chunk;
19
+ return input.trim() ? JSON.parse(input) : {};
20
+ }
21
+
22
+ function sha256(value) {
23
+ return createHash("sha256").update(value).digest("hex");
24
+ }
25
+
26
+ async function sha256File(value) {
27
+ return sha256(await readFile(value));
28
+ }
29
+
30
+ function triggerFrom(payload) {
31
+ return {
32
+ event: payload.event ?? payload.hook_event_name ?? "manual",
33
+ session_id: payload.session_id ?? payload.sessionId ?? null,
34
+ agent_id: payload.agent_id ?? payload.agentId ?? payload?.agent?.id ?? null,
35
+ agent_type: payload.agent_type ?? payload.agentType ?? payload?.agent?.type ?? null,
36
+ };
37
+ }
38
+
39
+ async function resolveAssignment(projectRoot, trigger, explicitAssignment) {
40
+ const assignmentPath = explicitAssignment
41
+ ? path.resolve(explicitAssignment)
42
+ : path.join(projectRoot, ".loop", "runtime", "assignments", `${trigger.agent_id}.json`);
43
+ const source = await readFile(assignmentPath, "utf8");
44
+ return { assignmentPath, assignment: JSON.parse(source), assignmentHash: sha256(source) };
45
+ }
46
+
47
+ async function buildInvalidEvidence({ runId, trigger, assignment, reason, outputPath }) {
48
+ const evidence = {
49
+ schema_version: 1,
50
+ run_id: runId,
51
+ feature_id: assignment?.feature_id ?? "unknown",
52
+ attempt: assignment?.attempt ?? null,
53
+ assignment_id: assignment?.assignment_id ?? null,
54
+ assignment_sha256: null,
55
+ trigger,
56
+ baseline: {
57
+ prd_sha256: assignment?.expected_prd_sha256 ?? null,
58
+ card_sha256: assignment?.expected_card_sha256 ?? null,
59
+ git_head: null,
60
+ candidate_fingerprint: null,
61
+ },
62
+ gates: [],
63
+ required_gates_passed: false,
64
+ status: "invalid_context",
65
+ failure: { code: "verification_context_invalid", reason },
66
+ created_at: new Date().toISOString(),
67
+ };
68
+ if (outputPath) await writeAtomicJson(outputPath, evidence);
69
+ return evidence;
70
+ }
71
+
72
+ async function main() {
73
+ const payload = await readStdin();
74
+ const trigger = triggerFrom(payload);
75
+ const requestedRunId = payload.run_id ?? option("--run-id") ?? null;
76
+ let runId = requestedRunId ?? randomUUID();
77
+ const projectArgument = option("--project") ?? payload.project_root ?? process.cwd();
78
+ const projectRoot = await realpath(projectArgument);
79
+ let assignment;
80
+ let assignmentPath;
81
+ let outputPath;
82
+ try {
83
+ let assignmentHash;
84
+ ({ assignmentPath, assignment, assignmentHash } = await resolveAssignment(projectRoot, trigger, option("--assignment")));
85
+ if (assignment.verification_run_id && requestedRunId && assignment.verification_run_id !== requestedRunId) throw new Error("verification run ID does not match assignment");
86
+ runId = assignment.verification_run_id ?? runId;
87
+ const evidenceDirectory = assignment.evidence_directory
88
+ ? path.resolve(projectRoot, assignment.evidence_directory)
89
+ : path.join(projectRoot, ".loop", "evidence", assignment.feature_id, `attempt-${String(assignment.attempt).padStart(3, "0")}`);
90
+ outputPath = path.join(evidenceDirectory, "verification.json");
91
+ if (assignment.status !== "active") throw new Error("assignment is not active");
92
+ if (!assignment.assignment_id) throw new Error("assignment ID is required");
93
+ if (assignment.agent_id !== trigger.agent_id || assignment.agent_type !== "triad_developer") throw new Error("developer assignment does not match trigger");
94
+ if ((await realpath(path.resolve(assignment.project_root ?? projectRoot))) !== projectRoot) throw new Error("assignment project root mismatch");
95
+ const worktree = await realpath(path.resolve(projectRoot, assignment.worktree));
96
+ if (!worktree.startsWith(`${projectRoot}${path.sep}`) && !assignment.allow_external_worktree) throw new Error("undeclared external worktree");
97
+ const prdPath = path.resolve(projectRoot, assignment.prd_path ?? "artifacts/prd.md");
98
+ const cardPath = path.resolve(projectRoot, assignment.card_path);
99
+ await access(prdPath);
100
+ await access(cardPath);
101
+ if ((await sha256File(prdPath)) !== assignment.expected_prd_sha256) throw new Error("PRD baseline hash mismatch");
102
+ if ((await sha256File(cardPath)) !== assignment.expected_card_sha256) throw new Error("feature card hash mismatch");
103
+ const before = await calculateCandidateFingerprint(worktree);
104
+ const branch = await worktreeBranch(worktree);
105
+ if (assignment.expected_branch && assignment.expected_branch !== branch) throw new Error("worktree branch does not match assignment");
106
+ const gatesPath = path.resolve(projectRoot, assignment.gates_path ?? ".loop/quality-gates.yaml");
107
+ const trusted = await loadTrustedGates(gatesPath, assignment.expected_gates_sha256);
108
+ if (!trusted.valid) throw new Error("quality gates are missing or changed from their declared hash");
109
+ const logDirectory = path.join(evidenceDirectory, "logs");
110
+ const gates = await executeGates(trusted.gates, worktree, logDirectory);
111
+ const after = await calculateCandidateFingerprint(worktree);
112
+ const candidateChanged = before.value !== after.value;
113
+ const requiredGatesPassed = !candidateChanged && gates.filter((gate) => gate.required).every((gate) => gate.status === "pass");
114
+ const evidence = {
115
+ schema_version: 1,
116
+ run_id: runId,
117
+ feature_id: assignment.feature_id,
118
+ attempt: assignment.attempt,
119
+ assignment_id: assignment.assignment_id,
120
+ assignment_sha256: assignmentHash,
121
+ trigger,
122
+ assignment_ref: path.relative(projectRoot, assignmentPath),
123
+ baseline: {
124
+ prd_sha256: assignment.expected_prd_sha256,
125
+ card_sha256: assignment.expected_card_sha256,
126
+ gates_sha256: trusted.actualHash,
127
+ git_head: before.git_head,
128
+ candidate_fingerprint: before.value,
129
+ branch,
130
+ },
131
+ gates,
132
+ required_gates_passed: requiredGatesPassed,
133
+ status: candidateChanged ? "invalidated" : requiredGatesPassed ? "pass" : "fail",
134
+ failure: candidateChanged ? { code: "candidate_changed_after_verification", reason: "worktree changed while gates ran" } : null,
135
+ created_at: new Date().toISOString(),
136
+ };
137
+ await writeAtomicJson(outputPath, evidence);
138
+ process.stdout.write(`${JSON.stringify({ run_id: runId, status: evidence.status, evidence: outputPath })}\n`);
139
+ process.exitCode = evidence.status === "pass" ? 0 : 2;
140
+ } catch (error) {
141
+ const fallbackAssignment = assignment ?? null;
142
+ if (!outputPath && fallbackAssignment?.feature_id) {
143
+ const directory = path.join(projectRoot, ".loop", "evidence", fallbackAssignment.feature_id, `attempt-${String(fallbackAssignment.attempt).padStart(3, "0")}`);
144
+ outputPath = path.join(directory, "verification.json");
145
+ }
146
+ const evidence = await buildInvalidEvidence({ runId, trigger, assignment: fallbackAssignment, reason: error.message, outputPath });
147
+ process.stdout.write(`${JSON.stringify({ run_id: runId, status: evidence.status, evidence: outputPath ?? null })}\n`);
148
+ process.exitCode = 3;
149
+ }
150
+ }
151
+
152
+ if (process.argv[1] === fileURLToPath(import.meta.url)) main();
@@ -0,0 +1,17 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://triad.engineering/schemas/evaluator-plus-result.schema.json",
4
+ "title": "Triad Evaluator+ report",
5
+ "type": "object",
6
+ "required": ["schema_version", "feature_id", "candidate_fingerprint", "verdict", "summary", "evidence_refs", "created_at"],
7
+ "properties": {
8
+ "schema_version": { "const": 1 },
9
+ "feature_id": { "type": "string", "minLength": 1 },
10
+ "candidate_fingerprint": { "type": "string", "minLength": 1 },
11
+ "verdict": { "enum": ["PASS", "FAIL", "INDETERMINATE"] },
12
+ "summary": { "type": "string", "minLength": 1 },
13
+ "evidence_refs": { "type": "array", "items": { "type": "string" } },
14
+ "created_at": { "type": "string", "format": "date-time" }
15
+ },
16
+ "additionalProperties": false
17
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://triad.engineering/schemas/runtime-capabilities.schema.json",
4
+ "title": "Triad runtime capability snapshot",
5
+ "type": "object",
6
+ "required": ["schema_version", "detected_at", "host", "host_runtime", "verifier_runtime", "lifecycle_async", "verification"],
7
+ "properties": {
8
+ "schema_version": { "const": 1 },
9
+ "host": { "enum": ["codex", "opencode", "claude-code", "antigravity"] },
10
+ "host_runtime": { "type": "object", "required": ["binary", "version", "available"] },
11
+ "verifier_runtime": { "type": "object", "required": ["binary", "version", "available"] },
12
+ "lifecycle_async": { "type": "object", "required": ["kind", "minimum_version", "available", "configured", "reason"] },
13
+ "verification": { "type": "object", "required": ["requested_mode", "selected_mode", "reason"], "properties": { "requested_mode": { "enum": ["auto", "async_hook", "hook_dispatch", "explicit_dispatch"] }, "selected_mode": { "enum": ["async_hook", "hook_dispatch", "explicit_dispatch", "unavailable"] }, "reason": { "type": "string" } } }
14
+ }
15
+ }
@@ -0,0 +1,21 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://triad.engineering/schemas/verification-evidence.schema.json",
4
+ "title": "Triad verification evidence",
5
+ "type": "object",
6
+ "required": ["schema_version", "run_id", "feature_id", "attempt", "assignment_id", "assignment_sha256", "trigger", "baseline", "gates", "required_gates_passed", "status", "created_at"],
7
+ "properties": {
8
+ "schema_version": { "const": 1 },
9
+ "run_id": { "type": "string", "minLength": 1 },
10
+ "feature_id": { "type": "string", "minLength": 1 },
11
+ "attempt": { "type": ["integer", "null"], "minimum": 1 },
12
+ "assignment_id": { "type": ["string", "null"] },
13
+ "assignment_sha256": { "type": ["string", "null"] },
14
+ "trigger": { "type": "object", "required": ["event", "agent_id", "agent_type"], "properties": { "event": { "type": "string" }, "agent_id": { "type": ["string", "null"] }, "agent_type": { "type": ["string", "null"] } } },
15
+ "baseline": { "type": "object", "required": ["prd_sha256", "card_sha256", "git_head", "candidate_fingerprint"], "properties": { "prd_sha256": { "type": ["string", "null"] }, "card_sha256": { "type": ["string", "null"] }, "git_head": { "type": ["string", "null"] }, "candidate_fingerprint": { "type": ["string", "null"] }, "branch": { "type": ["string", "null"] } } },
16
+ "gates": { "type": "array" },
17
+ "required_gates_passed": { "type": "boolean" },
18
+ "status": { "enum": ["pass", "fail", "invalid_context", "infrastructure_error", "invalidated"] },
19
+ "created_at": { "type": "string", "format": "date-time" }
20
+ }
21
+ }
@@ -0,0 +1,43 @@
1
+ ---
2
+ name: triad-loop-bootstrap
3
+ description: Create an isolated Triad project-control workspace from a PRD, including a frozen PRD baseline, manifest, feature plan, core work queue, verification templates, and delivery evidence templates. Use before the Orchestrator starts a new software initiative.
4
+ ---
5
+
6
+ # Triad Bootstrap
7
+
8
+ Create the project-control workspace outside product repositories. It is the
9
+ human/agent record of planning and evidence; it is not an executable workflow
10
+ engine.
11
+
12
+ Collect only missing essentials: project ID/title, readable PRD source, target
13
+ repositories with base/project branches and worktrees, measurable success
14
+ conditions, runnable quality gates, practical-test need, and integration need.
15
+
16
+ 1. Create `<control-repository>/projects/<project-id>/`.
17
+ 2. Copy `assets/project.yaml` to `project.yaml`; register only declared product
18
+ repositories, branches, and worktrees.
19
+ 3. Copy the approved PRD to `artifacts/prd.md`; record source, collection time,
20
+ snapshot, revision when available, and SHA-256.
21
+ 4. Copy `assets/loop-template/` to `.loop/`, then create bounded feature cards
22
+ under `features/` and a complete `feature-plan.md`.
23
+ 5. Replace every gate placeholder. Gate executors in v1 are only
24
+ `control-plane`; remove a non-applicable gate with a recorded reason instead
25
+ of declaring manual or MCP execution.
26
+ 6. Create `.loop/runtime/assignments/` and record the active adapter metadata in
27
+ `.loop/runtime/capabilities.json` by running
28
+ `.triad-runtime/triad-runtime-capabilities.mjs --adapter
29
+ .triad-runtime/adapter.json`.
30
+ 7. Show the full card division before delivery work. Continue unless the owner
31
+ changes it or an escalation condition exists.
32
+
33
+ The normal state route is `draft → ready → in_progress → verifying → in_review
34
+ → approved|rework|blocked`. The Orchestrator owns those records. Before each
35
+ Developer dispatch, write an active assignment with a unique assignment ID,
36
+ feature, attempt, expected branch, worktree, PRD/card/gate hashes, and verifier
37
+ run ID. The verifier produces environment-derived evidence; it never changes
38
+ card state.
39
+
40
+ Use local worktrees for enabled multi-repository integration and declare a final
41
+ integration card. Do not publish packages, alter registry versions, or create a
42
+ release as a bootstrap/integration step. Re-baseline only after an explicit owner
43
+ requirement change; preserve the previous PRD snapshot and record both hashes.
@@ -0,0 +1,4 @@
1
+ interface:
2
+ display_name: "Triad Loop Bootstrap"
3
+ short_description: "Prepare an evidence-driven loop"
4
+ default_prompt: "Use $triad-loop-bootstrap to prepare a triad engineering project from a PRD."
@@ -0,0 +1,22 @@
1
+ # Triad decision policy
2
+
3
+ | Current state | Next state | Owner | Required basis |
4
+ | --- | --- | --- | --- |
5
+ | `draft` | `ready` | Orchestrator | Complete card, declared plan, runnable gates. |
6
+ | `ready` | `in_progress` | Orchestrator | Dependencies approved and new attempt recorded. |
7
+ | `in_progress` | `verifying` | Orchestrator | Developer leaves a bounded candidate. |
8
+ | `verifying` | `in_review` | Orchestrator | Current passing environment-derived verifier evidence. |
9
+ | `verifying` | `rework` | Orchestrator | Failed, stale, missing, invalid, timed-out, or invalidated evidence. |
10
+ | `in_review` | `approved` | Orchestrator | Reviewer approves against current evidence. |
11
+ | `in_review` | `rework` | Orchestrator | Reviewer gives bounded actionable findings. |
12
+ | active state | `blocked` | Orchestrator | Material owner decision or external blocker. |
13
+ | `blocked` | `ready` or `deferred` | Orchestrator | Owner records the required decision. |
14
+
15
+ Only the Orchestrator records transitions. The verifier writes evidence only.
16
+ Developer and Reviewer reports are agent-reported claims; a `control-plane` gate
17
+ is authoritative only through matching verifier evidence. Every new patch needs
18
+ a new verifier run. Stop automatic retry at the declared limit and record the
19
+ owner decision needed. Normal delivery requires all non-deferred cards approved,
20
+ project gates passed, one local commit per approved card, and declared branches
21
+ pushed. Pull requests, releases, publication, force pushes, and demo start/stop
22
+ remain owner decisions.
@@ -0,0 +1,32 @@
1
+ # <feature ID> — <title>
2
+
3
+ ## Outcome and scope
4
+
5
+ - Target repository: `<repository ID or integration/local-worktrees>`
6
+ - Branch/worktree: `<declared branch and worktree>`
7
+ - Outcome: `<observable user or system result>`
8
+ - In scope: `<allowed files, behavior, and tests>`
9
+ - Out of scope: `<prohibited changes>`
10
+ - Dependencies: `<approved card IDs or none>`
11
+
12
+ ## Acceptance criteria
13
+
14
+ 1. `<pass/fail observable criterion>`
15
+ 2. `<pass/fail observable criterion>`
16
+
17
+ ## Metrics and gates
18
+
19
+ | ID | Target | Evidence command or observation |
20
+ | --- | --- | --- |
21
+ | `<metric>` | `<exact target>` | `<command/measurement>` |
22
+
23
+ - Required gates: `<gate IDs>`
24
+ - Allowed dependencies: `<names or none>`
25
+ - Test fixtures/examples: `<paths>`
26
+
27
+ ## Integration, practical test, and risk
28
+
29
+ - Local-worktree setup: `<not applicable or command>`
30
+ - Data/event/callback scenario: `<not applicable or exact scenario>`
31
+ - Owner practical test: `<steps and expected result>`
32
+ - Risks and rollback: `<known risk and recovery>`
@@ -0,0 +1,13 @@
1
+ # Feature plan — <project ID>
2
+
3
+ PRD baseline SHA-256: `<project.prd_baseline.sha256>`
4
+
5
+ | ID | Outcome | Repository | Depends on | Metrics | Required gates | Optimization | Owner practical test |
6
+ | --- | --- | --- | --- | --- | --- | --- | --- |
7
+ | `<ID>` | `<observable result>` | `<repository ID or integration/local-worktrees>` | `<IDs or none>` | `<exact target>` | `<gate IDs>` | `<none or bar/enforcement>` | `<scenario/environment>` |
8
+
9
+ Plan status: `declared | changed_by_owner`
10
+
11
+ Show this complete table before delegating the first card. Continue unless the
12
+ owner changes it or an escalation condition exists. If integration is enabled,
13
+ include one final card whose dependencies are all component cards.
@@ -0,0 +1,60 @@
1
+ # Delivery handoff — <project ID>
2
+
3
+ ## Decision
4
+
5
+ `delivered | delivery_blocked | delivered_without_demo`
6
+
7
+ ## Evidence
8
+
9
+ - PRD baseline: `<source and SHA-256>`
10
+ - Approved cards: `<IDs and count>`
11
+ - Branches and commits: `<repository: branch @ SHA>`
12
+ - Push evidence: `<remote and branches>`
13
+ - Gates and metrics: `<summary>`
14
+ - Deferred items or waivers: `<none or owner decision>`
15
+ - Known risks: `<none or list>`
16
+
17
+ ## Review and verification audit
18
+
19
+ | Feature | Verification runs | Final candidate fingerprint | Reviewer decision | Optional Evaluator+ report |
20
+ | --- | --- | --- | --- | --- |
21
+ | `<ID>` | `<count>` | `<SHA-256>` | `<approved|rework|blocked>` | `<none or report reference>` |
22
+
23
+ - Evidence/review chain: `<verification/review/evaluator references>`
24
+
25
+ ## Local-worktree integration
26
+
27
+ | Card | Consumer worktree | Provider worktree | Branch/commit map | Setup command | Data/event/callback gate |
28
+ | --- | --- | --- | --- | --- |
29
+ | `<not applicable or ID>` | `<path>` | `<path>` | `<repo: branch @ SHA>` | `<command/result>` | `<command/result>` |
30
+
31
+ Package publication/release: `not performed by this loop`
32
+
33
+ ## Orchestrator exception audit
34
+
35
+ - Exceptions: `<0 or count>` / `<total cards>`
36
+ - Normal roles restored: `<yes/no; evidence>`
37
+
38
+ | Exception ID | Trigger | Scope | Independent review | Return to normal delegation |
39
+ | --- | --- | --- | --- |
40
+ | `<none or ID>` | `<reason>` | `<bounded task>` | `<report/waiver>` | `<when/how>` |
41
+
42
+ ## Owner-controlled demo
43
+
44
+ State: `not_applicable | ready_to_start | active | closed | start_failed`
45
+
46
+ | Service | Worktree | Command | Local check | Remote URL | Remote check | Process group |
47
+ | --- | --- | --- | --- | --- | --- |
48
+ | `<id>` | `<path>` | `<command>` | `<not started/pass/fail>` | `<declared URL>` | `<not started/pass/fail>` | `<none or reference>` |
49
+
50
+ - Start: `<only on owner request>`
51
+ - Stop: `<only on owner completion>`
52
+ - Closure evidence: `<timestamp, process result, port-release check>`
53
+
54
+ ## Practical test and follow-up
55
+
56
+ | Feature | Environment | Steps | Expected result | Owner result |
57
+ | --- | --- | --- | --- | --- |
58
+ | `<ID>` | `<URL or none>` | `<steps>` | `<observable result>` | `to be tested` |
59
+
60
+ Feedback does not reopen this handoff. Follow-up: `none | feature card <ID> | successor project <ID> | owner decision required`.
@@ -0,0 +1,20 @@
1
+ version: 2
2
+ gates:
3
+ - id: test
4
+ description: Replace with the project test command.
5
+ command: REPLACE_ME_TEST_COMMAND
6
+ required: true
7
+ executor: control-plane
8
+ timeout_seconds: 600
9
+ - id: lint
10
+ description: Replace with the project lint command or remove with a recorded reason.
11
+ command: REPLACE_ME_LINT_COMMAND
12
+ required: true
13
+ executor: control-plane
14
+ timeout_seconds: 600
15
+ - id: build
16
+ description: Replace with the project build command or remove with a recorded reason.
17
+ command: REPLACE_ME_BUILD_COMMAND
18
+ required: true
19
+ executor: control-plane
20
+ timeout_seconds: 600
@@ -0,0 +1,32 @@
1
+ # Review — <feature ID> attempt <n>
2
+
3
+ ## Scope reviewed
4
+
5
+ - Branch/worktree: `<branch and path>`
6
+ - Commit or diff: `<SHA/range>`
7
+ - Card and PRD baseline: `<references>`
8
+ - Candidate fingerprint: `<verification/reviewed candidate SHA-256>`
9
+ - External verification evidence: `<run ID, status, and reference>`
10
+ - Quality bar/evaluation trail: `<not applicable or ID/hash/rounds/stop>`
11
+
12
+ ## Findings
13
+
14
+ | Severity | Finding | Evidence | Required action |
15
+ | --- | --- | --- | --- |
16
+ | `<critical/high/medium/low>` | `<finding>` | `<file/command/observation>` | `<action>` |
17
+
18
+ ## Independent evidence
19
+
20
+ - Acceptance criteria: `<pass/fail mapping>`
21
+ - Metrics: `<pass/fail mapping>`
22
+ - Gates rerun: `<command/result>`
23
+ - Scope/dependency check: `<result>`
24
+ - Evidence freshness and state-transition check: `<pass/fail>`
25
+ - Residual quality gap: `<none or preserved aspirational gap>`
26
+ - Residual risk: `<none or exact risk>`
27
+
28
+ ## Recommendation
29
+
30
+ `approved | rework | blocked`
31
+
32
+ Rationale: `<evidence-based rationale>`
@@ -0,0 +1,22 @@
1
+ version: 2
2
+ updated_at: null
3
+ project_decision: not_started
4
+
5
+ feature_plan:
6
+ path: feature-plan.md
7
+ status: declared
8
+ presented_at: null
9
+
10
+ bootstrap_verification: []
11
+ attempts: []
12
+ verification_runs: []
13
+ post_run_evaluations: []
14
+ runtime_errors: []
15
+ runtime_capabilities: []
16
+ escalations: []
17
+ arbitrations: []
18
+ coordinator_exceptions: []
19
+ git_delivery: []
20
+ post_delivery_feedback: []
21
+ demo_sessions: []
22
+ prd_rebaselines: []
@@ -0,0 +1,21 @@
1
+ {
2
+ "schema_version": 1,
3
+ "assignment_id": "REPLACE_ME_UUID",
4
+ "status": "active",
5
+ "agent_id": "REPLACE_ME_RUNTIME_AGENT_ID",
6
+ "agent_type": "triad_developer",
7
+ "feature_id": "REPLACE_ME_FEATURE_ID",
8
+ "attempt": 1,
9
+ "project_root": "REPLACE_ME_ABSOLUTE_CONTROL_WORKSPACE",
10
+ "worktree": "REPLACE_ME_DECLARED_WORKTREE_OR_ABSOLUTE_PATH",
11
+ "expected_branch": "REPLACE_ME_PROJECT_BRANCH",
12
+ "allow_external_worktree": false,
13
+ "prd_path": "artifacts/prd.md",
14
+ "card_path": "features/REPLACE_ME_FEATURE_ID.md",
15
+ "gates_path": ".loop/quality-gates.yaml",
16
+ "expected_prd_sha256": "REPLACE_ME_SHA256",
17
+ "expected_card_sha256": "REPLACE_ME_SHA256",
18
+ "expected_gates_sha256": "REPLACE_ME_SHA256",
19
+ "verification_run_id": "REPLACE_ME_UUID",
20
+ "evidence_directory": ".loop/evidence/REPLACE_ME_FEATURE_ID/attempt-001"
21
+ }
@@ -0,0 +1,27 @@
1
+ {
2
+ "schema_version": 1,
3
+ "detected_at": null,
4
+ "host": "codex",
5
+ "host_runtime": {
6
+ "binary": "codex",
7
+ "version": null,
8
+ "available": false
9
+ },
10
+ "verifier_runtime": {
11
+ "binary": "node",
12
+ "version": null,
13
+ "available": false
14
+ },
15
+ "lifecycle_async": {
16
+ "kind": "SubagentStop",
17
+ "minimum_version": "0.148.0",
18
+ "available": false,
19
+ "configured": false,
20
+ "reason": "not_detected"
21
+ },
22
+ "verification": {
23
+ "requested_mode": "auto",
24
+ "selected_mode": "explicit_dispatch",
25
+ "reason": "not_detected"
26
+ }
27
+ }