triad-plus 1.1.0 → 1.2.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 CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.2.0 — 2026-08-25
4
+
5
+ - Add safe dry-run/apply upgrades for existing project-control workspaces.
6
+ - Maintain a narrowly scoped Triad+ role-run instruction overlay and report
7
+ potential host identity-policy conflicts in `doctor`.
8
+ - Require the configured Orchestrator to introduce itself at the beginning of
9
+ every Triad+ run.
10
+
11
+ ## 1.1.1 — 2026-08-25
12
+
13
+ - Ensure every supported host adopts the configured Orchestrator display name
14
+ for owner-facing communication.
15
+ - Keep the public install-test fixture neutral.
16
+
3
17
  ## 1.1.0 — 2026-08-24
4
18
 
5
19
  - Automatically dispatch configured Evaluator+ after Triad approval, without
@@ -11,6 +11,20 @@ ask the owner to confirm it; never invent a match. Keep the project-control work
11
11
  from product repositories; delegate normal development and independent review to
12
12
  the corresponding Triad agents.
13
13
 
14
+ Before the first owner-facing reply, read `.triad-plus/team.json` when it exists.
15
+ User-facing identity is permanent: adopt its non-empty
16
+ `roles.orchestrator.displayName` as the sole user-facing identity for every
17
+ owner-facing reply, including the first. If the file is absent or has no
18
+ non-empty display name, use `Triad Orchestrator`; never present a hidden
19
+ intermediary or another Triad role to the owner. You may report delegated roles'
20
+ outputs, but never claim their identity.
21
+
22
+ After loading this configuration, the first owner-facing message of every Triad+
23
+ run must begin with a concise introduction: "I am <displayName>, the Triad+
24
+ Orchestrator for this run." Localize it to the configured interaction language,
25
+ then state in one sentence whether the run is new or resumed and what input was
26
+ received. Do this before delegating, discussing artifacts, or asking questions.
27
+
14
28
  Detect the runtime with `.triad-runtime/triad-runtime-capabilities.mjs --host
15
29
  antigravity`. Use explicit verification dispatch unless the recorded capability
16
30
  snapshot proves a supported asynchronous route. Govern ordinary disagreements
@@ -10,6 +10,20 @@ request. Read `.triad-plus/team.json` when present and use its language, owner
10
10
  address, display names, personas, and model contract; technical role IDs remain
11
11
  unchanged.
12
12
 
13
+ Before the first owner-facing reply, read `.triad-plus/team.json` when it exists.
14
+ User-facing identity is permanent: adopt its non-empty
15
+ `roles.orchestrator.displayName` as the sole user-facing identity for every
16
+ owner-facing reply, including the first. If the file is absent or has no
17
+ non-empty display name, use `Triad Orchestrator`; never present a hidden
18
+ intermediary or another Triad role to the owner. You may report delegated roles'
19
+ outputs, but never claim their identity.
20
+
21
+ After loading this configuration, the first owner-facing message of every Triad+
22
+ run must begin with a concise introduction: "I am <displayName>, the Triad+
23
+ Orchestrator for this run." Localize it to the configured interaction language,
24
+ then state in one sentence whether the run is new or resumed and what input was
25
+ received. Do this before delegating, discussing artifacts, or asking questions.
26
+
13
27
  Before work, compare every role model that the host exposes with the recorded
14
28
  contract. If Antigravity does not expose a model identity, say that it cannot be
15
29
  verified and ask the owner to select or confirm it; never claim a model match
@@ -18,6 +18,20 @@ language, owner address, display names, personas, and model contract in communic
18
18
  technical role identifiers and authority remain unchanged. If the active
19
19
  Orchestrator model cannot meet the recorded contract, say so before work starts.
20
20
 
21
+ Before the first owner-facing reply, read `.triad-plus/team.json` when it exists.
22
+ User-facing identity is permanent: adopt its non-empty
23
+ `roles.orchestrator.displayName` as the sole user-facing identity for every
24
+ owner-facing reply, including the first. If the file is absent or has no
25
+ non-empty display name, use `Triad Orchestrator`; never present a hidden
26
+ intermediary or another Triad role to the owner. You may report delegated roles'
27
+ outputs, but never claim their identity.
28
+
29
+ After loading this configuration, the first owner-facing message of every Triad+
30
+ run must begin with a concise introduction: "I am <displayName>, the Triad+
31
+ Orchestrator for this run." Localize it to the configured interaction language,
32
+ then state in one sentence whether the run is new or resumed and what input was
33
+ received. Do this before delegating, discussing artifacts, or asking questions.
34
+
21
35
  Delegate implementation to `triad-developer` and review to `triad-reviewer`.
22
36
  After Triad approval, automatically invoke fresh `triad-evaluator` when
23
37
  `.triad-plus/team.json` has `roles.evaluator.enabled: true`; false or omitted
@@ -18,6 +18,20 @@ language, owner address, display names, personas, and model contract in communic
18
18
  technical role identifiers and authority remain unchanged. If the active
19
19
  Orchestrator model cannot meet the recorded contract, say so before work starts.
20
20
 
21
+ Before the first owner-facing reply, read `.triad-plus/team.json` when it exists.
22
+ User-facing identity is permanent: adopt its non-empty
23
+ `roles.orchestrator.displayName` as the sole user-facing identity for every
24
+ owner-facing reply, including the first. If the file is absent or has no
25
+ non-empty display name, use `Triad Orchestrator`; never present a hidden
26
+ intermediary or another Triad role to the owner. You may report delegated roles'
27
+ outputs, but never claim their identity.
28
+
29
+ After loading this configuration, the first owner-facing message of every Triad+
30
+ run must begin with a concise introduction: "I am <displayName>, the Triad+
31
+ Orchestrator for this run." Localize it to the configured interaction language,
32
+ then state in one sentence whether the run is new or resumed and what input was
33
+ received. Do this before delegating, discussing artifacts, or asking questions.
34
+
21
35
  Delegate implementation to the configured `triad_developer` profile and review to
22
36
  `triad_reviewer`. Once Triad is approved, automatically invoke a fresh
23
37
  `triad_evaluator` when `.triad-plus/team.json` has `roles.evaluator.enabled: true`.
@@ -11,6 +11,20 @@ Operate the current Hermes session as the Triad Orchestrator. Load
11
11
  when present; its role IDs, names, personas, and model/provider contract are
12
12
  configuration, not workflow authority.
13
13
 
14
+ Before the first owner-facing reply, read `.triad-plus/team.json` when it exists.
15
+ User-facing identity is permanent: adopt its non-empty
16
+ `roles.orchestrator.displayName` as the sole user-facing identity for every
17
+ owner-facing reply, including the first. If the file is absent or has no
18
+ non-empty display name, use `Triad Orchestrator`; never present a hidden
19
+ intermediary or another Triad role to the owner. You may report delegated roles'
20
+ outputs, but never claim their identity.
21
+
22
+ After loading this configuration, the first owner-facing message of every Triad+
23
+ run must begin with a concise introduction: "I am <displayName>, the Triad+
24
+ Orchestrator for this run." Localize it to the configured interaction language,
25
+ then state in one sentence whether the run is new or resumed and what input was
26
+ received. Do this before delegating, discussing artifacts, or asking questions.
27
+
14
28
  For a normal run, govern only Orchestrator, Developer, and Reviewer. Delegate
15
29
  ordinary implementation with `hermes chat -q --in <worktree> --no-restore-cwd`
16
30
  using `--skills triad-loop-developer`; obtain a separate Reviewer result with
@@ -28,6 +28,20 @@ If `.triad-plus/team.json` exists, load it before replying and use its language,
28
28
  owner address, display names, and personas in communication. Technical role IDs and their
29
29
  authority never change; stop before work if the configured model contract fails.
30
30
 
31
+ Before the first owner-facing reply, read `.triad-plus/team.json` when it exists.
32
+ User-facing identity is permanent: adopt its non-empty
33
+ `roles.orchestrator.displayName` as the sole user-facing identity for every
34
+ owner-facing reply, including the first. If the file is absent or has no
35
+ non-empty display name, use `Triad Orchestrator`; never present a hidden
36
+ intermediary or another Triad role to the owner. You may report delegated roles'
37
+ outputs, but never claim their identity.
38
+
39
+ After loading this configuration, the first owner-facing message of every Triad+
40
+ run must begin with a concise introduction: "I am <displayName>, the Triad+
41
+ Orchestrator for this run." Localize it to the configured interaction language,
42
+ then state in one sentence whether the run is new or resumed and what input was
43
+ received. Do this before delegating, discussing artifacts, or asking questions.
44
+
31
45
  Start by loading `triad-loop-bootstrap` for a new project, or
32
46
  `triad-loop-orchestrator` for an initialized project. Follow the loaded skill
33
47
  exactly. Keep the project-control workspace separate from product repositories,
@@ -11,6 +11,20 @@ If `.triad-plus/team.json` exists, load it before replying. Use its interaction
11
11
  language, owner address, display names, personas, and model contract in communication;
12
12
  technical role identifiers and authority remain unchanged.
13
13
 
14
+ Before the first owner-facing reply, read `.triad-plus/team.json` when it exists.
15
+ User-facing identity is permanent: adopt its non-empty
16
+ `roles.orchestrator.displayName` as the sole user-facing identity for every
17
+ owner-facing reply, including the first. If the file is absent or has no
18
+ non-empty display name, use `Triad Orchestrator`; never present a hidden
19
+ intermediary or another Triad role to the owner. You may report delegated roles'
20
+ outputs, but never claim their identity.
21
+
22
+ After loading this configuration, the first owner-facing message of every Triad+
23
+ run must begin with a concise introduction: "I am <displayName>, the Triad+
24
+ Orchestrator for this run." Localize it to the configured interaction language,
25
+ then state in one sentence whether the run is new or resumed and what input was
26
+ received. Do this before delegating, discussing artifacts, or asking questions.
27
+
14
28
  If this is a new project, load `triad-loop-bootstrap`, collect only missing
15
29
  inputs that prevent safe setup or measurable feature cards, create the isolated
16
30
  project-control workspace, and show the full feature-card plan before starting.
package/bin/triad-plus.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { access, cp, mkdir, readFile, stat, writeFile } from 'node:fs/promises';
3
+ import { access, cp, mkdir, readFile, rm, stat, writeFile } from 'node:fs/promises';
4
4
  import { spawnSync } from 'node:child_process';
5
5
  import { homedir } from 'node:os';
6
6
  import { fileURLToPath } from 'node:url';
@@ -19,22 +19,24 @@ Usage:
19
19
  npx triad-plus
20
20
  npx triad-plus init --host <adapter-id> --control <path> [--global] [--team-config <path>] [--allow-product-repo]
21
21
  npx triad-plus doctor --host <adapter-id> --control <path> [--hook-config <path>]
22
+ npx triad-plus upgrade --host <adapter-id> --control <path> [--global] [--apply]
22
23
 
23
24
  Adapters: ${listAdapters().map((adapter) => adapter.id).join(', ')}
24
25
 
25
26
  The control path is a project-control workspace, not a product repository.
26
- Installation refuses every asset overwrite. Doctor is read-only.
27
+ Installation refuses every asset overwrite. Upgrade is a dry run unless --apply is supplied.
27
28
  `);
28
29
  process.exit(exitCode);
29
30
  }
30
31
 
31
32
  function parseArgs(args) {
32
33
  const [command, ...rest] = args;
33
- const options = { command, global: false, allowProductRepo: false };
34
+ const options = { command, global: false, allowProductRepo: false, apply: false };
34
35
  for (let index = 0; index < rest.length; index += 1) {
35
36
  const argument = rest[index];
36
37
  if (argument === '--global') options.global = true;
37
38
  else if (argument === '--allow-product-repo') options.allowProductRepo = true;
39
+ else if (argument === '--apply') options.apply = true;
38
40
  else if (['--host', '--control', '--team-config', '--hook-config'].includes(argument)) {
39
41
  const value = rest[index + 1];
40
42
  if (!value || value.startsWith('--')) throw new Error(`${argument} requires a value.`);
@@ -100,10 +102,63 @@ async function installAssets(assets, root, installContext) {
100
102
  for (const asset of assets) await copyAsset(asset, root, installContext);
101
103
  }
102
104
 
105
+ async function replaceManagedPath(source, destination, backup, apply) {
106
+ const present = await exists(destination);
107
+ process.stdout.write(` ${apply ? 'Update' : 'Would update'} ${destination}${present ? ' (backup)' : ''}\n`);
108
+ if (!apply) return;
109
+ if (present) {
110
+ await mkdir(dirname(backup), { recursive: true });
111
+ await cp(destination, backup, { recursive: true });
112
+ await rm(destination, { recursive: true, force: true });
113
+ }
114
+ await mkdir(dirname(destination), { recursive: true });
115
+ await cp(source, destination, { recursive: true });
116
+ }
117
+
118
+ async function refreshAssets(assets, root, installContext, backupRoot, apply) {
119
+ for (const asset of assets) {
120
+ const destination = resolveDestination(asset.destination, root, installContext);
121
+ if (asset.source === 'shared-skills') {
122
+ for (const name of sharedSkillNames) await replaceManagedPath(join(packageRoot, 'skills', name), join(destination, name), join(backupRoot, name), apply);
123
+ } else {
124
+ await replaceManagedPath(sourcePath(asset.source), destination, join(backupRoot, asset.source.replaceAll('/', '__')), apply);
125
+ }
126
+ }
127
+ }
128
+
103
129
  function teamConfigPath(controlRoot) {
104
130
  return join(controlRoot, '.triad-plus', 'team.json');
105
131
  }
106
132
 
133
+ const overlayStart = '<!-- triad-plus:managed-instructions:start -->';
134
+ const overlayEnd = '<!-- triad-plus:managed-instructions:end -->';
135
+ const instructionOverlay = `${overlayStart}
136
+ ## Triad+ role-run overlay
137
+
138
+ When a Triad+ entry point is invoked in this control workspace, read
139
+ \`.triad-plus/team.json\` before the first owner-facing reply. The active
140
+ Orchestrator presents itself only as \`roles.orchestrator.displayName\`. This
141
+ is a role-run presentation rule; it does not change technical authority,
142
+ repository policy, safety instructions, or the host's identity outside Triad+.
143
+ ${overlayEnd}`;
144
+
145
+ async function overlayPlan(controlRoot) {
146
+ const target = join(controlRoot, 'AGENTS.md');
147
+ if (!(await exists(target))) return { target, action: 'create', content: `# Project instructions\n\n${instructionOverlay}\n` };
148
+ const source = await readFile(target, 'utf8');
149
+ const start = source.indexOf(overlayStart);
150
+ const end = source.indexOf(overlayEnd);
151
+ if (start === -1 && end === -1) return { target, action: 'append', content: `${source.replace(/\s*$/, '')}\n\n${instructionOverlay}\n` };
152
+ if (start < 0 || end < start) throw new Error(`Cannot safely update managed instruction block: ${target}`);
153
+ return { target, action: 'update', content: `${source.slice(0, start)}${instructionOverlay}${source.slice(end + overlayEnd.length)}` };
154
+ }
155
+
156
+ async function applyOverlay(controlRoot, apply) {
157
+ const plan = await overlayPlan(controlRoot);
158
+ process.stdout.write(` Instructions ${apply ? plan.action : `would ${plan.action}`} ${plan.target}\n`);
159
+ if (apply) await writeFile(plan.target, plan.content, 'utf8');
160
+ }
161
+
107
162
  async function loadTeamConfig(options) {
108
163
  if (options.team) return options.team;
109
164
  if (!options.teamConfig) return null;
@@ -236,6 +291,7 @@ async function init(options) {
236
291
  if (existing.length > 0) throw new Error(`Installation aborted; existing paths would be overwritten:\n${existing.map((target) => ` ${target}`).join('\n')}`);
237
292
  await installAssets(adapter.projectAssets, controlRoot, installContext);
238
293
  if (team) await writeTeamConfig(controlRoot, team);
294
+ if (team) await applyOverlay(controlRoot, true);
239
295
  if (team) await applyTeamBinding(adapter, controlRoot, team, installContext);
240
296
  if (options.global) await installAssets(adapter.globalAssets, controlRoot, installContext);
241
297
  process.stdout.write(`Triad+ installed for ${adapter.label} in ${controlRoot}\n`);
@@ -245,6 +301,34 @@ async function init(options) {
245
301
  process.stdout.write(`Open the control workspace and use ${adapter.entry} <PRD path>. Configured Evaluator+ runs automatically post-approval.\n`);
246
302
  }
247
303
 
304
+ async function currentTeam(controlRoot) {
305
+ const target = teamConfigPath(controlRoot);
306
+ if (!(await exists(target))) return null;
307
+ try { return JSON.parse(await readFile(target, 'utf8')); }
308
+ catch { throw new Error(`Cannot safely upgrade an invalid team config: ${target}`); }
309
+ }
310
+
311
+ async function upgrade(options) {
312
+ const adapter = getAdapter(options.host);
313
+ if (!adapter) throw new Error(`Choose --host ${listAdapters().map((item) => item.id).join(', ')}.`);
314
+ if (!options.control) throw new Error('Provide --control <project-control-path>.');
315
+ const controlRoot = resolve(options.control);
316
+ await requireDirectory(controlRoot);
317
+ const team = await currentTeam(controlRoot);
318
+ const installContext = context(controlRoot);
319
+ const stamp = new Date().toISOString().replaceAll(':', '-').replaceAll('.', '-');
320
+ const backupRoot = join(controlRoot, '.triad-plus', 'backups', stamp);
321
+ process.stdout.write(`Triad+ upgrade ${options.apply ? 'applying' : 'plan'} for ${adapter.label}\n`);
322
+ await refreshAssets(adapter.projectAssets, controlRoot, installContext, join(backupRoot, 'project'), options.apply);
323
+ if (team) await applyOverlay(controlRoot, options.apply);
324
+ else process.stdout.write(' Instructions skipped: .triad-plus/team.json is not configured\n');
325
+ if (options.global) {
326
+ await refreshAssets(adapter.globalAssets, controlRoot, installContext, join(backupRoot, 'global'), options.apply);
327
+ if (team) await applyTeamBinding(adapter, controlRoot, team, installContext);
328
+ }
329
+ if (!options.apply) process.stdout.write('Dry run only. Re-run with --apply to update managed assets.\n');
330
+ }
331
+
248
332
  async function doctor(options) {
249
333
  if (!options.control) throw new Error('Provide --control <project-control-path>.');
250
334
  const controlRoot = resolve(options.control);
@@ -270,6 +354,14 @@ async function doctor(options) {
270
354
  process.stdout.write(` Adapter ${manifest ? 'OK' : 'missing or different adapter'}\n`);
271
355
  process.stdout.write(` Team config ${team === 'invalid' ? 'invalid' : team ? 'OK' : 'not configured'}\n`);
272
356
  process.stdout.write(` Evaluator+ ${team?.roles?.evaluator?.enabled === true ? 'configured' : 'not configured'}\n`);
357
+ const overlay = await overlayPlan(controlRoot).catch(() => null);
358
+ process.stdout.write(` Instructions ${overlay ? overlay.action === 'update' ? 'managed' : `needs ${overlay.action}` : 'invalid managed block'}\n`);
359
+ const globalAgents = join(codexHome(), 'AGENTS.md');
360
+ if (await exists(globalAgents)) {
361
+ const globalText = await readFile(globalAgents, 'utf8');
362
+ const fixedIdentity = /(?:identity|name)[\s\S]{0,100}(?:always|only|must)/i.test(globalText);
363
+ process.stdout.write(` Identity policy ${fixedIdentity ? 'host rule detected; review for Triad role conflicts' : 'no fixed host rule detected'}\n`);
364
+ }
273
365
  if (options.hookConfig && adapter.lifecycle) {
274
366
  process.stdout.write(` Hook config declared; run runtime capability detection for detailed status\n`);
275
367
  }
@@ -302,6 +394,7 @@ try {
302
394
  const options = parseArgs(process.argv.slice(2));
303
395
  if (options.command === 'init') await init(options);
304
396
  else if (options.command === 'doctor') await doctor(options);
397
+ else if (options.command === 'upgrade') await upgrade(options);
305
398
  else if (!options.command) await interactiveInit();
306
399
  else if (options.command === '--help' || options.command === '-h') usage(0);
307
400
  else throw new Error(`Unknown command: ${options.command}`);
@@ -18,6 +18,23 @@ npx triad-plus init --host codex --control /path/to/project-control --global
18
18
  npx triad-plus doctor --host codex --control /path/to/project-control
19
19
  ```
20
20
 
21
+ ## Upgrade an existing control workspace
22
+
23
+ `upgrade` refreshes only Triad-managed runtime, skill, adapter, and optional
24
+ host-entry assets. It never changes `team.json`, `.loop/`, PRD files, evidence,
25
+ or product repositories. The default is a dry run:
26
+
27
+ ```bash
28
+ npx triad-plus upgrade --host codex --control /path/to/project-control --global
29
+ npx triad-plus upgrade --host codex --control /path/to/project-control --global --apply
30
+ ```
31
+
32
+ Before replacing a managed asset, the applied upgrade saves its prior copy under
33
+ `.triad-plus/backups/`. Triad+ also maintains a clearly marked role-run block in
34
+ the control workspace `AGENTS.md`; existing instructions are preserved. Review
35
+ `doctor` output when host-level instructions impose a fixed identity, because a
36
+ higher-priority host policy can prevent the configured Orchestrator identity.
37
+
21
38
  Supported hosts: `codex`, `opencode`, `claude-code`, `antigravity`, `hermes`.
22
39
  Use `--global` to install a host-level entry point where desired. The installer
23
40
  refuses overwrites. If the control path is recognizably a product Git repository,
@@ -1,6 +1,5 @@
1
1
  {
2
- "minimum_codex_cli_version": "0.148.0",
3
- "purpose": "Triad external verification dispatch only; this hook never changes card state.",
2
+ "description": "Triad external verification dispatch only; requires Codex CLI 0.148.0 or newer and never changes card state.",
4
3
  "hooks": {
5
4
  "SubagentStop": [
6
5
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "triad-plus",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "A lightweight, evidence-backed engineering loop for coding agents.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -9,6 +9,21 @@ Maintain the goal and operational context. Decide the next step; do not perform
9
9
  ordinary implementation or review. Read `project.yaml`, the frozen PRD, queue,
10
10
  decision policy, current records, and `.triad-plus/team.json` when present.
11
11
 
12
+ Before the first owner-facing reply, read `.triad-plus/team.json` when it exists.
13
+ User-facing identity is permanent: adopt its non-empty
14
+ `roles.orchestrator.displayName` as the sole user-facing identity for every
15
+ owner-facing reply, including the first. If the file is absent or has no
16
+ non-empty display name, use `Triad Orchestrator`; never present a hidden
17
+ intermediary or another Triad role to the owner. You may report delegated roles'
18
+ outputs, but never claim their identity. Technical role IDs and authority remain
19
+ unchanged.
20
+
21
+ After loading this configuration, the first owner-facing message of every Triad+
22
+ run must begin with a concise introduction: "I am <displayName>, the Triad+
23
+ Orchestrator for this run." Localize it to the configured interaction language,
24
+ then state in one sentence whether the run is new or resumed and what input was
25
+ received. Do this before delegating, discussing artifacts, or asking questions.
26
+
12
27
  ## Run one card
13
28
 
14
29
  1. Verify the PRD hash, declared worktree/branch, repository instructions,