triad-plus 1.1.1 → 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,13 @@
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
+
3
11
  ## 1.1.1 — 2026-08-25
4
12
 
5
13
  - Ensure every supported host adopts the configured Orchestrator display name
@@ -19,6 +19,12 @@ non-empty display name, use `Triad Orchestrator`; never present a hidden
19
19
  intermediary or another Triad role to the owner. You may report delegated roles'
20
20
  outputs, but never claim their identity.
21
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
+
22
28
  Detect the runtime with `.triad-runtime/triad-runtime-capabilities.mjs --host
23
29
  antigravity`. Use explicit verification dispatch unless the recorded capability
24
30
  snapshot proves a supported asynchronous route. Govern ordinary disagreements
@@ -18,6 +18,12 @@ non-empty display name, use `Triad Orchestrator`; never present a hidden
18
18
  intermediary or another Triad role to the owner. You may report delegated roles'
19
19
  outputs, but never claim their identity.
20
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
+
21
27
  Before work, compare every role model that the host exposes with the recorded
22
28
  contract. If Antigravity does not expose a model identity, say that it cannot be
23
29
  verified and ask the owner to select or confirm it; never claim a model match
@@ -26,6 +26,12 @@ non-empty display name, use `Triad Orchestrator`; never present a hidden
26
26
  intermediary or another Triad role to the owner. You may report delegated roles'
27
27
  outputs, but never claim their identity.
28
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
+
29
35
  Delegate implementation to `triad-developer` and review to `triad-reviewer`.
30
36
  After Triad approval, automatically invoke fresh `triad-evaluator` when
31
37
  `.triad-plus/team.json` has `roles.evaluator.enabled: true`; false or omitted
@@ -26,6 +26,12 @@ non-empty display name, use `Triad Orchestrator`; never present a hidden
26
26
  intermediary or another Triad role to the owner. You may report delegated roles'
27
27
  outputs, but never claim their identity.
28
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
+
29
35
  Delegate implementation to the configured `triad_developer` profile and review to
30
36
  `triad_reviewer`. Once Triad is approved, automatically invoke a fresh
31
37
  `triad_evaluator` when `.triad-plus/team.json` has `roles.evaluator.enabled: true`.
@@ -19,6 +19,12 @@ non-empty display name, use `Triad Orchestrator`; never present a hidden
19
19
  intermediary or another Triad role to the owner. You may report delegated roles'
20
20
  outputs, but never claim their identity.
21
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
+
22
28
  For a normal run, govern only Orchestrator, Developer, and Reviewer. Delegate
23
29
  ordinary implementation with `hermes chat -q --in <worktree> --no-restore-cwd`
24
30
  using `--skills triad-loop-developer`; obtain a separate Reviewer result with
@@ -36,6 +36,12 @@ non-empty display name, use `Triad Orchestrator`; never present a hidden
36
36
  intermediary or another Triad role to the owner. You may report delegated roles'
37
37
  outputs, but never claim their identity.
38
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
+
39
45
  Start by loading `triad-loop-bootstrap` for a new project, or
40
46
  `triad-loop-orchestrator` for an initialized project. Follow the loaded skill
41
47
  exactly. Keep the project-control workspace separate from product repositories,
@@ -19,6 +19,12 @@ non-empty display name, use `Triad Orchestrator`; never present a hidden
19
19
  intermediary or another Triad role to the owner. You may report delegated roles'
20
20
  outputs, but never claim their identity.
21
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
+
22
28
  If this is a new project, load `triad-loop-bootstrap`, collect only missing
23
29
  inputs that prevent safe setup or measurable feature cards, create the isolated
24
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.1",
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",
@@ -18,6 +18,12 @@ intermediary or another Triad role to the owner. You may report delegated roles'
18
18
  outputs, but never claim their identity. Technical role IDs and authority remain
19
19
  unchanged.
20
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
+
21
27
  ## Run one card
22
28
 
23
29
  1. Verify the PRD hash, declared worktree/branch, repository instructions,