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,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,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
|
+
}
|