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
|
package/adapters/codex/README.md
CHANGED
|
@@ -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.
|
|
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");
|