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,224 @@
|
|
|
1
|
+
import { homedir } from 'node:os';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
|
|
5
|
+
export const sharedSkillNames = [
|
|
6
|
+
'triad-loop-bootstrap',
|
|
7
|
+
'triad-loop-orchestrator',
|
|
8
|
+
'triad-loop-developer',
|
|
9
|
+
'triad-loop-evaluator',
|
|
10
|
+
'triad-loop-reviewer'
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
export const roleDefinitions = [
|
|
14
|
+
{ id: 'orchestrator', label: 'Orchestrator', defaultEffort: 'medium', core: true },
|
|
15
|
+
{ id: 'developer', label: 'Developer', defaultEffort: 'max', core: true },
|
|
16
|
+
{ id: 'reviewer', label: 'Reviewer', defaultEffort: 'medium', core: true },
|
|
17
|
+
{ id: 'evaluator', label: 'Evaluator+', defaultEffort: 'medium', core: false }
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
const regularRoles = roleDefinitions.filter((role) => role.core);
|
|
21
|
+
|
|
22
|
+
function hostHome(...parts) {
|
|
23
|
+
return join(homedir(), ...parts);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function hermesProfileHome() {
|
|
27
|
+
if (process.env.HERMES_HOME) return process.env.HERMES_HOME;
|
|
28
|
+
const root = hostHome('.hermes');
|
|
29
|
+
try {
|
|
30
|
+
const profile = readFileSync(join(root, 'active_profile'), 'utf8').trim();
|
|
31
|
+
if (profile) return join(root, 'profiles', profile);
|
|
32
|
+
} catch {}
|
|
33
|
+
return root;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function projectRuntimeAssets(id) {
|
|
37
|
+
return [
|
|
38
|
+
{ source: 'runtime', destination: '.triad-runtime' },
|
|
39
|
+
{ source: 'schemas', destination: '.triad-runtime/schemas' },
|
|
40
|
+
{ source: `adapters/${id}/runtime.json`, destination: '.triad-runtime/adapter.json', file: true }
|
|
41
|
+
];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function sharedSkills(destination) {
|
|
45
|
+
return { source: 'shared-skills', destination };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function rolePaths(root, names, extension = '.md') {
|
|
49
|
+
return names.map((name) => join(root, 'agents', `${name}${extension}`));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const definitions = [
|
|
53
|
+
{
|
|
54
|
+
id: 'codex',
|
|
55
|
+
label: 'Codex',
|
|
56
|
+
binary: 'codex',
|
|
57
|
+
entry: '/prompts:triad',
|
|
58
|
+
lifecycle: { kind: 'SubagentStop', agentType: 'triad_developer', requiresAsync: true },
|
|
59
|
+
modelBinding: 'global-profiles',
|
|
60
|
+
globalEntry: true,
|
|
61
|
+
projectAssets: [
|
|
62
|
+
sharedSkills('.agents/skills'),
|
|
63
|
+
...projectRuntimeAssets('codex')
|
|
64
|
+
],
|
|
65
|
+
globalAssets: [
|
|
66
|
+
{ source: 'adapters/codex/prompts/triad.md', destination: ({ codexHome }) => join(codexHome, 'prompts', 'triad.md'), file: true },
|
|
67
|
+
sharedSkills(({ codexHome }) => join(codexHome, 'skills'))
|
|
68
|
+
],
|
|
69
|
+
projectPaths(controlRoot) {
|
|
70
|
+
return [
|
|
71
|
+
...sharedSkillNames.map((name) => join(controlRoot, '.agents', 'skills', name)),
|
|
72
|
+
join(controlRoot, '.triad-runtime')
|
|
73
|
+
];
|
|
74
|
+
},
|
|
75
|
+
globalPaths({ codexHome }) {
|
|
76
|
+
return [join(codexHome, 'prompts', 'triad.md'), ...sharedSkillNames.map((name) => join(codexHome, 'skills', name))];
|
|
77
|
+
},
|
|
78
|
+
roleModelPaths({ codexHome }) {
|
|
79
|
+
return roleDefinitions.map((role) => join(codexHome, 'agents', `triad_${role.id}.toml`));
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
id: 'opencode',
|
|
84
|
+
label: 'OpenCode',
|
|
85
|
+
binary: 'opencode',
|
|
86
|
+
entry: '/triad',
|
|
87
|
+
lifecycle: null,
|
|
88
|
+
modelBinding: 'project-frontmatter',
|
|
89
|
+
modelRoles: roleDefinitions.map((role) => role.id),
|
|
90
|
+
projectAssets: [
|
|
91
|
+
{ source: 'adapters/opencode/.opencode/agents', destination: '.opencode/agents' },
|
|
92
|
+
{ source: 'adapters/opencode/.opencode/commands/triad.md', destination: '.opencode/commands/triad.md', file: true },
|
|
93
|
+
sharedSkills('.opencode/skills'),
|
|
94
|
+
...projectRuntimeAssets('opencode')
|
|
95
|
+
],
|
|
96
|
+
globalAssets: [
|
|
97
|
+
{ source: 'adapters/opencode/.opencode/agents', destination: () => hostHome('.config', 'opencode', 'agents') },
|
|
98
|
+
{ source: 'adapters/opencode/.opencode/commands/triad.md', destination: () => hostHome('.config', 'opencode', 'commands', 'triad.md'), file: true },
|
|
99
|
+
sharedSkills(() => hostHome('.config', 'opencode', 'skills'))
|
|
100
|
+
],
|
|
101
|
+
projectPaths(controlRoot) {
|
|
102
|
+
return [
|
|
103
|
+
...rolePaths(join(controlRoot, '.opencode'), roleDefinitions.map((role) => `triad-${role.id}`)),
|
|
104
|
+
join(controlRoot, '.opencode', 'commands', 'triad.md'),
|
|
105
|
+
...sharedSkillNames.map((name) => join(controlRoot, '.opencode', 'skills', name)),
|
|
106
|
+
join(controlRoot, '.triad-runtime')
|
|
107
|
+
];
|
|
108
|
+
},
|
|
109
|
+
globalPaths() {
|
|
110
|
+
const root = hostHome('.config', 'opencode');
|
|
111
|
+
return [...rolePaths(root, roleDefinitions.map((role) => `triad-${role.id}`)), join(root, 'commands', 'triad.md'), ...sharedSkillNames.map((name) => join(root, 'skills', name))];
|
|
112
|
+
},
|
|
113
|
+
roleModelPaths(controlRoot) {
|
|
114
|
+
return roleDefinitions.map((role) => join(controlRoot, '.opencode', 'agents', `triad-${role.id}.md`));
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
id: 'claude-code',
|
|
119
|
+
label: 'Claude Code',
|
|
120
|
+
binary: 'claude',
|
|
121
|
+
entry: '/triad',
|
|
122
|
+
lifecycle: { kind: 'SubagentStop', agentType: 'triad-developer', requiresAsync: false },
|
|
123
|
+
modelBinding: 'project-frontmatter',
|
|
124
|
+
modelRoles: ['developer', 'reviewer', 'evaluator'],
|
|
125
|
+
projectAssets: [
|
|
126
|
+
{ source: 'adapters/claude-code/.claude/agents', destination: '.claude/agents' },
|
|
127
|
+
{ source: 'adapters/claude-code/.claude/commands/triad.md', destination: '.claude/commands/triad.md', file: true },
|
|
128
|
+
{ source: 'integrations/claude-code/hooks.json', destination: '.claude/triad-hooks.json', file: true },
|
|
129
|
+
sharedSkills('.claude/skills'),
|
|
130
|
+
...projectRuntimeAssets('claude-code')
|
|
131
|
+
],
|
|
132
|
+
globalAssets: [
|
|
133
|
+
{ source: 'adapters/claude-code/.claude/agents', destination: () => hostHome('.claude', 'agents') },
|
|
134
|
+
{ source: 'adapters/claude-code/.claude/commands/triad.md', destination: () => hostHome('.claude', 'commands', 'triad.md'), file: true },
|
|
135
|
+
sharedSkills(() => hostHome('.claude', 'skills'))
|
|
136
|
+
],
|
|
137
|
+
projectPaths(controlRoot) {
|
|
138
|
+
const root = join(controlRoot, '.claude');
|
|
139
|
+
return [
|
|
140
|
+
join(root, 'agents', 'triad-developer.md'),
|
|
141
|
+
join(root, 'agents', 'triad-reviewer.md'),
|
|
142
|
+
join(root, 'agents', 'triad-evaluator.md'),
|
|
143
|
+
join(root, 'commands', 'triad.md'),
|
|
144
|
+
join(root, 'triad-hooks.json'),
|
|
145
|
+
...sharedSkillNames.map((name) => join(root, 'skills', name)),
|
|
146
|
+
join(controlRoot, '.triad-runtime')
|
|
147
|
+
];
|
|
148
|
+
},
|
|
149
|
+
globalPaths() {
|
|
150
|
+
const root = hostHome('.claude');
|
|
151
|
+
return [
|
|
152
|
+
join(root, 'agents', 'triad-developer.md'), join(root, 'agents', 'triad-reviewer.md'), join(root, 'agents', 'triad-evaluator.md'),
|
|
153
|
+
join(root, 'commands', 'triad.md'), ...sharedSkillNames.map((name) => join(root, 'skills', name))
|
|
154
|
+
];
|
|
155
|
+
},
|
|
156
|
+
roleModelPaths(controlRoot) {
|
|
157
|
+
return ['developer', 'reviewer', 'evaluator'].map((role) => join(controlRoot, '.claude', 'agents', `triad-${role}.md`));
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
id: 'antigravity',
|
|
162
|
+
label: 'Antigravity',
|
|
163
|
+
binary: 'agy',
|
|
164
|
+
entry: '/triad',
|
|
165
|
+
lifecycle: null,
|
|
166
|
+
modelBinding: 'team-record',
|
|
167
|
+
projectAssets: [
|
|
168
|
+
{ source: 'adapters/antigravity/.agents/agents', destination: '.agents/agents' },
|
|
169
|
+
{ source: 'adapters/antigravity/.agents/skills/triad', destination: '.agents/skills/triad' },
|
|
170
|
+
sharedSkills('.agents/skills'),
|
|
171
|
+
...projectRuntimeAssets('antigravity')
|
|
172
|
+
],
|
|
173
|
+
globalAssets: [
|
|
174
|
+
{ source: 'adapters/antigravity/.agents/agents', destination: () => hostHome('.gemini', 'config', 'agents') },
|
|
175
|
+
{ source: 'adapters/antigravity/.agents/skills/triad', destination: () => hostHome('.gemini', 'config', 'skills', 'triad') },
|
|
176
|
+
sharedSkills(() => hostHome('.gemini', 'config', 'skills'))
|
|
177
|
+
],
|
|
178
|
+
projectPaths(controlRoot) {
|
|
179
|
+
const root = join(controlRoot, '.agents');
|
|
180
|
+
return [
|
|
181
|
+
...roleDefinitions.map((role) => join(root, 'agents', `triad-${role.id}`, 'agent.md')),
|
|
182
|
+
join(root, 'skills', 'triad'), ...sharedSkillNames.map((name) => join(root, 'skills', name)), join(controlRoot, '.triad-runtime')
|
|
183
|
+
];
|
|
184
|
+
},
|
|
185
|
+
globalPaths() {
|
|
186
|
+
const root = hostHome('.gemini', 'config');
|
|
187
|
+
return [...roleDefinitions.map((role) => join(root, 'agents', `triad-${role.id}`, 'agent.md')), join(root, 'skills', 'triad'), ...sharedSkillNames.map((name) => join(root, 'skills', name))];
|
|
188
|
+
}
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
id: 'hermes',
|
|
192
|
+
label: 'Hermes Agent',
|
|
193
|
+
binary: 'hermes',
|
|
194
|
+
binaryCandidates: ['hermes', join(homedir(), '.local', 'bin', 'hermes')],
|
|
195
|
+
entry: '/triad',
|
|
196
|
+
lifecycle: null,
|
|
197
|
+
modelBinding: 'team-record',
|
|
198
|
+
globalEntry: true,
|
|
199
|
+
projectAssets: [
|
|
200
|
+
...projectRuntimeAssets('hermes')
|
|
201
|
+
],
|
|
202
|
+
globalAssets: [
|
|
203
|
+
{ source: 'adapters/hermes/skills/triad', destination: () => join(hermesProfileHome(), 'skills', 'triad') },
|
|
204
|
+
sharedSkills(() => join(hermesProfileHome(), 'skills'))
|
|
205
|
+
],
|
|
206
|
+
projectPaths(controlRoot) {
|
|
207
|
+
return [join(controlRoot, '.triad-runtime')];
|
|
208
|
+
},
|
|
209
|
+
globalPaths() {
|
|
210
|
+
const root = join(hermesProfileHome(), 'skills');
|
|
211
|
+
return [join(root, 'triad'), ...sharedSkillNames.map((name) => join(root, name))];
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
];
|
|
215
|
+
|
|
216
|
+
export const adapterRegistry = new Map(definitions.map((adapter) => [adapter.id, adapter]));
|
|
217
|
+
|
|
218
|
+
export function getAdapter(id) {
|
|
219
|
+
return adapterRegistry.get(id) ?? null;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export function listAdapters() {
|
|
223
|
+
return definitions;
|
|
224
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" role="img" aria-labelledby="title desc">
|
|
2
|
+
<title id="title">Triad+ Engineering Loop</title>
|
|
3
|
+
<desc id="desc">Three role nodes form a directional loop around verified evidence.</desc>
|
|
4
|
+
<rect width="512" height="512" rx="112" fill="#0E172A"/>
|
|
5
|
+
<path d="M256 102A154 154 0 0 1 408 283" fill="none" stroke="#7C8CFF" stroke-width="20" stroke-linecap="round"/>
|
|
6
|
+
<path d="M256 102A154 154 0 0 1 408 283" fill="none" stroke="#35C3B1" stroke-width="20" stroke-linecap="round" transform="rotate(120 256 256)"/>
|
|
7
|
+
<path d="M256 102A154 154 0 0 1 408 283" fill="none" stroke="#F5B95E" stroke-width="20" stroke-linecap="round" transform="rotate(240 256 256)"/>
|
|
8
|
+
<path d="M406 293L401 269L419 273Z" fill="#7C8CFF"/>
|
|
9
|
+
<path d="M406 293L401 269L419 273Z" fill="#35C3B1" transform="rotate(120 256 256)"/>
|
|
10
|
+
<path d="M406 293L401 269L419 273Z" fill="#F5B95E" transform="rotate(240 256 256)"/>
|
|
11
|
+
<circle cx="357" cy="171" r="37" fill="#7C8CFF"/>
|
|
12
|
+
<circle cx="357" cy="171" r="37" fill="#35C3B1" transform="rotate(120 256 256)"/>
|
|
13
|
+
<circle cx="357" cy="171" r="37" fill="#F5B95E" transform="rotate(240 256 256)"/>
|
|
14
|
+
<circle cx="256" cy="256" r="74" fill="#16233E" stroke="#C8D2FF" stroke-width="10"/>
|
|
15
|
+
<path d="M222 256l23 24 49-55" fill="none" stroke="#FFFFFF" stroke-width="20" stroke-linecap="round" stroke-linejoin="round"/>
|
|
16
|
+
</svg>
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { access, cp, mkdir, readFile, stat, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { spawnSync } from 'node:child_process';
|
|
5
|
+
import { homedir } from 'node:os';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { dirname, join, resolve } from 'node:path';
|
|
8
|
+
import process from 'node:process';
|
|
9
|
+
import { createInterface } from 'node:readline/promises';
|
|
10
|
+
import { getAdapter, listAdapters, roleDefinitions, sharedSkillNames } from '../adapters/registry.mjs';
|
|
11
|
+
|
|
12
|
+
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
13
|
+
|
|
14
|
+
function usage(exitCode = 0) {
|
|
15
|
+
const stream = exitCode === 0 ? process.stdout : process.stderr;
|
|
16
|
+
stream.write(`Triad+ installer
|
|
17
|
+
|
|
18
|
+
Usage:
|
|
19
|
+
npx triad-plus
|
|
20
|
+
npx triad-plus init --host <adapter-id> --control <path> [--global] [--team-config <path>] [--allow-product-repo]
|
|
21
|
+
npx triad-plus doctor --host <adapter-id> --control <path> [--hook-config <path>]
|
|
22
|
+
|
|
23
|
+
Adapters: ${listAdapters().map((adapter) => adapter.id).join(', ')}
|
|
24
|
+
|
|
25
|
+
The control path is a project-control workspace, not a product repository.
|
|
26
|
+
Installation refuses every asset overwrite. Doctor is read-only.
|
|
27
|
+
`);
|
|
28
|
+
process.exit(exitCode);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function parseArgs(args) {
|
|
32
|
+
const [command, ...rest] = args;
|
|
33
|
+
const options = { command, global: false, allowProductRepo: false };
|
|
34
|
+
for (let index = 0; index < rest.length; index += 1) {
|
|
35
|
+
const argument = rest[index];
|
|
36
|
+
if (argument === '--global') options.global = true;
|
|
37
|
+
else if (argument === '--allow-product-repo') options.allowProductRepo = true;
|
|
38
|
+
else if (['--host', '--control', '--team-config', '--hook-config'].includes(argument)) {
|
|
39
|
+
const value = rest[index + 1];
|
|
40
|
+
if (!value || value.startsWith('--')) throw new Error(`${argument} requires a value.`);
|
|
41
|
+
options[argument.slice(2).replace(/-([a-z])/g, (_, letter) => letter.toUpperCase())] = value;
|
|
42
|
+
index += 1;
|
|
43
|
+
} else if (argument === '--help' || argument === '-h') usage(0);
|
|
44
|
+
else throw new Error(`Unknown argument: ${argument}`);
|
|
45
|
+
}
|
|
46
|
+
return options;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function exists(target) {
|
|
50
|
+
try { await access(target); return true; } catch { return false; }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function requireDirectory(target) {
|
|
54
|
+
if (!(await exists(target))) return mkdir(target, { recursive: true });
|
|
55
|
+
if (!(await stat(target)).isDirectory()) throw new Error(`Control path is not a directory: ${target}`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function codexHome() {
|
|
59
|
+
return process.env.CODEX_HOME || join(homedir(), '.codex');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function context(controlRoot) {
|
|
63
|
+
return { controlRoot, codexHome: codexHome() };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function resolveDestination(destination, root, installContext) {
|
|
67
|
+
const value = typeof destination === 'function' ? destination(installContext) : join(root, destination);
|
|
68
|
+
return resolve(value);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function sourcePath(source) {
|
|
72
|
+
return join(packageRoot, source);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function planForAssets(assets, root, installContext) {
|
|
76
|
+
const paths = [];
|
|
77
|
+
for (const asset of assets) {
|
|
78
|
+
const destination = resolveDestination(asset.destination, root, installContext);
|
|
79
|
+
if (asset.source === 'shared-skills') {
|
|
80
|
+
paths.push(...sharedSkillNames.map((name) => join(destination, name)));
|
|
81
|
+
} else {
|
|
82
|
+
paths.push(destination);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return paths;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function copyAsset(asset, root, installContext) {
|
|
89
|
+
const destination = resolveDestination(asset.destination, root, installContext);
|
|
90
|
+
if (asset.source === 'shared-skills') {
|
|
91
|
+
await mkdir(destination, { recursive: true });
|
|
92
|
+
for (const name of sharedSkillNames) await cp(join(packageRoot, 'skills', name), join(destination, name), { recursive: true });
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
await mkdir(dirname(destination), { recursive: true });
|
|
96
|
+
await cp(sourcePath(asset.source), destination, { recursive: !asset.file });
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function installAssets(assets, root, installContext) {
|
|
100
|
+
for (const asset of assets) await copyAsset(asset, root, installContext);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function teamConfigPath(controlRoot) {
|
|
104
|
+
return join(controlRoot, '.triad-plus', 'team.json');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function loadTeamConfig(options) {
|
|
108
|
+
if (options.team) return options.team;
|
|
109
|
+
if (!options.teamConfig) return null;
|
|
110
|
+
let team;
|
|
111
|
+
try { team = JSON.parse(await readFile(resolve(options.teamConfig), 'utf8')); }
|
|
112
|
+
catch (error) { throw new Error(`Cannot read --team-config: ${error.message}`); }
|
|
113
|
+
if (team?.schema_version !== 1 || !team.interaction || !team.roles) throw new Error('--team-config must contain schema_version 1, interaction, and roles.');
|
|
114
|
+
for (const role of roleDefinitions) {
|
|
115
|
+
const value = team.roles[role.id];
|
|
116
|
+
if (!value || typeof value.displayName !== 'string' || ![null, undefined].includes(value.model) && typeof value.model !== 'string') {
|
|
117
|
+
throw new Error(`--team-config has an invalid ${role.id} role definition.`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return team;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function writeTeamConfig(controlRoot, team) {
|
|
124
|
+
const target = teamConfigPath(controlRoot);
|
|
125
|
+
await mkdir(dirname(target), { recursive: true });
|
|
126
|
+
await writeFile(target, `${JSON.stringify(team, null, 2)}\n`, 'utf8');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function applyMarkdownModel(target, model) {
|
|
130
|
+
if (!model) return;
|
|
131
|
+
const source = await readFile(target, 'utf8');
|
|
132
|
+
if (!source.startsWith('---\n')) throw new Error(`Agent definition has no YAML frontmatter: ${target}`);
|
|
133
|
+
const closing = source.indexOf('\n---\n', 4);
|
|
134
|
+
if (closing === -1) throw new Error(`Agent definition has invalid YAML frontmatter: ${target}`);
|
|
135
|
+
const frontmatter = source.slice(4, closing).replace(/^model:\s*.*\n?/m, '');
|
|
136
|
+
await writeFile(target, `---\n${frontmatter}model: ${JSON.stringify(model)}${source.slice(closing)}`, 'utf8');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function writeRoleProfiles(paths, team) {
|
|
140
|
+
await mkdir(dirname(paths[0]), { recursive: true });
|
|
141
|
+
for (const [index, role] of roleDefinitions.entries()) {
|
|
142
|
+
const configuration = team.roles[role.id];
|
|
143
|
+
const instructions = [
|
|
144
|
+
`Act as ${configuration.displayName}, the ${role.label} role in Triad+.`,
|
|
145
|
+
`Persona: ${configuration.persona || 'professional and role-focused'}.`,
|
|
146
|
+
'Read .triad-plus/team.json in the active project-control workspace before working.',
|
|
147
|
+
'Technical role IDs define authority; display names never change it.'
|
|
148
|
+
].join('\n');
|
|
149
|
+
const profile = [
|
|
150
|
+
`name = ${JSON.stringify(`triad_${role.id}`)}`,
|
|
151
|
+
`description = ${JSON.stringify(`Triad+ ${role.label}: ${configuration.displayName}.`)}`,
|
|
152
|
+
...(configuration.model ? [`model = ${JSON.stringify(configuration.model)}`] : []),
|
|
153
|
+
...(configuration.reasoning_effort ? [`model_reasoning_effort = ${JSON.stringify(configuration.reasoning_effort)}`] : []),
|
|
154
|
+
`developer_instructions = ${JSON.stringify(instructions)}`,
|
|
155
|
+
''
|
|
156
|
+
].join('\n');
|
|
157
|
+
await writeFile(paths[index], profile, 'utf8');
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async function applyTeamBinding(adapter, controlRoot, team, installContext) {
|
|
162
|
+
if (!team || adapter.modelBinding === 'team-record') return;
|
|
163
|
+
if (adapter.modelBinding === 'project-frontmatter') {
|
|
164
|
+
const paths = adapter.roleModelPaths(controlRoot, installContext);
|
|
165
|
+
const roles = (adapter.modelRoles ?? roleDefinitions.map((role) => role.id))
|
|
166
|
+
.map((roleId) => roleDefinitions.find((role) => role.id === roleId));
|
|
167
|
+
for (const [index, role] of roles.entries()) await applyMarkdownModel(paths[index], team.roles[role.id].model);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
if (adapter.modelBinding === 'global-profiles') {
|
|
171
|
+
await writeRoleProfiles(adapter.roleModelPaths(installContext), team);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
throw new Error(`Unknown model binding: ${adapter.modelBinding}`);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function productRepositoryAt(controlRoot) {
|
|
178
|
+
const git = spawnSync('git', ['-C', controlRoot, 'rev-parse', '--show-toplevel'], { encoding: 'utf8' });
|
|
179
|
+
if (git.status !== 0 || resolve(git.stdout.trim()) !== controlRoot) return false;
|
|
180
|
+
return ['package.json', 'pyproject.toml', 'Cargo.toml', 'pom.xml', 'go.mod', 'Gemfile']
|
|
181
|
+
.some((marker) => spawnSync('test', ['-e', join(controlRoot, marker)]).status === 0);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async function collisions(paths) {
|
|
185
|
+
const result = [];
|
|
186
|
+
for (const target of paths) if (await exists(target)) result.push(target);
|
|
187
|
+
return result;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function commandVersion(binary) {
|
|
191
|
+
const candidates = Array.isArray(binary) ? binary : [binary];
|
|
192
|
+
for (const candidate of candidates) {
|
|
193
|
+
const result = spawnSync(candidate, ['--version'], { encoding: 'utf8' });
|
|
194
|
+
if (result.status === 0) return String(result.stdout).trim().split('\n')[0];
|
|
195
|
+
}
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async function collectTeamConfiguration(prompt, adapter) {
|
|
200
|
+
const language = (await prompt.question('Conversation language [English]: ')).trim() || 'English';
|
|
201
|
+
const ownerName = (await prompt.question('How should Triad+ address the project owner [Owner]: ')).trim() || 'Owner';
|
|
202
|
+
const communicationStyle = (await prompt.question('Preferred communication style [professional and concise]: ')).trim() || 'professional and concise';
|
|
203
|
+
const evaluatorEnabled = ['y', 'yes'].includes((await prompt.question('Configure optional post-run Evaluator+? [y/N]: ')).trim().toLowerCase());
|
|
204
|
+
const roles = {};
|
|
205
|
+
for (const role of roleDefinitions) {
|
|
206
|
+
const enabled = role.core || evaluatorEnabled;
|
|
207
|
+
const displayName = (await prompt.question(`${role.label} display name [${role.label}]: `)).trim() || role.label;
|
|
208
|
+
const persona = (await prompt.question(`${role.label} persona [professional and role-focused]: `)).trim() || 'professional and role-focused';
|
|
209
|
+
const model = (await prompt.question(`${role.label} model ID [host default]: `)).trim();
|
|
210
|
+
const reasoningEffort = model && adapter.modelBinding === 'global-profiles'
|
|
211
|
+
? (await prompt.question(`${role.label} reasoning effort [${role.defaultEffort}]: `)).trim() || role.defaultEffort
|
|
212
|
+
: null;
|
|
213
|
+
roles[role.id] = { displayName, persona, model: model || null, reasoning_effort: reasoningEffort, enabled };
|
|
214
|
+
}
|
|
215
|
+
return { schema_version: 1, interaction: { language, owner_name: ownerName, communication_style: communicationStyle }, roles };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async function init(options) {
|
|
219
|
+
const adapter = getAdapter(options.host);
|
|
220
|
+
if (!adapter) throw new Error(`Choose --host ${listAdapters().map((item) => item.id).join(', ')}.`);
|
|
221
|
+
if (!options.control) throw new Error('Provide --control <project-control-path>.');
|
|
222
|
+
const team = await loadTeamConfig(options);
|
|
223
|
+
const controlRoot = resolve(options.control);
|
|
224
|
+
await requireDirectory(controlRoot);
|
|
225
|
+
if (!options.allowProductRepo && productRepositoryAt(controlRoot)) {
|
|
226
|
+
throw new Error('Control path appears to be a product repository. Use a separate project-control workspace, or pass --allow-product-repo after reviewing the risk.');
|
|
227
|
+
}
|
|
228
|
+
const installContext = context(controlRoot);
|
|
229
|
+
const planned = [
|
|
230
|
+
...adapter.projectPaths(controlRoot, installContext),
|
|
231
|
+
...(team ? [teamConfigPath(controlRoot)] : []),
|
|
232
|
+
...(options.global ? adapter.globalPaths(installContext) : []),
|
|
233
|
+
...(team && options.global && adapter.modelBinding === 'global-profiles' ? adapter.roleModelPaths(installContext) : [])
|
|
234
|
+
];
|
|
235
|
+
const existing = await collisions(planned);
|
|
236
|
+
if (existing.length > 0) throw new Error(`Installation aborted; existing paths would be overwritten:\n${existing.map((target) => ` ${target}`).join('\n')}`);
|
|
237
|
+
await installAssets(adapter.projectAssets, controlRoot, installContext);
|
|
238
|
+
if (team) await writeTeamConfig(controlRoot, team);
|
|
239
|
+
if (team) await applyTeamBinding(adapter, controlRoot, team, installContext);
|
|
240
|
+
if (options.global) await installAssets(adapter.globalAssets, controlRoot, installContext);
|
|
241
|
+
process.stdout.write(`Triad+ installed for ${adapter.label} in ${controlRoot}\n`);
|
|
242
|
+
if (adapter.globalEntry) {
|
|
243
|
+
process.stdout.write(`Install user-level assets with --global to expose ${adapter.entry}.\n`);
|
|
244
|
+
}
|
|
245
|
+
process.stdout.write(`Open the control workspace and use ${adapter.entry} <PRD path>. Evaluator+ is optional and post-run only.\n`);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
async function doctor(options) {
|
|
249
|
+
if (!options.control) throw new Error('Provide --control <project-control-path>.');
|
|
250
|
+
const controlRoot = resolve(options.control);
|
|
251
|
+
const requested = options.host ? [getAdapter(options.host)] : listAdapters();
|
|
252
|
+
if (requested.some((adapter) => !adapter)) throw new Error(`Choose --host ${listAdapters().map((item) => item.id).join(', ')}.`);
|
|
253
|
+
let team = null;
|
|
254
|
+
if (await exists(teamConfigPath(controlRoot))) {
|
|
255
|
+
try { team = JSON.parse(await readFile(teamConfigPath(controlRoot), 'utf8')); }
|
|
256
|
+
catch { team = 'invalid'; }
|
|
257
|
+
}
|
|
258
|
+
for (const adapter of requested) {
|
|
259
|
+
const installContext = context(controlRoot);
|
|
260
|
+
const absent = [];
|
|
261
|
+
for (const target of adapter.projectPaths(controlRoot, installContext)) if (!(await exists(target))) absent.push(target);
|
|
262
|
+
const binary = commandVersion(adapter.binaryCandidates ?? adapter.binary);
|
|
263
|
+
const node = commandVersion('node');
|
|
264
|
+
const manifestPath = join(controlRoot, '.triad-runtime', 'adapter.json');
|
|
265
|
+
let manifest = false;
|
|
266
|
+
try { manifest = (JSON.parse(await readFile(manifestPath, 'utf8'))?.id === adapter.id); } catch {}
|
|
267
|
+
process.stdout.write(`${adapter.label.padEnd(14)} ${absent.length ? 'not installed' : 'OK'}\n`);
|
|
268
|
+
process.stdout.write(` Host runtime ${binary ? `OK (${binary})` : 'not installed or version unavailable'}\n`);
|
|
269
|
+
process.stdout.write(` Verifier ${node && await exists(join(controlRoot, '.triad-runtime', 'triad-verify.mjs')) ? 'OK' : 'incomplete'}\n`);
|
|
270
|
+
process.stdout.write(` Adapter ${manifest ? 'OK' : 'missing or different adapter'}\n`);
|
|
271
|
+
process.stdout.write(` Team config ${team === 'invalid' ? 'invalid' : team ? 'OK' : 'not configured'}\n`);
|
|
272
|
+
process.stdout.write(` Evaluator+ ${team?.roles?.evaluator?.enabled === true ? 'configured' : 'not configured'}\n`);
|
|
273
|
+
if (options.hookConfig && adapter.lifecycle) {
|
|
274
|
+
process.stdout.write(` Hook config declared; run runtime capability detection for detailed status\n`);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
async function interactiveInit() {
|
|
280
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) throw new Error('The interactive wizard needs a terminal. Use init in a non-interactive shell.');
|
|
281
|
+
const prompt = createInterface({ input: process.stdin, output: process.stdout });
|
|
282
|
+
try {
|
|
283
|
+
process.stdout.write('\nTriad+ setup\n');
|
|
284
|
+
listAdapters().forEach((adapter, index) => process.stdout.write(`${index + 1}) ${adapter.label}\n`));
|
|
285
|
+
let adapter;
|
|
286
|
+
while (!adapter) {
|
|
287
|
+
const answer = (await prompt.question(`Choose the host [1-${listAdapters().length}]: `)).trim().toLowerCase();
|
|
288
|
+
adapter = listAdapters()[Number(answer) - 1] ?? getAdapter(answer);
|
|
289
|
+
if (!adapter) process.stdout.write('Choose a listed host.\n');
|
|
290
|
+
}
|
|
291
|
+
const control = (await prompt.question(`Project-control workspace [${join(process.cwd(), 'triad-control')}]: `)).trim() || join(process.cwd(), 'triad-control');
|
|
292
|
+
const global = ['y', 'yes'].includes((await prompt.question(`Also install user-level ${adapter.entry} assets? [y/N]: `)).trim().toLowerCase());
|
|
293
|
+
const team = await collectTeamConfiguration(prompt, adapter);
|
|
294
|
+
const confirm = (await prompt.question('Type install to continue: ')).trim().toLowerCase();
|
|
295
|
+
if (confirm !== 'install') return process.stdout.write('Cancelled. No files were changed.\n');
|
|
296
|
+
await init({ host: adapter.id, control, global, team });
|
|
297
|
+
await doctor({ host: adapter.id, control });
|
|
298
|
+
} finally { prompt.close(); }
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
try {
|
|
302
|
+
const options = parseArgs(process.argv.slice(2));
|
|
303
|
+
if (options.command === 'init') await init(options);
|
|
304
|
+
else if (options.command === 'doctor') await doctor(options);
|
|
305
|
+
else if (!options.command) await interactiveInit();
|
|
306
|
+
else if (options.command === '--help' || options.command === '-h') usage(0);
|
|
307
|
+
else throw new Error(`Unknown command: ${options.command}`);
|
|
308
|
+
} catch (error) {
|
|
309
|
+
process.stderr.write(`${error.message}\n`);
|
|
310
|
+
usage(2);
|
|
311
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Antigravity adapter
|
|
2
|
+
|
|
3
|
+
Install with `npx triad-plus init --host antigravity --control <path>`. Open the
|
|
4
|
+
control workspace and run `/triad <absolute-prd-path>`. Antigravity receives
|
|
5
|
+
project agent assets, shared skills, and the common verifier runtime.
|
|
6
|
+
|
|
7
|
+
It uses explicit verification dispatch. The team file records requested role
|
|
8
|
+
models and personas; Triad+ does not claim a host-native per-agent model setting
|
|
9
|
+
where Antigravity does not expose one.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Architecture
|
|
2
|
+
|
|
3
|
+
Triad+ has three small layers:
|
|
4
|
+
|
|
5
|
+
```text
|
|
6
|
+
Triad Core roles Orchestrator / Developer / Reviewer
|
|
7
|
+
│
|
|
8
|
+
Runtime adapter host detection, assets, entry point, capabilities, model binding
|
|
9
|
+
│
|
|
10
|
+
Verification layer declared gates and environment-derived evidence
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
The host agent is the Orchestrator. It owns operational context, delegation, and
|
|
14
|
+
the next decision. The runtime does not schedule work or implement a general
|
|
15
|
+
state machine.
|
|
16
|
+
|
|
17
|
+
Adapters are registered descriptors in `adapters/registry.mjs`. They describe
|
|
18
|
+
only real host differences: binary discovery, installation destinations, native
|
|
19
|
+
entry point, optional hook lifecycle, and supported model binding. Runtime-
|
|
20
|
+
specific behavior belongs in the adapter. Adding a runtime must not add
|
|
21
|
+
runtime-specific branches to Core installer or verification logic.
|
|
22
|
+
|
|
23
|
+
Evaluator+ is outside the Core: it runs after a Reviewer-approved result, receives
|
|
24
|
+
a deliberately limited fresh packet, and cannot reopen the completed run.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Claude Code adapter
|
|
2
|
+
|
|
3
|
+
Install with `npx triad-plus init --host claude-code --control <path>`. Open the
|
|
4
|
+
control workspace and run `/triad <absolute-prd-path>`. Claude Code uses its
|
|
5
|
+
delegated Developer, Reviewer, and optional Evaluator+ definitions; the active
|
|
6
|
+
main session acts as Orchestrator.
|
|
7
|
+
|
|
8
|
+
A complete `SubagentStop` hook can dispatch verification. Explicit dispatch is
|
|
9
|
+
always available, and the hook never owns a Triad transition.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Codex adapter
|
|
2
|
+
|
|
3
|
+
Install with `npx triad-plus init --host codex --control <path> --global`.
|
|
4
|
+
Open the control workspace and run `/prompts:triad <absolute-prd-path>`.
|
|
5
|
+
|
|
6
|
+
Codex uses role profiles for the Orchestrator, Developer, Reviewer, and optional
|
|
7
|
+
Evaluator+. The command detects whether a complete, version-compatible async
|
|
8
|
+
`SubagentStop` verification hook is installed. If it is not, the Orchestrator
|
|
9
|
+
explicitly invokes the verifier. Hook output is evidence only; it never changes
|
|
10
|
+
Triad state by itself.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Compatibility matrix
|
|
2
|
+
|
|
3
|
+
| Runtime | Install | Orchestrator | Developer | Reviewer | Evaluator+ | Verification dispatch | Hook support |
|
|
4
|
+
| --- | --- | --- | --- | --- | --- | --- | --- |
|
|
5
|
+
| Codex | Yes | Yes | Yes | Yes | Yes | Explicit fallback; async hook when validated | Optional async `SubagentStop` |
|
|
6
|
+
| Claude Code | Yes | Yes | Yes | Yes | Yes | Explicit fallback; hook when validated | Optional `SubagentStop` |
|
|
7
|
+
| OpenCode | Yes | Yes | Yes | Yes | Yes | Explicit dispatch | No adapter hook |
|
|
8
|
+
| Antigravity | Yes | Yes | Yes | Yes | Yes | Explicit dispatch | No adapter hook |
|
|
9
|
+
| Hermes Agent | Yes | Yes | Yes | Yes | Yes | Explicit dispatch | No adapter hook |
|
|
10
|
+
|
|
11
|
+
All adapters consume the same role IDs and project-control records. Runtime
|
|
12
|
+
model/effort application is limited to the facilities each host exposes; the
|
|
13
|
+
full requested contract is always retained in `.triad-plus/team.json`.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Configuration
|
|
2
|
+
|
|
3
|
+
`.triad-plus/team.json` separates stable role IDs from user-facing names and
|
|
4
|
+
personas. A schema-version-1 configuration has four role records:
|
|
5
|
+
|
|
6
|
+
```json
|
|
7
|
+
{
|
|
8
|
+
"schema_version": 1,
|
|
9
|
+
"interaction": { "language": "English", "owner_name": "Owner", "communication_style": "concise" },
|
|
10
|
+
"roles": {
|
|
11
|
+
"orchestrator": { "displayName": "Coordinator", "persona": "calm and precise", "model": null, "reasoning_effort": null },
|
|
12
|
+
"developer": { "displayName": "Builder", "persona": "methodical", "model": null, "reasoning_effort": null },
|
|
13
|
+
"reviewer": { "displayName": "Critic", "persona": "independent", "model": null, "reasoning_effort": null },
|
|
14
|
+
"evaluator": { "displayName": "Evaluator", "persona": "fresh and evidence-led", "model": null, "reasoning_effort": null, "enabled": false }
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
`orchestrator`, `developer`, and `reviewer` are the Core roles. `evaluator` is
|
|
20
|
+
optional and enabled only when `roles.evaluator.enabled` is `true`. Omitting that
|
|
21
|
+
field is backward-compatible and means Evaluator+ is not configured.
|
|
22
|
+
|
|
23
|
+
The runtime adapter is selected per installed control workspace (`--host`). The
|
|
24
|
+
team file records role-level models and effort, but an adapter writes those into
|
|
25
|
+
host-native profiles only where the host supports that facility. A blank model
|
|
26
|
+
means the host default. Never put tokens, API keys, or private deployment data in
|
|
27
|
+
this file.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Evaluator+
|
|
2
|
+
|
|
3
|
+
Evaluator+ is optional and outside the Triad production loop. Invoke it only
|
|
4
|
+
after the Reviewer has approved a feature or delivery.
|
|
5
|
+
|
|
6
|
+
It receives the goal, acceptance target, final artifact, and verifier evidence.
|
|
7
|
+
It should not receive developer reasoning, prior reviewer conversation, or
|
|
8
|
+
attempt history unless the owner explicitly requires it. It reports `PASS`,
|
|
9
|
+
`FAIL`, or `INDETERMINATE`, with concise evidence references, under
|
|
10
|
+
`artifacts/evaluator-plus/`.
|
|
11
|
+
|
|
12
|
+
An Evaluator+ `FAIL` does not reopen Triad, change the approved state, or start
|
|
13
|
+
repair. An owner or a later Orchestrator may use it as input to a new run.
|