triad-plus 1.4.0 → 1.4.1

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,14 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.4.1 — 2026-08-28
4
+
5
+ - Fix Codex Orchestrator liveness across `wait_agent` polling timeouts. While a
6
+ delegated Developer or Reviewer assignment remains active, a timeout is
7
+ non-terminal: the Orchestrator refreshes status and waits again in the same
8
+ parent turn instead of returning control to the owner. Validated with real
9
+ Codex Developer and Reviewer wait timeouts, explicit verification,
10
+ independent review, dependent-card continuation, and zero owner follow-ups.
11
+
3
12
  ## 1.3.0 — 2026-08-26
4
13
 
5
14
  - Keep the declared card chain unattended from Developer completion through
@@ -39,3 +39,10 @@ verification dispatch even when an async hook is available. The async
39
39
  `SubagentStop` route remains an experimental opt-in (`async_hook`) and falls
40
40
  back to explicit dispatch when unavailable. On Codex CLI 0.142, explicit
41
41
  dispatch is the safe expected route.
42
+
43
+ The Codex conversation remains the Orchestrator parent while a delegated
44
+ Developer or Reviewer is active. A `wait_agent` timeout is only a polling
45
+ interval: the parent refreshes status and waits again in the same turn. A
46
+ progress update never returns control to the owner while delegated work
47
+ remains active; completion is collected automatically and proceeds to
48
+ verification, review, and the next dependency-satisfied card.
@@ -57,3 +57,27 @@ for owner input: immediately wait for configured hook evidence or invoke the
57
57
  verifier, then dispatch the Reviewer on a pass. Ask the owner only for a
58
58
  declared escalation, a blocked verdict, an unrecoverable runtime error, or an
59
59
  explicit owner pause.
60
+
61
+ ## Codex parent-turn liveness (mandatory)
62
+
63
+ The current Codex conversation is the Orchestrator parent. The Orchestrator
64
+ MUST NOT end its owner-facing turn while any delegated Developer or Reviewer
65
+ assignment is still active, unless there is a declared escalation, a `blocked`
66
+ verdict, an unrecoverable runtime failure, or an explicit owner pause.
67
+
68
+ `wait_agent` timeout is only a polling interval elapsed. It is not an agent
69
+ failure, assignment completion, blocked condition, or owner wait. After a
70
+ timeout, refresh the assignment and delegated-agent status. If it is still
71
+ active, immediately call `wait_agent` again in the same Orchestrator turn. If
72
+ it completed, collect the report and continue the normal transition; if it
73
+ failed or disappeared, classify the runtime failure.
74
+
75
+ A progress update is informational and non-pausing. It must be followed
76
+ immediately by the next wait or transition, never by returning the conversation
77
+ to the owner while delegated work remains active.
78
+
79
+ When the Developer completes, collect the report and invoke explicit
80
+ `triad-verify` without owner input. When the Reviewer completes, record its
81
+ `approved`, `rework`, or `blocked` verdict; after `approved`, commit as
82
+ required, promote dependency-satisfied cards, and activate the next card
83
+ immediately. The same liveness rule applies while waiting for the Reviewer.
@@ -12,3 +12,9 @@ The async hook remains available as an experimental opt-in by setting
12
12
  `requested_mode=async_hook` in capability detection. Hook output is evidence
13
13
  only; it never changes Triad state by itself. If the requested experimental hook
14
14
  is unavailable, capability detection fails safe to explicit dispatch.
15
+
16
+ During delegated work the Codex Orchestrator parent turn stays alive. A
17
+ `wait_agent` timeout is a non-terminal polling interval: refresh status and
18
+ wait again in the same turn while the Developer or Reviewer remains active.
19
+ Progress messages are informational, not owner-wait points; completion flows
20
+ directly to verification, review, and card continuation.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "triad-plus",
3
- "version": "1.4.0",
3
+ "version": "1.4.1",
4
4
  "description": "A lightweight, evidence-backed engineering loop for coding agents.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -14,6 +14,7 @@
14
14
  "runtime/",
15
15
  "schemas/",
16
16
  "skills/",
17
+ "tests/codex-liveness-test.mjs",
17
18
  "docs/",
18
19
  "assets/",
19
20
  "README.md",
@@ -27,7 +28,7 @@
27
28
  "node": ">=20"
28
29
  },
29
30
  "scripts": {
30
- "test": "node tests/runtime-forward-test.mjs && node tests/cli-install-test.mjs",
31
+ "test": "node tests/runtime-forward-test.mjs && node tests/cli-install-test.mjs && node tests/codex-liveness-test.mjs",
31
32
  "pack:check": "npm pack --dry-run"
32
33
  },
33
34
  "repository": {
@@ -80,6 +80,36 @@ An owner-facing activation or progress update is informational output, never an
80
80
  implicit pause. After sending it, continue the recorded next action without
81
81
  waiting for a reply unless one of the valid human wait conditions applies.
82
82
 
83
+ ## Orchestrator liveness while delegated work is active
84
+
85
+ The Orchestrator parent turn is the owner of unattended progress. It MUST NOT
86
+ end its owner-facing turn while a delegated Developer or Reviewer assignment is
87
+ active, unless a declared escalation, a `blocked` verdict, an unrecoverable
88
+ runtime failure, or an explicit owner pause is present.
89
+
90
+ A host wait primitive (Codex uses `wait_agent`) has non-terminal timeout
91
+ semantics. A `wait_agent` timeout means only that the polling interval elapsed;
92
+ it is not an agent failure, assignment completion, blocked condition, or
93
+ permission to return control to the owner.
94
+
95
+ After a wait timeout:
96
+
97
+ 1. refresh the assignment and delegated-agent status;
98
+ 2. if the assignment is still active, immediately issue another wait in the
99
+ same Orchestrator turn;
100
+ 3. if the assignment completed, collect its report and continue the normal
101
+ verification/review transition;
102
+ 4. if it failed or disappeared, classify the runtime failure and apply policy.
103
+
104
+ A progress update is informational and non-pausing. Emit it only as a preamble
105
+ to the next wait or transition; never finish the owner-facing turn after that
106
+ update while work remains active.
107
+
108
+ The same rule applies to Developer and Reviewer. After Developer completion,
109
+ collect the report and invoke verification without owner input. After Reviewer
110
+ completion, record `approved`, `rework`, or `blocked`; on `approved`, commit,
111
+ promote dependency-satisfied cards, and assign the next card immediately.
112
+
83
113
  ## Authority and delivery
84
114
 
85
115
  Resolve ordinary Developer–Reviewer disagreement from evidence and record the
@@ -0,0 +1,98 @@
1
+ import assert from "node:assert/strict";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+
6
+ const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
7
+ const sharedContract = fs.readFileSync(
8
+ path.join(root, "skills/triad-loop-orchestrator/SKILL.md"),
9
+ "utf8",
10
+ );
11
+ const codexContract = fs.readFileSync(
12
+ path.join(root, "adapters/codex/prompts/triad.md"),
13
+ "utf8",
14
+ );
15
+
16
+ const normalized = (value) => value.replace(/\s+/g, " ").trim().toLowerCase();
17
+ const shared = normalized(sharedContract);
18
+ const codex = normalized(codexContract);
19
+
20
+ function requireContract(text, phrase, label) {
21
+ assert.ok(
22
+ text.includes(normalized(phrase)),
23
+ `${label} must contain: ${phrase}`,
24
+ );
25
+ }
26
+
27
+ for (const [label, contract] of [
28
+ ["shared Orchestrator skill", shared],
29
+ ["Codex Orchestrator prompt", codex],
30
+ ]) {
31
+ requireContract(contract, "MUST NOT end its owner-facing turn while", label);
32
+ requireContract(contract, "delegated Developer or Reviewer assignment", label);
33
+ requireContract(contract, "`wait_agent` timeout", label);
34
+ requireContract(contract, "polling interval", label);
35
+ requireContract(contract, "refresh the assignment and delegated-agent status", label);
36
+ requireContract(contract, "still active", label);
37
+ requireContract(contract, "same Orchestrator turn", label);
38
+ requireContract(contract, "progress update is informational and non-pausing", label);
39
+ requireContract(contract, "without owner input", label);
40
+ requireContract(contract, "dependency-satisfied cards", label);
41
+ }
42
+
43
+ requireContract(
44
+ shared,
45
+ "declared escalation, a `blocked` verdict, an unrecoverable runtime failure, or an explicit owner pause",
46
+ "shared Orchestrator skill",
47
+ );
48
+ requireContract(
49
+ codex,
50
+ "immediately call `wait_agent` again in the same Orchestrator turn",
51
+ "Codex Orchestrator prompt",
52
+ );
53
+ requireContract(codex, "invoke explicit `triad-verify`", "Codex Orchestrator prompt");
54
+
55
+ // A small executable model of the required host-wait semantics. This is not a
56
+ // replacement for the live Codex gate; it prevents the contract from drifting
57
+ // back to treating a polling timeout as terminal.
58
+ function collectAfterTimeout(waitResults, statusResults) {
59
+ const events = [];
60
+ let waitIndex = 0;
61
+ let statusIndex = 0;
62
+
63
+ while (true) {
64
+ const result = waitResults[waitIndex++];
65
+ assert.ok(result, "fixture must provide a wait result");
66
+
67
+ if (result === "completed") {
68
+ events.push("collect");
69
+ return { events, ownerWait: false, nextAction: "collect" };
70
+ }
71
+
72
+ assert.equal(result, "timeout", "fixture only models timeout/completion");
73
+ events.push("timeout", "refresh");
74
+ const status = statusResults[statusIndex++];
75
+ assert.ok(status, "fixture must provide a refreshed status");
76
+
77
+ if (status === "active") {
78
+ events.push("rewait");
79
+ continue;
80
+ }
81
+ if (status === "completed") {
82
+ events.push("collect");
83
+ return { events, ownerWait: false, nextAction: "collect" };
84
+ }
85
+ assert.fail(`unexpected fixture status: ${status}`);
86
+ }
87
+ }
88
+
89
+ for (const role of ["Developer", "Reviewer"]) {
90
+ const result = collectAfterTimeout(["timeout", "completed"], ["active"]);
91
+ assert.deepEqual(result.events, ["timeout", "refresh", "rewait", "collect"]);
92
+ assert.equal(result.ownerWait, false);
93
+ assert.equal(result.nextAction, "collect");
94
+ // Keep the two role paths explicit in the test diagnostics.
95
+ assert.ok(["Developer", "Reviewer"].includes(role));
96
+ }
97
+
98
+ console.log("Codex liveness contract and timeout fixture: PASS");