triad-plus 1.8.0 → 1.10.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 +36 -0
- package/README.md +21 -12
- package/adapters/antigravity/.agents/agents/triad-developer/agent.md +7 -3
- package/adapters/antigravity/.agents/agents/triad-orchestrator/agent.md +5 -0
- package/adapters/antigravity/.agents/agents/triad-reviewer/agent.md +7 -2
- package/adapters/antigravity/.agents/skills/triad/SKILL.md +22 -0
- package/adapters/antigravity/README.md +1 -1
- package/adapters/antigravity/install.sh +1 -1
- package/adapters/claude-code/.claude/agents/triad-developer.md +9 -4
- package/adapters/claude-code/.claude/agents/triad-reviewer.md +10 -5
- package/adapters/claude-code/.claude/commands/triad.md +22 -0
- package/adapters/claude-code/README.md +12 -4
- package/adapters/claude-code/install.sh +1 -1
- package/adapters/codex/README.md +1 -1
- package/adapters/codex/install.sh +3 -3
- package/adapters/codex/prompts/triad.md +28 -0
- package/adapters/copilot/.github/agents/triad-developer.agent.md +12 -6
- package/adapters/copilot/.github/agents/triad-evaluator.agent.md +2 -1
- package/adapters/copilot/.github/agents/triad-orchestrator.agent.md +15 -1
- package/adapters/copilot/.github/agents/triad-reviewer.agent.md +11 -5
- package/adapters/copilot/.github/skills/triad/SKILL.md +23 -0
- package/adapters/hermes/install.sh +1 -1
- package/adapters/hermes/skills/triad/SKILL.md +18 -0
- package/adapters/opencode/.opencode/agents/triad-developer.md +23 -4
- package/adapters/opencode/.opencode/agents/triad-orchestrator.md +16 -0
- package/adapters/opencode/.opencode/agents/triad-reviewer.md +25 -3
- package/adapters/opencode/.opencode/commands/triad.md +21 -0
- package/adapters/opencode/README.md +29 -5
- package/adapters/opencode/install.sh +1 -1
- package/adapters/registry.mjs +23 -1
- package/bin/triad-plus.js +210 -38
- package/docs/architecture.md +8 -0
- package/docs/assignment-packets.md +35 -0
- package/docs/bmad-integration.md +187 -82
- package/docs/configuration.md +29 -0
- package/docs/npx-installation.md +20 -0
- package/docs/opencode-replication.md +8 -0
- package/docs/operating-guide.it.md +8 -0
- package/docs/operating-guide.md +7 -0
- package/docs/runtimes.md +1 -1
- package/docs/verification.md +20 -0
- package/integrations/bmad/README.md +17 -9
- package/integrations/bmad/epics-parser.mjs +697 -0
- package/integrations/bmad/story-importer.mjs +143 -5
- package/package.json +2 -2
- package/runtime/lib/assignment-packet.mjs +553 -0
- package/runtime/lib/model-config.mjs +165 -0
- package/runtime/lib/repository-context.mjs +192 -0
- package/runtime/lib/terminal.mjs +86 -0
- package/runtime/triad-assignment-packet.mjs +62 -0
- package/runtime/triad-bmad-intake.mjs +150 -0
- package/runtime/triad-runtime-context.mjs +88 -0
- package/runtime/triad-verify.mjs +41 -21
- package/schemas/verification-evidence.schema.json +8 -0
- package/skills/triad-loop-bootstrap/SKILL.md +39 -4
- package/skills/triad-loop-bootstrap/assets/loop-template/runtime/assignments/assignment.template.json +14 -1
- package/skills/triad-loop-developer/SKILL.md +33 -8
- package/skills/triad-loop-orchestrator/SKILL.md +96 -11
- package/skills/triad-loop-reviewer/SKILL.md +31 -5
- package/skills/triad-model-configuration/SKILL.md +98 -0
- package/skills/triad-model-configuration/agents/openai.yaml +4 -0
|
@@ -17,6 +17,7 @@ permission:
|
|
|
17
17
|
triad-reviewer: allow
|
|
18
18
|
skill:
|
|
19
19
|
"triad-loop-*": allow
|
|
20
|
+
"triad-model-configuration": allow
|
|
20
21
|
---
|
|
21
22
|
|
|
22
23
|
You are the Triad+ Engineering Loop orchestrator. Govern the workflow and the
|
|
@@ -64,6 +65,21 @@ developer-reviewer disagreements and record the rationale; escalate only a
|
|
|
64
65
|
decision that changes product intent, criteria, metrics, gates, architecture,
|
|
65
66
|
security, budget, or accepted risk.
|
|
66
67
|
|
|
68
|
+
Before each delegation, create the immutable Assignment Packet and bind it to
|
|
69
|
+
the assignment by running:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
node .triad-runtime/triad-assignment-packet.mjs \
|
|
73
|
+
--project /absolute/path/to/control-workspace \
|
|
74
|
+
--assignment .loop/runtime/assignments/<assignment-file>.json
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Use its returned `dispatch.cwd` as the OpenCode task workdir; it MUST equal the
|
|
78
|
+
assigned product worktree. Give Developer and Reviewer the same packet plus
|
|
79
|
+
explicit control/repository/branch/card/skill paths. The packet is primary;
|
|
80
|
+
full PRD/ADR reads are only fallback for a missing or contradictory detail.
|
|
81
|
+
Do not use the packet to bypass real skill reads or verifier hash checks.
|
|
82
|
+
|
|
67
83
|
Perform hands-on development or review only when delegation is genuinely
|
|
68
84
|
unavailable or cannot proceed. Record the trigger, scope, alternatives, risk,
|
|
69
85
|
validation, independent-review evidence, and the restoration of normal roles.
|
|
@@ -18,14 +18,33 @@ permission:
|
|
|
18
18
|
|
|
19
19
|
You are the independent Triad+ Engineering Loop reviewer. Load
|
|
20
20
|
`triad-loop-reviewer` at the start of every assignment and follow it exactly.
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
The host SHOULD launch this activation with its workdir/cwd equal to the
|
|
22
|
+
candidate product worktree when direct inspection is needed. Read the same
|
|
23
|
+
immutable Assignment Packet used by the Developer, then the card, diff,
|
|
24
|
+
developer evidence, repository instructions, prior attempts, gates, and
|
|
25
|
+
metrics. Consult only a necessary PRD/ADR section when the packet is
|
|
26
|
+
insufficient or contradictory; do not reconstruct the full requirement by
|
|
27
|
+
default. Independently rerun enough required gates to verify claims.
|
|
24
28
|
|
|
25
29
|
At activation, read `.triad-plus/team.json`. Your first report to the
|
|
26
30
|
Orchestrator identifies the configured `roles.reviewer.displayName` as the
|
|
27
31
|
Triad+ Reviewer and names the feature and attempt under review.
|
|
28
32
|
|
|
33
|
+
Before inspecting the candidate, run this exact runtime-context proof from the
|
|
34
|
+
actual Reviewer cwd:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
node /absolute/path/to/control-workspace/.triad-runtime/triad-runtime-context.mjs \
|
|
38
|
+
--project /absolute/path/to/control-workspace \
|
|
39
|
+
--assignment /absolute/path/to/control-workspace/.loop/runtime/assignments/<assignment-file>.json
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Record packet cwd, process cwd, shell `pwd`, Git top-level, and each resolved
|
|
43
|
+
repository-skill path/hash. On non-zero status, `NotFound`, hash mismatch,
|
|
44
|
+
mixed repository roots, or cwd inconsistency, return `blocked` / invalid
|
|
45
|
+
runtime context. Do not use a same-named skill from the control workspace or
|
|
46
|
+
global skills as a fallback.
|
|
47
|
+
|
|
29
48
|
Return one evidence-based recommendation: `approved`, `rework`, or `blocked`.
|
|
30
49
|
List severity-ranked findings before gate and metric evidence, residual risks,
|
|
31
50
|
and the recommendation. A blocked recommendation must identify the exact owner
|
|
@@ -34,3 +53,6 @@ decision or external condition required.
|
|
|
34
53
|
Do not modify code, implement routine fixes, commit, push, publish packages,
|
|
35
54
|
create releases, or change workflow state. The orchestrator makes transitions;
|
|
36
55
|
the developer implements rework.
|
|
56
|
+
|
|
57
|
+
Use OpenCode read/grep/glob/list tools for simple reads; reserve Bash for
|
|
58
|
+
builds, tests, git, scripts, and system commands.
|
|
@@ -7,6 +7,11 @@ Operate a Triad+ Engineering Loop for the following owner request:
|
|
|
7
7
|
|
|
8
8
|
$ARGUMENTS
|
|
9
9
|
|
|
10
|
+
When the input is a native BMAD `epics.md` (or its `_bmad-output` directory),
|
|
11
|
+
use the deterministic native intake from `triad-loop-orchestrator`. Do not ask
|
|
12
|
+
the owner to create one Story file per BMAD Story or to run
|
|
13
|
+
`import-bmad-story` repeatedly; keep `epics.md` read-only.
|
|
14
|
+
|
|
10
15
|
If `.triad-plus/team.json` exists, load it before replying. Use its interaction
|
|
11
16
|
language, owner address, display names, personas, and model contract in communication;
|
|
12
17
|
technical role identifiers and authority remain unchanged.
|
|
@@ -43,3 +48,19 @@ When true, automatically dispatch `triad-evaluator` in a fresh context with only
|
|
|
43
48
|
the approved evaluation packet. When false or omitted, finish without evaluation.
|
|
44
49
|
An Evaluator+ verdict never reopens Triad, assigns Developer work, or starts
|
|
45
50
|
repair; `--evaluator` and `--no-evaluator` are per-run overrides when supplied.
|
|
51
|
+
|
|
52
|
+
Before delegating each card, create the immutable Assignment Packet with:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
node .triad-runtime/triad-assignment-packet.mjs \
|
|
56
|
+
--project /absolute/path/to/control-workspace \
|
|
57
|
+
--assignment .loop/runtime/assignments/<assignment-file>.json
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Dispatch the Developer with the returned `dispatch.cwd` as its OpenCode
|
|
61
|
+
workdir, which MUST be the assigned product worktree. Pass the packet and card
|
|
62
|
+
as the primary contract, followed by explicit control paths and mandatory skill
|
|
63
|
+
paths. Dispatch the Reviewer with the same packet and candidate worktree when
|
|
64
|
+
it needs direct inspection; do not reconstruct the full PRD/ADR by default.
|
|
65
|
+
Use native read/grep/glob/list tools for simple reads and Bash only for
|
|
66
|
+
build/test/git/script/system commands.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Triad+ Engineering Loop for OpenCode
|
|
2
2
|
|
|
3
|
-
This adapter makes the repository's existing
|
|
3
|
+
This adapter makes the repository's existing six standard Agent Skills
|
|
4
4
|
operational in OpenCode. It installs four specialized agents and the `/triad`
|
|
5
5
|
command; it does not duplicate the method or alter `opencode.json`.
|
|
6
6
|
|
|
@@ -26,6 +26,7 @@ skills/triad-loop-orchestrator/
|
|
|
26
26
|
skills/triad-loop-developer/
|
|
27
27
|
skills/triad-loop-evaluator/
|
|
28
28
|
skills/triad-loop-reviewer/
|
|
29
|
+
skills/triad-model-configuration/
|
|
29
30
|
.triad-runtime/
|
|
30
31
|
triad-verify.mjs
|
|
31
32
|
triad-runtime-capabilities.mjs
|
|
@@ -53,10 +54,26 @@ remain in a declared project workspace.
|
|
|
53
54
|
|
|
54
55
|
## Configure models deliberately
|
|
55
56
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
57
|
+
`.triad-plus/team.json` is the source of truth for role-level model
|
|
58
|
+
configuration. When a team file supplies values, Triad+ materializes the
|
|
59
|
+
OpenCode-native `model` and `variant` frontmatter fields in each project agent
|
|
60
|
+
definition during `init` and `upgrade --apply`:
|
|
61
|
+
|
|
62
|
+
```text
|
|
63
|
+
team.json model → OpenCode model
|
|
64
|
+
team.json reasoning_effort → OpenCode variant
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Do not treat generated `.opencode/agents/*.md` files as an independent
|
|
68
|
+
configuration source; a later managed upgrade regenerates them from
|
|
69
|
+
`.triad-plus/team.json`. A `null` or blank value leaves the corresponding field
|
|
70
|
+
absent so OpenCode uses its host/session default.
|
|
71
|
+
|
|
72
|
+
The adapter only writes fields supported by this OpenCode materialization. Host
|
|
73
|
+
and provider capabilities may still limit which model or reasoning levels are
|
|
74
|
+
available. Inspect identifiers and variants with `opencode models` and keep
|
|
75
|
+
role-specific routing explicit in the team file. OpenCode's role `variant` is
|
|
76
|
+
not the same thing as the default model/session selected by the TUI:
|
|
60
77
|
|
|
61
78
|
| Agent | Recommended profile |
|
|
62
79
|
| --- | --- |
|
|
@@ -69,6 +86,13 @@ Use `opencode models` to inspect the provider/model identifiers available in
|
|
|
69
86
|
the local installation. Keep the developer, evaluator, and reviewer separately
|
|
70
87
|
configured when independent model routing is required.
|
|
71
88
|
|
|
89
|
+
The adapter targets the installed OpenCode agent contract: `variant` is the
|
|
90
|
+
host-native per-agent option, while `team.json` remains the canonical desired
|
|
91
|
+
configuration. A null or blank model/effort removes the managed field and lets
|
|
92
|
+
OpenCode use its host or session default. `upgrade --apply` refreshes managed
|
|
93
|
+
agents and then reapplies this same binding; it does not rewrite `team.json`.
|
|
94
|
+
OpenCode 1.18.x exposes this contract through the `AgentConfig.variant` field.
|
|
95
|
+
|
|
72
96
|
## Start the loop
|
|
73
97
|
|
|
74
98
|
Launch OpenCode from the project-control repository, then enter:
|
|
@@ -31,7 +31,7 @@ adapter_root="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
|
|
31
31
|
repository_root="$(cd -- "$adapter_root/../.." && pwd)"
|
|
32
32
|
|
|
33
33
|
agent_names=(triad-orchestrator triad-developer triad-evaluator triad-reviewer)
|
|
34
|
-
skill_names=(triad-loop-bootstrap triad-loop-orchestrator triad-loop-developer triad-loop-evaluator triad-loop-reviewer)
|
|
34
|
+
skill_names=(triad-loop-bootstrap triad-loop-orchestrator triad-loop-developer triad-loop-evaluator triad-loop-reviewer triad-model-configuration)
|
|
35
35
|
command_name="triad"
|
|
36
36
|
collisions=()
|
|
37
37
|
|
package/adapters/registry.mjs
CHANGED
|
@@ -7,7 +7,8 @@ export const sharedSkillNames = [
|
|
|
7
7
|
'triad-loop-orchestrator',
|
|
8
8
|
'triad-loop-developer',
|
|
9
9
|
'triad-loop-evaluator',
|
|
10
|
-
'triad-loop-reviewer'
|
|
10
|
+
'triad-loop-reviewer',
|
|
11
|
+
'triad-model-configuration'
|
|
11
12
|
];
|
|
12
13
|
|
|
13
14
|
export const roleDefinitions = [
|
|
@@ -37,6 +38,7 @@ function projectRuntimeAssets(id) {
|
|
|
37
38
|
return [
|
|
38
39
|
{ source: 'runtime', destination: '.triad-runtime' },
|
|
39
40
|
{ source: 'schemas', destination: '.triad-runtime/schemas' },
|
|
41
|
+
{ source: 'integrations/bmad', destination: '.triad-runtime/integrations/bmad' },
|
|
40
42
|
{ source: `adapters/${id}/runtime.json`, destination: '.triad-runtime/adapter.json', file: true }
|
|
41
43
|
];
|
|
42
44
|
}
|
|
@@ -86,6 +88,13 @@ const definitions = [
|
|
|
86
88
|
entry: '/triad',
|
|
87
89
|
lifecycle: null,
|
|
88
90
|
modelBinding: 'project-frontmatter',
|
|
91
|
+
// OpenCode's agent frontmatter has a first-class `variant` field. Keep
|
|
92
|
+
// Triad's canonical `reasoning_effort` name in team.json and map it to
|
|
93
|
+
// that host-native field rather than emitting provider options such as
|
|
94
|
+
// `reasoningEffort` into the agent profile.
|
|
95
|
+
modelFields: ['model', 'variant'],
|
|
96
|
+
modelFieldSources: { model: 'model', variant: 'reasoning_effort' },
|
|
97
|
+
modelCleanupFields: ['reasoningEffort'],
|
|
89
98
|
modelRoles: roleDefinitions.map((role) => role.id),
|
|
90
99
|
projectAssets: [
|
|
91
100
|
{ source: 'adapters/opencode/.opencode/agents', destination: '.opencode/agents' },
|
|
@@ -112,6 +121,10 @@ const definitions = [
|
|
|
112
121
|
},
|
|
113
122
|
roleModelPaths(controlRoot) {
|
|
114
123
|
return roleDefinitions.map((role) => join(controlRoot, '.opencode', 'agents', `triad-${role.id}.md`));
|
|
124
|
+
},
|
|
125
|
+
globalRoleModelPaths() {
|
|
126
|
+
const root = hostHome('.config', 'opencode');
|
|
127
|
+
return rolePaths(root, roleDefinitions.map((role) => `triad-${role.id}`));
|
|
115
128
|
}
|
|
116
129
|
},
|
|
117
130
|
{
|
|
@@ -155,6 +168,10 @@ const definitions = [
|
|
|
155
168
|
},
|
|
156
169
|
roleModelPaths(controlRoot) {
|
|
157
170
|
return ['developer', 'reviewer', 'evaluator'].map((role) => join(controlRoot, '.claude', 'agents', `triad-${role}.md`));
|
|
171
|
+
},
|
|
172
|
+
globalRoleModelPaths() {
|
|
173
|
+
const root = hostHome('.claude');
|
|
174
|
+
return ['developer', 'reviewer', 'evaluator'].map((role) => join(root, 'agents', `triad-${role}.md`));
|
|
158
175
|
}
|
|
159
176
|
},
|
|
160
177
|
{
|
|
@@ -220,6 +237,7 @@ const definitions = [
|
|
|
220
237
|
lifecycle: null,
|
|
221
238
|
modelBinding: 'project-frontmatter',
|
|
222
239
|
modelFields: ['model', 'reasoningEffort'],
|
|
240
|
+
modelFieldSources: { model: 'model', reasoningEffort: 'reasoning_effort' },
|
|
223
241
|
modelRoles: roleDefinitions.map((role) => role.id),
|
|
224
242
|
projectAssets: [
|
|
225
243
|
{ source: 'adapters/copilot/.github/agents', destination: '.github/agents' },
|
|
@@ -251,6 +269,10 @@ const definitions = [
|
|
|
251
269
|
},
|
|
252
270
|
roleModelPaths(controlRoot) {
|
|
253
271
|
return roleDefinitions.map((role) => join(controlRoot, '.github', 'agents', `triad-${role.id}.agent.md`));
|
|
272
|
+
},
|
|
273
|
+
globalRoleModelPaths() {
|
|
274
|
+
const root = hostHome('.copilot');
|
|
275
|
+
return roleDefinitions.map((role) => join(root, 'agents', `triad-${role.id}.agent.md`));
|
|
254
276
|
}
|
|
255
277
|
}
|
|
256
278
|
];
|
package/bin/triad-plus.js
CHANGED
|
@@ -9,6 +9,17 @@ import process from 'node:process';
|
|
|
9
9
|
import { createInterface } from 'node:readline/promises';
|
|
10
10
|
import { getAdapter, listAdapters, roleDefinitions, sharedSkillNames } from '../adapters/registry.mjs';
|
|
11
11
|
import { writeImportedCard } from '../integrations/bmad/story-importer.mjs';
|
|
12
|
+
import {
|
|
13
|
+
hostModelField,
|
|
14
|
+
materializedHostRoleConfiguration,
|
|
15
|
+
supportsNativeReasoning,
|
|
16
|
+
validateTeamConfiguration
|
|
17
|
+
} from '../runtime/lib/model-config.mjs';
|
|
18
|
+
import {
|
|
19
|
+
formatDoctorLine,
|
|
20
|
+
formatDoctorSection,
|
|
21
|
+
formatSetupSummary
|
|
22
|
+
} from '../runtime/lib/terminal.mjs';
|
|
12
23
|
|
|
13
24
|
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
14
25
|
|
|
@@ -19,7 +30,7 @@ function usage(exitCode = 0) {
|
|
|
19
30
|
Usage:
|
|
20
31
|
npx triad-plus
|
|
21
32
|
npx triad-plus init --host <adapter-id> --control <path> [--global] [--team-config <path>] [--allow-product-repo]
|
|
22
|
-
npx triad-plus doctor --host <adapter-id> --control <path> [--hook-config <path>]
|
|
33
|
+
npx triad-plus doctor --host <adapter-id> --control <path> [--global] [--hook-config <path>]
|
|
23
34
|
npx triad-plus upgrade --host <adapter-id> --control <path> [--global] [--apply]
|
|
24
35
|
npx triad-plus import-bmad-story --source <story.md> --output <card.md> [--target-repository <id>] [--provenance <record.json>] [--required-gate <id>] [--depends-on <card-id>]
|
|
25
36
|
|
|
@@ -180,19 +191,13 @@ async function applyOverlay(controlRoot, apply, team) {
|
|
|
180
191
|
}
|
|
181
192
|
|
|
182
193
|
async function loadTeamConfig(options) {
|
|
183
|
-
if (options.team) return options.team;
|
|
194
|
+
if (options.team) return validateTeamConfiguration(options.team);
|
|
184
195
|
if (!options.teamConfig) return null;
|
|
185
196
|
let team;
|
|
186
197
|
try { team = JSON.parse(await readFile(resolve(options.teamConfig), 'utf8')); }
|
|
187
198
|
catch (error) { throw new Error(`Cannot read --team-config: ${error.message}`); }
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
const value = team.roles[role.id];
|
|
191
|
-
if (!value || typeof value.displayName !== 'string' || ![null, undefined].includes(value.model) && typeof value.model !== 'string') {
|
|
192
|
-
throw new Error(`--team-config has an invalid ${role.id} role definition.`);
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
return team;
|
|
199
|
+
try { return validateTeamConfiguration(team); }
|
|
200
|
+
catch (error) { throw new Error(`--team-config is invalid: ${error.message}`); }
|
|
196
201
|
}
|
|
197
202
|
|
|
198
203
|
async function writeTeamConfig(controlRoot, team) {
|
|
@@ -201,20 +206,22 @@ async function writeTeamConfig(controlRoot, team) {
|
|
|
201
206
|
await writeFile(target, `${JSON.stringify(team, null, 2)}\n`, 'utf8');
|
|
202
207
|
}
|
|
203
208
|
|
|
204
|
-
async function applyMarkdownModel(target, configuration, fields = ['model']) {
|
|
205
|
-
|
|
209
|
+
async function applyMarkdownModel(target, configuration, fields = ['model'], cleanupFields = []) {
|
|
210
|
+
const managedFields = [...new Set([...fields, ...cleanupFields])];
|
|
211
|
+
if (managedFields.length === 0) return;
|
|
206
212
|
const source = await readFile(target, 'utf8');
|
|
207
213
|
if (!source.startsWith('---\n')) throw new Error(`Agent definition has no YAML frontmatter: ${target}`);
|
|
208
214
|
const closing = source.indexOf('\n---\n', 4);
|
|
209
215
|
if (closing === -1) throw new Error(`Agent definition has invalid YAML frontmatter: ${target}`);
|
|
210
216
|
let frontmatter = source.slice(4, closing);
|
|
211
|
-
for (const field of
|
|
217
|
+
for (const field of managedFields) frontmatter = frontmatter.replace(new RegExp(`^${field}:\\s*.*\\n?`, 'm'), '');
|
|
212
218
|
const values = fields
|
|
213
|
-
.map((field) => ({ field, value:
|
|
219
|
+
.map((field) => ({ field, value: configuration?.[field] }))
|
|
214
220
|
.filter(({ value }) => value !== null && value !== undefined && value !== '');
|
|
215
|
-
if (values.length === 0) return;
|
|
216
221
|
const additions = values.map(({ field, value }) => `${field}: ${JSON.stringify(value)}`).join('\n');
|
|
217
|
-
|
|
222
|
+
const normalizedFrontmatter = frontmatter.replace(/\s*$/, '');
|
|
223
|
+
const next = `---\n${normalizedFrontmatter}${additions ? `\n${additions}` : ''}${source.slice(closing)}`;
|
|
224
|
+
if (next !== source) await writeFile(target, next, 'utf8');
|
|
218
225
|
}
|
|
219
226
|
|
|
220
227
|
async function writeRoleProfiles(paths, team) {
|
|
@@ -240,18 +247,29 @@ async function writeRoleProfiles(paths, team) {
|
|
|
240
247
|
}
|
|
241
248
|
}
|
|
242
249
|
|
|
243
|
-
async function applyTeamBinding(adapter, controlRoot, team, installContext) {
|
|
250
|
+
async function applyTeamBinding(adapter, controlRoot, team, installContext, scope = 'project') {
|
|
244
251
|
if (!team || adapter.modelBinding === 'team-record') return;
|
|
245
252
|
if (adapter.modelBinding === 'project-frontmatter') {
|
|
246
|
-
const
|
|
253
|
+
const pathFactory = scope === 'global' ? adapter.globalRoleModelPaths : adapter.roleModelPaths;
|
|
254
|
+
if (typeof pathFactory !== 'function') return;
|
|
255
|
+
const paths = scope === 'global'
|
|
256
|
+
? pathFactory(installContext)
|
|
257
|
+
: pathFactory(controlRoot, installContext);
|
|
247
258
|
const roles = (adapter.modelRoles ?? roleDefinitions.map((role) => role.id))
|
|
248
259
|
.map((roleId) => roleDefinitions.find((role) => role.id === roleId));
|
|
249
260
|
for (const [index, role] of roles.entries()) {
|
|
250
|
-
|
|
261
|
+
const fields = adapter.modelFields ?? ['model'];
|
|
262
|
+
await applyMarkdownModel(
|
|
263
|
+
paths[index],
|
|
264
|
+
materializedHostRoleConfiguration(adapter, role.id, team.roles[role.id]),
|
|
265
|
+
fields,
|
|
266
|
+
adapter.modelCleanupFields ?? []
|
|
267
|
+
);
|
|
251
268
|
}
|
|
252
269
|
return;
|
|
253
270
|
}
|
|
254
271
|
if (adapter.modelBinding === 'global-profiles') {
|
|
272
|
+
if (scope !== 'global') return;
|
|
255
273
|
await writeRoleProfiles(adapter.roleModelPaths(installContext), team);
|
|
256
274
|
return;
|
|
257
275
|
}
|
|
@@ -276,6 +294,100 @@ async function allExist(paths) {
|
|
|
276
294
|
return true;
|
|
277
295
|
}
|
|
278
296
|
|
|
297
|
+
function sharedSkillRoots(adapter, root, installContext, scope) {
|
|
298
|
+
const assets = scope === 'global' ? adapter.globalAssets : adapter.projectAssets;
|
|
299
|
+
return assets
|
|
300
|
+
.filter((asset) => asset.source === 'shared-skills')
|
|
301
|
+
.map((asset) => resolveDestination(asset.destination, root, installContext));
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
async function anyExist(paths) {
|
|
305
|
+
for (const target of paths) if (await exists(target)) return true;
|
|
306
|
+
return false;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/** Return one status per shared skill so a partial install cannot look healthy. */
|
|
310
|
+
async function sharedSkillStatuses(adapter, root, installContext, scope) {
|
|
311
|
+
const roots = sharedSkillRoots(adapter, root, installContext, scope);
|
|
312
|
+
if (roots.length === 0) return [];
|
|
313
|
+
const configuredRoots = [];
|
|
314
|
+
for (const skillRoot of roots) if (await exists(skillRoot)) configuredRoots.push(skillRoot);
|
|
315
|
+
const statuses = [];
|
|
316
|
+
for (const name of sharedSkillNames) {
|
|
317
|
+
const paths = roots.map((skillRoot) => join(skillRoot, name));
|
|
318
|
+
if (configuredRoots.length === 0) {
|
|
319
|
+
statuses.push({ name, status: 'not configured', paths });
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
let present = 0;
|
|
323
|
+
for (const target of paths) if (await exists(target)) present += 1;
|
|
324
|
+
statuses.push({ name, status: present === paths.length ? 'OK' : 'MISSING', paths });
|
|
325
|
+
}
|
|
326
|
+
return statuses;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function roleModelEntries(adapter, controlRoot, installContext, scope) {
|
|
330
|
+
let paths;
|
|
331
|
+
if (scope === 'global') {
|
|
332
|
+
if (adapter.modelBinding === 'global-profiles') paths = adapter.roleModelPaths?.(installContext) ?? [];
|
|
333
|
+
else paths = adapter.globalRoleModelPaths?.(installContext) ?? [];
|
|
334
|
+
} else {
|
|
335
|
+
if (adapter.modelBinding !== 'project-frontmatter') return [];
|
|
336
|
+
paths = adapter.roleModelPaths?.(controlRoot, installContext) ?? [];
|
|
337
|
+
}
|
|
338
|
+
const roleIds = adapter.modelRoles ?? roleDefinitions.map((role) => role.id);
|
|
339
|
+
return roleIds.map((roleId, index) => ({
|
|
340
|
+
role: roleDefinitions.find((definition) => definition.id === roleId),
|
|
341
|
+
path: paths[index]
|
|
342
|
+
})).filter((entry) => entry.role && entry.path);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function parseFrontmatterFields(source, fields) {
|
|
346
|
+
const result = {};
|
|
347
|
+
if (!source.startsWith('---\n')) return result;
|
|
348
|
+
const closing = source.indexOf('\n---\n', 4);
|
|
349
|
+
if (closing < 0) return result;
|
|
350
|
+
const frontmatter = source.slice(4, closing);
|
|
351
|
+
for (const field of fields) {
|
|
352
|
+
const match = frontmatter.match(new RegExp(`^${field}:\\s*(.*?)\\s*$`, 'm'));
|
|
353
|
+
if (!match) {
|
|
354
|
+
result[field] = null;
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
try { result[field] = JSON.parse(match[1]); }
|
|
358
|
+
catch { result[field] = match[1].replace(/^['"]|['"]$/g, ''); }
|
|
359
|
+
}
|
|
360
|
+
return result;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
async function modelBindingEvidence(adapter, controlRoot, installContext, team, scope) {
|
|
364
|
+
const entries = roleModelEntries(adapter, controlRoot, installContext, scope);
|
|
365
|
+
if (!entries.length) return [];
|
|
366
|
+
const evidence = [];
|
|
367
|
+
for (const { role, path: target } of entries) {
|
|
368
|
+
const configuration = team?.roles?.[role.id];
|
|
369
|
+
if (!configuration) {
|
|
370
|
+
evidence.push({ role, status: 'not configured', target });
|
|
371
|
+
continue;
|
|
372
|
+
}
|
|
373
|
+
const expected = materializedHostRoleConfiguration(adapter, role.id, configuration);
|
|
374
|
+
const fields = [...new Set([...(adapter.modelFields ?? Object.keys(expected)), ...(adapter.modelCleanupFields ?? [])])];
|
|
375
|
+
if (Object.keys(expected).length === 0) {
|
|
376
|
+
evidence.push({ role, status: 'UNSUPPORTED', target, expected, observed: null });
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
if (!(await exists(target))) {
|
|
380
|
+
evidence.push({ role, status: 'UNKNOWN / NOT OBSERVABLE', target, expected, observed: null });
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
const observed = parseFrontmatterFields(await readFile(target, 'utf8'), fields);
|
|
384
|
+
const matches = fields.every((field) => (expected[field] ?? null) === (observed[field] ?? null));
|
|
385
|
+
const allDefault = Object.values(expected).every((value) => value === null || value === undefined || value === '');
|
|
386
|
+
evidence.push({ role, status: matches && allDefault ? 'HOST DEFAULT' : matches ? 'VERIFIED' : 'MATERIALIZED MISMATCH', target, expected, observed });
|
|
387
|
+
}
|
|
388
|
+
return evidence;
|
|
389
|
+
}
|
|
390
|
+
|
|
279
391
|
function commandVersion(binary) {
|
|
280
392
|
const candidates = Array.isArray(binary) ? binary : [binary];
|
|
281
393
|
for (const candidate of candidates) {
|
|
@@ -307,8 +419,9 @@ async function collectTeamConfiguration(prompt, adapter) {
|
|
|
307
419
|
const displayName = (await prompt.question(`${role.label} display name [${role.label}]: `)).trim() || role.label;
|
|
308
420
|
const persona = (await prompt.question(`${role.label} persona [professional and role-focused]: `)).trim() || 'professional and role-focused';
|
|
309
421
|
const model = (await prompt.question(`${role.label} model ID [host default]: `)).trim();
|
|
310
|
-
const reasoningEffort =
|
|
311
|
-
? (await prompt.question(`${role.label} reasoning effort [${role.defaultEffort}]: `)).trim()
|
|
422
|
+
const reasoningEffort = supportsNativeReasoning(adapter, role.id)
|
|
423
|
+
? (await prompt.question(`${role.label} reasoning effort [${adapter.modelBinding === 'global-profiles' && model ? role.defaultEffort : 'host default'}]: `)).trim()
|
|
424
|
+
|| (adapter.modelBinding === 'global-profiles' && model ? role.defaultEffort : null)
|
|
312
425
|
: null;
|
|
313
426
|
roles[role.id] = { displayName, persona, model: model || null, reasoning_effort: reasoningEffort, enabled };
|
|
314
427
|
}
|
|
@@ -330,15 +443,19 @@ async function init(options) {
|
|
|
330
443
|
...adapter.projectPaths(controlRoot, installContext),
|
|
331
444
|
...(team ? [teamConfigPath(controlRoot)] : []),
|
|
332
445
|
...(options.global ? adapter.globalPaths(installContext) : []),
|
|
333
|
-
...(team && options.global && adapter.modelBinding === 'global-profiles' ? adapter.roleModelPaths(installContext) : [])
|
|
446
|
+
...(team && options.global && adapter.modelBinding === 'global-profiles' ? adapter.roleModelPaths(installContext) : []),
|
|
447
|
+
...(team && options.global && adapter.modelBinding === 'project-frontmatter' && adapter.globalRoleModelPaths
|
|
448
|
+
? adapter.globalRoleModelPaths(installContext)
|
|
449
|
+
: [])
|
|
334
450
|
];
|
|
335
451
|
const existing = await collisions(planned);
|
|
336
452
|
if (existing.length > 0) throw new Error(`Installation aborted; existing paths would be overwritten:\n${existing.map((target) => ` ${target}`).join('\n')}`);
|
|
337
453
|
await installAssets(adapter.projectAssets, controlRoot, installContext);
|
|
338
454
|
if (team) await writeTeamConfig(controlRoot, team);
|
|
339
455
|
if (team) await applyOverlay(controlRoot, true, team);
|
|
340
|
-
if (team) await applyTeamBinding(adapter, controlRoot, team, installContext);
|
|
341
456
|
if (options.global) await installAssets(adapter.globalAssets, controlRoot, installContext);
|
|
457
|
+
if (team) await applyTeamBinding(adapter, controlRoot, team, installContext, 'project');
|
|
458
|
+
if (team && options.global) await applyTeamBinding(adapter, controlRoot, team, installContext, 'global');
|
|
342
459
|
process.stdout.write(`Triad+ installed for ${adapter.label} in ${controlRoot}\n`);
|
|
343
460
|
if (adapter.globalEntry) {
|
|
344
461
|
process.stdout.write(`Install user-level assets with --global to expose ${adapter.entry}.\n`);
|
|
@@ -349,7 +466,7 @@ async function init(options) {
|
|
|
349
466
|
async function currentTeam(controlRoot) {
|
|
350
467
|
const target = teamConfigPath(controlRoot);
|
|
351
468
|
if (!(await exists(target))) return null;
|
|
352
|
-
try { return JSON.parse(await readFile(target, 'utf8')); }
|
|
469
|
+
try { return validateTeamConfiguration(JSON.parse(await readFile(target, 'utf8'))); }
|
|
353
470
|
catch { throw new Error(`Cannot safely upgrade an invalid team config: ${target}`); }
|
|
354
471
|
}
|
|
355
472
|
|
|
@@ -394,12 +511,15 @@ async function upgrade(options) {
|
|
|
394
511
|
const backupRoot = join(controlRoot, '.triad-plus', 'backups', stamp);
|
|
395
512
|
process.stdout.write(`Triad+ upgrade ${options.apply ? 'applying' : 'plan'} for ${adapter.label}\n`);
|
|
396
513
|
await refreshAssets(adapter.projectAssets, controlRoot, installContext, join(backupRoot, 'project'), options.apply);
|
|
514
|
+
if (team && options.apply && adapter.modelBinding === 'project-frontmatter') {
|
|
515
|
+
await applyTeamBinding(adapter, controlRoot, team, installContext, 'project');
|
|
516
|
+
}
|
|
397
517
|
await upgradeRunStateDelivery(controlRoot, backupRoot, options.apply);
|
|
398
518
|
if (team) await applyOverlay(controlRoot, options.apply, team);
|
|
399
519
|
else process.stdout.write(' Instructions skipped: .triad-plus/team.json is not configured\n');
|
|
400
520
|
if (options.global) {
|
|
401
521
|
await refreshAssets(adapter.globalAssets, controlRoot, installContext, join(backupRoot, 'global'), options.apply);
|
|
402
|
-
if (team) await applyTeamBinding(adapter, controlRoot, team, installContext);
|
|
522
|
+
if (team && options.apply) await applyTeamBinding(adapter, controlRoot, team, installContext, 'global');
|
|
403
523
|
}
|
|
404
524
|
if (!options.apply) process.stdout.write('Dry run only. Re-run with --apply to update managed assets.\n');
|
|
405
525
|
}
|
|
@@ -426,13 +546,18 @@ async function doctor(options) {
|
|
|
426
546
|
if (requested.some((adapter) => !adapter)) throw new Error(`Choose --host ${listAdapters().map((item) => item.id).join(', ')}.`);
|
|
427
547
|
let team = null;
|
|
428
548
|
if (await exists(teamConfigPath(controlRoot))) {
|
|
429
|
-
try { team = JSON.parse(await readFile(teamConfigPath(controlRoot), 'utf8')); }
|
|
549
|
+
try { team = validateTeamConfiguration(JSON.parse(await readFile(teamConfigPath(controlRoot), 'utf8'))); }
|
|
430
550
|
catch { team = 'invalid'; }
|
|
431
551
|
}
|
|
432
552
|
for (const adapter of requested) {
|
|
433
553
|
const installContext = context(controlRoot);
|
|
434
554
|
const absent = [];
|
|
435
555
|
for (const target of adapter.projectPaths(controlRoot, installContext)) if (!(await exists(target))) absent.push(target);
|
|
556
|
+
const globalDetected = adapter.globalPaths ? await anyExist(adapter.globalPaths(installContext)) : false;
|
|
557
|
+
const inspectGlobal = Boolean(options.global || globalDetected);
|
|
558
|
+
const globalTargets = inspectGlobal && adapter.globalPaths ? adapter.globalPaths(installContext) : [];
|
|
559
|
+
const globalAbsent = [];
|
|
560
|
+
for (const target of globalTargets) if (!(await exists(target))) globalAbsent.push(target);
|
|
436
561
|
const binary = commandVersion(adapter.binaryCandidates ?? adapter.binary);
|
|
437
562
|
const node = commandVersion('node');
|
|
438
563
|
const manifestPath = join(controlRoot, '.triad-runtime', 'adapter.json');
|
|
@@ -442,25 +567,71 @@ async function doctor(options) {
|
|
|
442
567
|
const roleTargets = targets.filter((target) => /[/\\]agents[/\\]triad-/.test(target));
|
|
443
568
|
const triadSkillTargets = targets.filter((target) => /[/\\]skills[/\\]triad$/.test(target));
|
|
444
569
|
const capability = manifest ? capabilitySnapshot(controlRoot, manifestPath) : null;
|
|
445
|
-
process.stdout.write(
|
|
446
|
-
process.stdout.write(
|
|
447
|
-
process.stdout.write(
|
|
448
|
-
process.stdout.write(`
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
process.stdout.write(`
|
|
452
|
-
process.stdout.write(`
|
|
453
|
-
|
|
570
|
+
process.stdout.write(`\n${formatDoctorSection(`Triad+ doctor — ${adapter.label}`)}\n`);
|
|
571
|
+
process.stdout.write(`${formatDoctorLine(adapter.label, absent.length || globalAbsent.length ? 'incomplete' : 'OK')}\n`);
|
|
572
|
+
process.stdout.write(`${formatDoctorSection('Runtime and installation')}\n`);
|
|
573
|
+
process.stdout.write(` ${formatDoctorLine('Host runtime', binary ? `OK (${binary})` : 'not installed or version unavailable')}\n`);
|
|
574
|
+
process.stdout.write(` ${formatDoctorLine('Verifier', node && await exists(join(controlRoot, '.triad-runtime', 'triad-verify.mjs')) ? 'OK' : 'incomplete')}\n`);
|
|
575
|
+
process.stdout.write(` ${formatDoctorLine('Adapter', manifest ? 'OK' : 'missing or different adapter')}\n`);
|
|
576
|
+
if (roleTargets.length) process.stdout.write(` ${formatDoctorLine('Role agents', await allExist(roleTargets) ? 'OK' : 'missing')}\n`);
|
|
577
|
+
if (triadSkillTargets.length) process.stdout.write(` ${formatDoctorLine('Triad skill', await allExist(triadSkillTargets) ? 'OK' : 'missing')}\n`);
|
|
578
|
+
const projectSharedSkills = await sharedSkillStatuses(adapter, controlRoot, installContext, 'project');
|
|
579
|
+
if (projectSharedSkills.length) {
|
|
580
|
+
process.stdout.write(`${formatDoctorSection('Shared skills')}\n`);
|
|
581
|
+
for (const skill of projectSharedSkills) process.stdout.write(` ${formatDoctorLine(skill.name, skill.status)}\n`);
|
|
582
|
+
}
|
|
583
|
+
if (inspectGlobal) {
|
|
584
|
+
const globalSharedSkills = await sharedSkillStatuses(adapter, controlRoot, installContext, 'global');
|
|
585
|
+
if (globalSharedSkills.length) {
|
|
586
|
+
process.stdout.write(`${formatDoctorSection('Global shared skills')}\n`);
|
|
587
|
+
for (const skill of globalSharedSkills) process.stdout.write(` ${formatDoctorLine(skill.name, skill.status)}\n`);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
process.stdout.write(`${formatDoctorSection('Verification and capabilities')}\n`);
|
|
591
|
+
process.stdout.write(` ${formatDoctorLine('Verification', capability?.verification?.selected_mode ?? 'unavailable')}${capability?.verification?.reason ? ` (${capability.verification.reason})` : ''}\n`);
|
|
592
|
+
process.stdout.write(`${formatDoctorSection('Configuration')}\n`);
|
|
593
|
+
process.stdout.write(` ${formatDoctorLine('Team config', team === 'invalid' ? 'invalid' : team ? 'OK' : 'not configured')}\n`);
|
|
594
|
+
process.stdout.write(` ${formatDoctorLine('Evaluator+', team?.roles?.evaluator?.enabled === true ? 'configured' : 'not configured')}\n`);
|
|
595
|
+
const modelFields = adapter.modelBinding === 'global-profiles'
|
|
596
|
+
? 'model, reasoning_effort'
|
|
597
|
+
: Array.isArray(adapter.modelFields) && adapter.modelFields.length ? adapter.modelFields.join(', ') : 'team.json record / host-managed';
|
|
598
|
+
process.stdout.write(` ${formatDoctorLine('Model binding', `${adapter.modelBinding ?? 'unavailable'} (${modelFields})`)}\n`);
|
|
599
|
+
const modelScopes = [];
|
|
600
|
+
const validTeam = team !== 'invalid' ? team : null;
|
|
601
|
+
if (adapter.modelBinding === 'project-frontmatter') {
|
|
602
|
+
modelScopes.push(['project', await modelBindingEvidence(adapter, controlRoot, installContext, validTeam, 'project')]);
|
|
603
|
+
}
|
|
604
|
+
if (inspectGlobal && (adapter.modelBinding === 'project-frontmatter' || adapter.modelBinding === 'global-profiles')) {
|
|
605
|
+
modelScopes.push(['global', await modelBindingEvidence(adapter, controlRoot, installContext, validTeam, 'global')]);
|
|
606
|
+
}
|
|
607
|
+
if (modelScopes.some(([, entries]) => entries.length)) {
|
|
608
|
+
process.stdout.write(`${formatDoctorSection('Model configuration evidence')}\n`);
|
|
609
|
+
for (const [scope, entries] of modelScopes) {
|
|
610
|
+
for (const entry of entries) {
|
|
611
|
+
const field = hostModelField(adapter, entry.role.id, 'reasoning_effort');
|
|
612
|
+
const desired = validTeam?.roles?.[entry.role.id];
|
|
613
|
+
const desiredText = desired
|
|
614
|
+
? `desired model=${desired.model || 'host default'}${field ? ` ${field}=${desired.reasoning_effort || 'host default'}` : ''}`
|
|
615
|
+
: 'desired unavailable';
|
|
616
|
+
const observedText = entry.observed ? ` — materialized ${JSON.stringify(entry.observed)}` : '';
|
|
617
|
+
process.stdout.write(` ${formatDoctorLine(`${scope} ${entry.role.label}`, entry.status)} ${desiredText}${observedText}\n`);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
if (adapter.modelBinding === 'project-frontmatter') {
|
|
621
|
+
process.stdout.write(` ${formatDoctorLine('Host default', 'UNKNOWN / NOT OBSERVABLE')}\n`);
|
|
622
|
+
process.stdout.write(` ${formatDoctorLine('Current session', 'UNKNOWN / NOT OBSERVABLE')}\n`);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
454
625
|
const overlay = await overlayPlan(controlRoot, team).catch(() => null);
|
|
455
|
-
process.stdout.write(` Instructions
|
|
626
|
+
process.stdout.write(` ${formatDoctorLine('Instructions', overlay ? overlay.action === 'update' ? 'managed' : `needs ${overlay.action}` : 'invalid managed block')}\n`);
|
|
456
627
|
const globalAgents = join(codexHome(), 'AGENTS.md');
|
|
457
628
|
if (await exists(globalAgents)) {
|
|
458
629
|
const globalText = await readFile(globalAgents, 'utf8');
|
|
459
630
|
const fixedIdentity = /(?:identity|name)[\s\S]{0,100}(?:always|only|must)/i.test(globalText);
|
|
460
|
-
process.stdout.write(` Identity policy
|
|
631
|
+
process.stdout.write(` ${formatDoctorLine('Identity policy', fixedIdentity ? 'host rule detected; review for Triad role conflicts' : 'no fixed host rule detected')}\n`);
|
|
461
632
|
}
|
|
462
633
|
if (options.hookConfig && adapter.lifecycle) {
|
|
463
|
-
process.stdout.write(` Hook config
|
|
634
|
+
process.stdout.write(` ${formatDoctorLine('Hook config', 'declared; run runtime capability detection for detailed status')}\n`);
|
|
464
635
|
}
|
|
465
636
|
}
|
|
466
637
|
}
|
|
@@ -480,6 +651,7 @@ async function interactiveInit() {
|
|
|
480
651
|
const control = (await prompt.question(`Project-control workspace [${join(process.cwd(), 'triad-control')}]: `)).trim() || join(process.cwd(), 'triad-control');
|
|
481
652
|
const global = ['y', 'yes'].includes((await prompt.question(`Also install user-level ${adapter.entry} assets? [y/N]: `)).trim().toLowerCase());
|
|
482
653
|
const team = await collectTeamConfiguration(prompt, adapter);
|
|
654
|
+
process.stdout.write(`\n${formatSetupSummary({ adapter, control, global, team })}`);
|
|
483
655
|
const confirm = (await prompt.question('Type install to continue: ')).trim().toLowerCase();
|
|
484
656
|
if (confirm !== 'install') return process.stdout.write('Cancelled. No files were changed.\n');
|
|
485
657
|
await init({ host: adapter.id, control, global, team });
|
package/docs/architecture.md
CHANGED
|
@@ -14,6 +14,14 @@ The host agent is the Orchestrator. It owns operational context, delegation, and
|
|
|
14
14
|
the next decision. The runtime does not schedule work or implement a general
|
|
15
15
|
state machine.
|
|
16
16
|
|
|
17
|
+
At assignment time the Orchestrator may materialize a small immutable
|
|
18
|
+
Assignment Packet. It is a bounded projection of the card and relevant context,
|
|
19
|
+
not a second control plane or PRD. The host receives an explicit dispatch
|
|
20
|
+
context whose `cwd` is the declared product worktree; control-workspace paths
|
|
21
|
+
remain explicit for policy, state, and evidence. Developer and Reviewer share
|
|
22
|
+
the packet, while mandatory repository skills continue to be read and checked
|
|
23
|
+
from their real worktree paths.
|
|
24
|
+
|
|
17
25
|
Adapters are registered descriptors in `adapters/registry.mjs`. They describe
|
|
18
26
|
only real host differences: binary discovery, installation destinations, native
|
|
19
27
|
entry point, optional hook lifecycle, and supported model binding. Runtime-
|