oh-my-opencode 4.16.3 → 4.17.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/.agents/command/omomomo.md +1 -1
- package/.agents/skills/hyperplan/SKILL.md +3 -3
- package/.agents/skills/omomomo/SKILL.md +1 -1
- package/.opencode/command/omomomo.md +1 -1
- package/.opencode/skills/hyperplan/SKILL.md +3 -3
- package/.opencode/skills/pre-publish-review/SKILL.md +3 -0
- package/README.ja.md +4 -4
- package/README.ko.md +4 -4
- package/README.md +2 -2
- package/README.ru.md +4 -4
- package/README.zh-cn.md +4 -4
- package/dist/cli/index.js +179 -63
- package/dist/cli-node/index.js +179 -63
- package/dist/features/team-mode/test-support/async-test-helpers.d.ts +2 -2
- package/dist/index.js +121 -58
- package/dist/plugin/chat-message/types.d.ts +4 -0
- package/dist/plugin/command-execute-before.d.ts +1 -0
- package/dist/plugin/stop-continuation.d.ts +17 -0
- package/dist/skills/remove-ai-slops/SKILL.md +2 -2
- package/dist/skills/start-work/SKILL.md +5 -2
- package/dist/skills/ulw-plan/scripts/scaffold-plan.mjs +2 -2
- package/dist/tui.js +71 -29
- package/package.json +15 -14
- package/packages/git-bash-mcp/package.json +27 -0
- package/packages/omo-codex/plugin/.codex-plugin/plugin.json +3 -1
- package/packages/omo-codex/plugin/components/bootstrap/dist/cli.js +84 -19
- package/packages/omo-codex/plugin/components/bootstrap/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/bootstrap/package.json +1 -1
- package/packages/omo-codex/plugin/components/codegraph/package.json +1 -1
- package/packages/omo-codex/plugin/components/comment-checker/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/comment-checker/package.json +1 -1
- package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +2 -2
- package/packages/omo-codex/plugin/components/git-bash/package.json +1 -1
- package/packages/omo-codex/plugin/components/lazycodex-executor-verify/AGENTS.md +2 -2
- package/packages/omo-codex/plugin/components/lazycodex-executor-verify/dist/cli.js +6 -2
- package/packages/omo-codex/plugin/components/lazycodex-executor-verify/dist/codex-hook.js +6 -2
- package/packages/omo-codex/plugin/components/lazycodex-executor-verify/hooks/hooks.json +2 -2
- package/packages/omo-codex/plugin/components/lazycodex-executor-verify/package.json +1 -1
- package/packages/omo-codex/plugin/components/lazycodex-executor-verify/src/codex-hook.ts +6 -2
- package/packages/omo-codex/plugin/components/lazycodex-executor-verify/test/cli.test.ts +1 -1
- package/packages/omo-codex/plugin/components/lazycodex-executor-verify/test/codex-hook.test.ts +67 -2
- package/packages/omo-codex/plugin/components/lsp/hooks/hooks.json +2 -2
- package/packages/omo-codex/plugin/components/lsp/package.json +1 -1
- package/packages/omo-codex/plugin/components/lsp/test/package-smoke.test.ts +0 -13
- package/packages/omo-codex/plugin/components/rules/bundled-rules/hephaestus/gpt-5.6.md +1 -1
- package/packages/omo-codex/plugin/components/rules/hooks/hooks.json +4 -4
- package/packages/omo-codex/plugin/components/rules/package.json +1 -1
- package/packages/omo-codex/plugin/components/start-work-continuation/hooks/hooks.json +2 -2
- package/packages/omo-codex/plugin/components/start-work-continuation/package.json +1 -1
- package/packages/omo-codex/plugin/components/start-work-continuation/test/codex-hook.test.ts +1 -79
- package/packages/omo-codex/plugin/components/teammode/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/teammode/package.json +1 -1
- package/packages/omo-codex/plugin/components/telemetry/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/telemetry/package.json +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/agents/explorer.toml +2 -2
- package/packages/omo-codex/plugin/components/ultrawork/agents/lazycodex-code-reviewer.toml +2 -2
- package/packages/omo-codex/plugin/components/ultrawork/agents/lazycodex-gate-reviewer.toml +6 -6
- package/packages/omo-codex/plugin/components/ultrawork/agents/lazycodex-qa-executor.toml +5 -5
- package/packages/omo-codex/plugin/components/ultrawork/agents/lazycodex-worker-high.toml +26 -0
- package/packages/omo-codex/plugin/components/ultrawork/agents/{lazycodex-executor.toml → lazycodex-worker-low.toml} +6 -4
- package/packages/omo-codex/plugin/components/ultrawork/agents/lazycodex-worker-medium.toml +26 -0
- package/packages/omo-codex/plugin/components/ultrawork/agents/librarian.toml +2 -2
- package/packages/omo-codex/plugin/components/ultrawork/agents/plan.toml +4 -4
- package/packages/omo-codex/plugin/components/ultrawork/directive.md +55 -28
- package/packages/omo-codex/plugin/components/ultrawork/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/package.json +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/skills/ultrawork/SKILL.md +55 -28
- package/packages/omo-codex/plugin/components/ultrawork/skills/ulw-plan/scripts/scaffold-plan.mjs +2 -2
- package/packages/omo-codex/plugin/components/ultrawork/test/codex-hook.test.ts +22 -0
- package/packages/omo-codex/plugin/components/ultrawork/test/package-smoke.test.ts +0 -68
- package/packages/omo-codex/plugin/components/ulw-loop/AGENTS.md +1 -1
- package/packages/omo-codex/plugin/components/ulw-loop/CHANGELOG.md +2 -0
- package/packages/omo-codex/plugin/components/ulw-loop/README.md +3 -1
- package/packages/omo-codex/plugin/components/ulw-loop/directive.md +55 -28
- package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint.js +6 -1
- package/packages/omo-codex/plugin/components/ulw-loop/dist/cli-subcommands.d.ts +1 -1
- package/packages/omo-codex/plugin/components/ulw-loop/dist/cli-subcommands.js +13 -2
- package/packages/omo-codex/plugin/components/ulw-loop/dist/cli.js +405 -25
- package/packages/omo-codex/plugin/components/ulw-loop/dist/codex-goal-instruction.js +12 -1
- package/packages/omo-codex/plugin/components/ulw-loop/dist/domain-types.d.ts +4 -2
- package/packages/omo-codex/plugin/components/ulw-loop/dist/paths.d.ts +7 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/paths.js +16 -1
- package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-crud.js +1 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/quality-gate-verdicts.d.ts +6 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/quality-gate-verdicts.js +20 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/quality-gate.d.ts +1 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/quality-gate.js +12 -9
- package/packages/omo-codex/plugin/components/ulw-loop/dist/spawn-guard.d.ts +3 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/spawn-guard.js +148 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/stop-resume-hook.d.ts +2 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/stop-resume-hook.js +209 -0
- package/packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json +25 -2
- package/packages/omo-codex/plugin/components/ulw-loop/package.json +1 -1
- package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/SKILL.md +14 -15
- package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/references/full-workflow.md +29 -27
- package/packages/omo-codex/plugin/components/ulw-loop/src/checkpoint.ts +6 -1
- package/packages/omo-codex/plugin/components/ulw-loop/src/cli-subcommands.ts +14 -3
- package/packages/omo-codex/plugin/components/ulw-loop/src/cli.ts +10 -0
- package/packages/omo-codex/plugin/components/ulw-loop/src/codex-goal-instruction.ts +12 -1
- package/packages/omo-codex/plugin/components/ulw-loop/src/domain-types.ts +4 -2
- package/packages/omo-codex/plugin/components/ulw-loop/src/paths.ts +27 -1
- package/packages/omo-codex/plugin/components/ulw-loop/src/plan-crud.ts +1 -0
- package/packages/omo-codex/plugin/components/ulw-loop/src/quality-gate-verdicts.ts +23 -0
- package/packages/omo-codex/plugin/components/ulw-loop/src/quality-gate.ts +16 -9
- package/packages/omo-codex/plugin/components/ulw-loop/src/spawn-guard.ts +138 -0
- package/packages/omo-codex/plugin/components/ulw-loop/src/stop-resume-hook.ts +208 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/cli-create-goals.test.ts +12 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/cli-entrypoint.test.ts +4 -1
- package/packages/omo-codex/plugin/components/ulw-loop/test/codex-goal-instruction.test.ts +8 -35
- package/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/quality-gate-builder.ts +24 -13
- package/packages/omo-codex/plugin/components/ulw-loop/test/package-smoke.test.ts +5 -2
- package/packages/omo-codex/plugin/components/ulw-loop/test/paths.test.ts +43 -8
- package/packages/omo-codex/plugin/components/ulw-loop/test/quality-gate.test.ts +55 -2
- package/packages/omo-codex/plugin/components/ulw-loop/test/spawn-guard.test.ts +228 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/stop-resume-hook.test.ts +193 -0
- package/packages/omo-codex/plugin/hooks/post-compact-resetting-git-bash-mcp-reminder.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-compact-resetting-lsp-diagnostics-cache.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-compact-resetting-project-rule-cache.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-codegraph-init-guidance.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-comments.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-lsp-diagnostics.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-thread-title-hygiene.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-matching-project-rules.json +1 -1
- package/packages/omo-codex/plugin/hooks/pre-tool-use-enforcing-unlimited-goal-budget.json +1 -1
- package/packages/omo-codex/plugin/hooks/pre-tool-use-guarding-ulw-loop-spawns.json +18 -0
- package/packages/omo-codex/plugin/hooks/pre-tool-use-recommending-git-bash-mcp.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-checking-auto-update.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-checking-bootstrap-provisioning.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-checking-codegraph-bootstrap.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-loading-project-rules.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-recording-session-telemetry.json +1 -1
- package/packages/omo-codex/plugin/hooks/stop-checking-start-work-continuation.json +1 -1
- package/packages/omo-codex/plugin/hooks/stop-checking-ulw-loop-resume.json +17 -0
- package/packages/omo-codex/plugin/hooks/subagent-stop-checking-start-work-continuation.json +1 -1
- package/packages/omo-codex/plugin/hooks/subagent-stop-verifying-lazycodex-executor-evidence.json +2 -2
- package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ultrawork-trigger.json +1 -1
- package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ulw-loop-steering.json +1 -1
- package/packages/omo-codex/plugin/hooks/user-prompt-submit-loading-project-rules.json +1 -1
- package/packages/omo-codex/plugin/model-catalog.json +16 -7
- package/packages/omo-codex/plugin/package-lock.json +13 -13
- package/packages/omo-codex/plugin/package.json +1 -1
- package/packages/omo-codex/plugin/scripts/migrate-codex-config/catalog.mjs +16 -7
- package/packages/omo-codex/plugin/scripts/sync-skills.mjs +8 -2
- package/packages/omo-codex/plugin/skills/init-deep/SKILL.md +2 -2
- package/packages/omo-codex/plugin/skills/refactor/SKILL.md +2 -2
- package/packages/omo-codex/plugin/skills/remove-ai-slops/SKILL.md +4 -4
- package/packages/omo-codex/plugin/skills/review-work/SKILL.md +8 -2
- package/packages/omo-codex/plugin/skills/start-work/SKILL.md +5 -2
- package/packages/omo-codex/plugin/skills/ultrawork/SKILL.md +55 -28
- package/packages/omo-codex/plugin/skills/ulw-loop/SKILL.md +14 -15
- package/packages/omo-codex/plugin/skills/ulw-loop/references/full-workflow.md +29 -27
- package/packages/omo-codex/plugin/skills/ulw-plan/scripts/scaffold-plan.mjs +2 -2
- package/packages/omo-codex/plugin/skills/ulw-research/SKILL.md +2 -2
- package/packages/omo-codex/plugin/skills/visual-qa/SKILL.md +2 -2
- package/packages/omo-codex/plugin/test/aggregate-agents.test.mjs +76 -16
- package/packages/omo-codex/plugin/test/aggregate-hooks.test.mjs +23 -2
- package/packages/omo-codex/plugin/test/aggregate-manifest.test.mjs +1 -1
- package/packages/omo-codex/plugin/test/aggregate-model-catalog.test.mjs +4 -4
- package/packages/omo-codex/plugin/test/auto-update.test.mjs +4 -4
- package/packages/omo-codex/plugin/test/component-hook-contract-cases.mjs +2 -2
- package/packages/omo-codex/plugin/test/lcx-bug-skills.test.mjs +4 -101
- package/packages/omo-codex/plugin/test/migrate-codex-config.test.mjs +14 -14
- package/packages/omo-codex/plugin/test/sync-skills-orchestration.test.mjs +11 -0
- package/packages/omo-codex/plugin/test/sync-skills-test-support.mjs +1 -1
- package/packages/omo-codex/plugin/test/sync-skills.test.mjs +4 -2
- package/packages/omo-codex/plugin/test/ulw-plan-skill-contract.test.mjs +9 -40
- package/packages/omo-codex/plugin/test/ulw-research-skill-contract.test.mjs +4 -277
- package/packages/omo-codex/scripts/install-config-reasoning.test.mjs +5 -5
- package/packages/omo-codex/scripts/install-config.test.mjs +22 -3
- package/packages/omo-codex/scripts/install-dist/install-local.mjs +91 -21
- package/packages/shared-skills/skills/remove-ai-slops/SKILL.md +2 -2
- package/packages/shared-skills/skills/start-work/SKILL.md +5 -2
- package/packages/shared-skills/skills/ulw-plan/scripts/scaffold-plan.mjs +2 -2
- package/dist/skills/visual-qa/scripts/skill-prompt-contract.test.ts +0 -296
- package/packages/omo-codex/plugin/components/ulw-loop/test/skill-contract.test.ts +0 -70
- package/packages/omo-codex/plugin/test/ulw-research-epistemic-contract.test.mjs +0 -98
- package/packages/shared-skills/skills/visual-qa/scripts/skill-prompt-contract.test.ts +0 -296
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { literal, invalid, textField } from "./quality-gate-fields.js";
|
|
2
|
+
export function passedVerdict(value, field) {
|
|
3
|
+
if (value === "not_applicable")
|
|
4
|
+
invalid(`${field} must not be not_applicable.`, field);
|
|
5
|
+
return literal(value, "passed", field);
|
|
6
|
+
}
|
|
7
|
+
export function codeQualityStatusField(value, field) {
|
|
8
|
+
if (value === "CLEAR" || value === "WATCH")
|
|
9
|
+
return value;
|
|
10
|
+
invalid(`${field} must be CLEAR or WATCH.`, field);
|
|
11
|
+
}
|
|
12
|
+
export function adversarialVerdict(row, field) {
|
|
13
|
+
const value = row["verdict"];
|
|
14
|
+
if (value === "passed")
|
|
15
|
+
return { verdict: "passed" };
|
|
16
|
+
if (value === "not_applicable") {
|
|
17
|
+
return { verdict: "not_applicable", reason: textField(row["reason"], `${field}.reason`) };
|
|
18
|
+
}
|
|
19
|
+
invalid(`${field} must be passed or not_applicable with a reason.`, field);
|
|
20
|
+
}
|
|
@@ -9,5 +9,6 @@ export interface QualityGateFs {
|
|
|
9
9
|
export interface ValidateQualityGateOptions {
|
|
10
10
|
readonly repoRoot: string;
|
|
11
11
|
readonly fs: QualityGateFs;
|
|
12
|
+
readonly currentAttemptDir?: string;
|
|
12
13
|
}
|
|
13
14
|
export declare function validateQualityGate(input: unknown, opts?: ValidateQualityGateOptions): UlwLoopQualityGate;
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { resolve } from "node:path";
|
|
2
|
+
import { isWithinAttemptDir } from "./paths.js";
|
|
2
3
|
import { emptyBlockers, invalid, literal, numberField, section, stringArray, textField, } from "./quality-gate-fields.js";
|
|
4
|
+
import { adversarialVerdict, codeQualityStatusField, passedVerdict } from "./quality-gate-verdicts.js";
|
|
3
5
|
const REVIEWER_ROLES = {
|
|
4
6
|
codeReview: "lazycodex-code-reviewer",
|
|
5
7
|
manualQa: "lazycodex-qa-executor",
|
|
@@ -32,11 +34,6 @@ function kindField(value, field) {
|
|
|
32
34
|
return value;
|
|
33
35
|
invalid(`${field} must be a supported artifact kind.`, field);
|
|
34
36
|
}
|
|
35
|
-
function passedVerdict(value, field) {
|
|
36
|
-
if (value === "not_applicable")
|
|
37
|
-
invalid(`${field} must not be not_applicable.`, field);
|
|
38
|
-
return literal(value, "passed", field);
|
|
39
|
-
}
|
|
40
37
|
function artifactCompatible(surface, kind) {
|
|
41
38
|
switch (surface) {
|
|
42
39
|
case "cli":
|
|
@@ -59,9 +56,13 @@ function checkFile(path, field, opts) {
|
|
|
59
56
|
const absolute = resolve(opts.repoRoot, path);
|
|
60
57
|
if (!opts.fs.existsSync(absolute))
|
|
61
58
|
invalid(`${field} must point to an existing artifact.`, field);
|
|
62
|
-
|
|
63
|
-
if (stat.size <= 0)
|
|
59
|
+
if (opts.fs.statSync(absolute).size <= 0)
|
|
64
60
|
invalid(`${field} must point to a non-empty artifact.`, field);
|
|
61
|
+
if (opts.currentAttemptDir !== undefined) {
|
|
62
|
+
const attemptRoot = resolve(opts.repoRoot, opts.currentAttemptDir);
|
|
63
|
+
if (!isWithinAttemptDir(absolute, attemptRoot))
|
|
64
|
+
invalid(`${field} (${path}) must point to an artifact from the current attempt (${opts.currentAttemptDir}).`, field);
|
|
65
|
+
}
|
|
65
66
|
}
|
|
66
67
|
function artifactMap(refs) {
|
|
67
68
|
const byId = new Map();
|
|
@@ -118,7 +119,7 @@ export function validateQualityGate(input, opts) {
|
|
|
118
119
|
codeReview: {
|
|
119
120
|
by: reviewerRoleField(codeReview["by"], REVIEWER_ROLES.codeReview, "codeReview.by"),
|
|
120
121
|
recommendation: literal(codeReview["recommendation"], "APPROVE", "codeReview.recommendation"),
|
|
121
|
-
codeQualityStatus:
|
|
122
|
+
codeQualityStatus: codeQualityStatusField(codeReview["codeQualityStatus"], "codeReview.codeQualityStatus"),
|
|
122
123
|
reportPath: codeReportPath,
|
|
123
124
|
evidence: textField(codeReview["evidence"], "codeReview.evidence"),
|
|
124
125
|
blockers: emptyBlockers(codeReview["blockers"], "codeReview.blockers"),
|
|
@@ -182,12 +183,14 @@ function parseAdversarialCases(value, byId) {
|
|
|
182
183
|
return value.map((item, index) => {
|
|
183
184
|
const row = section(item, `manualQa.adversarialCases[${index}]`);
|
|
184
185
|
const artifacts = referencedArtifacts(row["artifactRefs"], `manualQa.adversarialCases[${index}].artifactRefs`, byId);
|
|
186
|
+
const verdictInfo = adversarialVerdict(row, `manualQa.adversarialCases[${index}]`);
|
|
185
187
|
return {
|
|
186
188
|
id: textField(row["id"], `manualQa.adversarialCases[${index}].id`),
|
|
187
189
|
criterionRef: textField(row["criterionRef"], `manualQa.adversarialCases[${index}].criterionRef`),
|
|
188
190
|
scenario: textField(row["scenario"], `manualQa.adversarialCases[${index}].scenario`),
|
|
189
191
|
expectedBehavior: textField(row["expectedBehavior"], `manualQa.adversarialCases[${index}].expectedBehavior`),
|
|
190
|
-
verdict:
|
|
192
|
+
verdict: verdictInfo.verdict,
|
|
193
|
+
...(verdictInfo.reason === undefined ? {} : { reason: verdictInfo.reason }),
|
|
191
194
|
artifactRefs: artifacts.map((artifact) => artifact.id),
|
|
192
195
|
};
|
|
193
196
|
});
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { parsePreToolUsePayload } from "./codex-hook.js";
|
|
4
|
+
import { isFinalRunCompletionCandidate } from "./goal-status.js";
|
|
5
|
+
import { ulwLoopAttemptEvidenceDir, ulwLoopDir } from "./paths.js";
|
|
6
|
+
// spawn_agent = v1; collaborationspawn_agent = the delimiter-free flattened v2
|
|
7
|
+
// hook token from codex-rs hook_names.rs; collaboration.spawn_agent = the
|
|
8
|
+
// dotted token observed live in the task-1 probe (hook-tool-tokens.txt).
|
|
9
|
+
const SPAWN_TOOL_TOKENS = new Set(["spawn_agent", "collaborationspawn_agent", "collaboration.spawn_agent"]);
|
|
10
|
+
const DEFAULT_FANOUT_LIMIT = 60;
|
|
11
|
+
const GATE_MESSAGE_PATTERN = /lazycodex-gate-reviewer|final gate review/i;
|
|
12
|
+
export function applySpawnGuards(payload) {
|
|
13
|
+
if (payload.hook_event_name !== "PreToolUse" || !SPAWN_TOOL_TOKENS.has(payload.tool_name))
|
|
14
|
+
return "";
|
|
15
|
+
const stateDir = ulwLoopDir(payload.cwd, { sessionId: payload.session_id });
|
|
16
|
+
const plan = readPlan(join(stateDir, "goals.json"));
|
|
17
|
+
if (plan === null)
|
|
18
|
+
return "";
|
|
19
|
+
const fanOutDenial = consumeFanOutBudget(stateDir);
|
|
20
|
+
if (fanOutDenial !== null)
|
|
21
|
+
return deny(fanOutDenial);
|
|
22
|
+
const missingArtifact = missingGateArtifact(payload, plan);
|
|
23
|
+
if (missingArtifact !== null)
|
|
24
|
+
return deny(`spawn code-review + QA first; gate audits their artifacts: missing ${missingArtifact}`);
|
|
25
|
+
return "";
|
|
26
|
+
}
|
|
27
|
+
export async function runSpawnGuardCli(stdin, stdout) {
|
|
28
|
+
try {
|
|
29
|
+
const chunks = [];
|
|
30
|
+
for await (const chunk of stdin)
|
|
31
|
+
chunks.push(Buffer.from(chunk));
|
|
32
|
+
const payload = parsePreToolUsePayload(Buffer.concat(chunks).toString("utf8"));
|
|
33
|
+
if (payload === null)
|
|
34
|
+
return;
|
|
35
|
+
const output = applySpawnGuards(payload);
|
|
36
|
+
if (output.length > 0)
|
|
37
|
+
stdout.write(output);
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
if (error instanceof Error)
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
// Per-session spawn counter; depth/lineage tracking is descoped — this is a
|
|
45
|
+
// total-volume backstop against fan-out explosions, not a recursion tracker.
|
|
46
|
+
function consumeFanOutBudget(stateDir) {
|
|
47
|
+
const counterPath = join(stateDir, "spawn-count.json");
|
|
48
|
+
const count = readCount(counterPath) + 1;
|
|
49
|
+
writeFileSync(counterPath, JSON.stringify({ count }));
|
|
50
|
+
const limit = fanOutLimit();
|
|
51
|
+
if (count <= limit)
|
|
52
|
+
return null;
|
|
53
|
+
return `ulw-loop spawn fan-out cap reached (${count}/${limit}). Consolidate work into the agents already running, or raise OMO_SPAWN_FANOUT_LIMIT if this volume is intentional.`;
|
|
54
|
+
}
|
|
55
|
+
function missingGateArtifact(payload, plan) {
|
|
56
|
+
if (!isGateReviewerSpawn(payload.tool_input))
|
|
57
|
+
return null;
|
|
58
|
+
const goal = plan.goals.find((candidate) => isFinalRunCompletionCandidate(plan, candidate));
|
|
59
|
+
if (goal === undefined || goal.status === "complete")
|
|
60
|
+
return null;
|
|
61
|
+
if (!goal.successCriteria.every((criterion) => criterion.status === "pass"))
|
|
62
|
+
return null;
|
|
63
|
+
const scope = { sessionId: payload.session_id };
|
|
64
|
+
if (plan.evidenceLayoutVersion === 2) {
|
|
65
|
+
const attemptDir = ulwLoopAttemptEvidenceDir(goal.id, goal.attempt, scope);
|
|
66
|
+
for (const name of [`${goal.id}-code-review.md`, `${goal.id}-manual-qa.md`]) {
|
|
67
|
+
const relative = `${attemptDir}/${name}`;
|
|
68
|
+
if (!isNonEmptyFile(join(payload.cwd, relative)))
|
|
69
|
+
return relative;
|
|
70
|
+
}
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
const flatReport = `.omo/evidence/${goal.id}-code-review.md`;
|
|
74
|
+
if (!isNonEmptyFile(join(payload.cwd, flatReport)))
|
|
75
|
+
return flatReport;
|
|
76
|
+
// v1 manual-QA approximation: any other non-empty evidence file counts.
|
|
77
|
+
if (!hasOtherEvidenceFile(join(payload.cwd, ".omo", "evidence"), `${goal.id}-code-review.md`))
|
|
78
|
+
return `.omo/evidence/<any manual-QA artifact besides ${goal.id}-code-review.md>`;
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
function isGateReviewerSpawn(toolInput) {
|
|
82
|
+
if (typeof toolInput !== "object" || toolInput === null)
|
|
83
|
+
return false;
|
|
84
|
+
const record = toolInput;
|
|
85
|
+
const agentType = record["agent_type"];
|
|
86
|
+
if (typeof agentType === "string")
|
|
87
|
+
return agentType === "lazycodex-gate-reviewer";
|
|
88
|
+
const message = record["message"];
|
|
89
|
+
return typeof message === "string" && GATE_MESSAGE_PATTERN.test(message);
|
|
90
|
+
}
|
|
91
|
+
function deny(reason) {
|
|
92
|
+
return `${JSON.stringify({
|
|
93
|
+
hookSpecificOutput: {
|
|
94
|
+
hookEventName: "PreToolUse",
|
|
95
|
+
permissionDecision: "deny",
|
|
96
|
+
permissionDecisionReason: reason,
|
|
97
|
+
additionalContext: reason,
|
|
98
|
+
},
|
|
99
|
+
})}\n`;
|
|
100
|
+
}
|
|
101
|
+
function fanOutLimit() {
|
|
102
|
+
const raw = process.env["OMO_SPAWN_FANOUT_LIMIT"];
|
|
103
|
+
if (raw === undefined)
|
|
104
|
+
return DEFAULT_FANOUT_LIMIT;
|
|
105
|
+
const parsed = Number.parseInt(raw, 10);
|
|
106
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_FANOUT_LIMIT;
|
|
107
|
+
}
|
|
108
|
+
function isNonEmptyFile(path) {
|
|
109
|
+
try {
|
|
110
|
+
return existsSync(path) && statSync(path).size > 0;
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
if (error instanceof Error)
|
|
114
|
+
return false;
|
|
115
|
+
throw error;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
function hasOtherEvidenceFile(evidenceDir, excludedName) {
|
|
119
|
+
try {
|
|
120
|
+
return readdirSync(evidenceDir).some((name) => name !== excludedName && isNonEmptyFile(join(evidenceDir, name)));
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
if (error instanceof Error)
|
|
124
|
+
return false;
|
|
125
|
+
throw error;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
function readCount(counterPath) {
|
|
129
|
+
try {
|
|
130
|
+
const parsed = JSON.parse(readFileSync(counterPath, "utf8"));
|
|
131
|
+
return typeof parsed["count"] === "number" && parsed["count"] >= 0 ? parsed["count"] : 0;
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
if (error instanceof Error)
|
|
135
|
+
return 0;
|
|
136
|
+
throw error;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
function readPlan(goalsPath) {
|
|
140
|
+
try {
|
|
141
|
+
return JSON.parse(readFileSync(goalsPath, "utf8"));
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
if (error instanceof Error)
|
|
145
|
+
return null;
|
|
146
|
+
throw error;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { isAbsolute, join } from "node:path";
|
|
3
|
+
import { normalizeUlwLoopSessionId, ulwLoopDir } from "./paths.js";
|
|
4
|
+
// Turn-death recovery only: Codex emits Stop when a turn ends, so a run that
|
|
5
|
+
// dies mid-turn (crash, kill, context blowup before any Stop) never reaches
|
|
6
|
+
// this handler. Mid-turn stalls are out of scope by design.
|
|
7
|
+
const RESUME_CAP = 2;
|
|
8
|
+
// Mirrors start-work-continuation's context-pressure bail-out: injecting a
|
|
9
|
+
// resume directive into an already-overflowing context makes things worse.
|
|
10
|
+
const CONTEXT_PRESSURE_MARKERS = [
|
|
11
|
+
"context compacted",
|
|
12
|
+
"context_length_exceeded",
|
|
13
|
+
"skill descriptions were shortened",
|
|
14
|
+
"context_too_large",
|
|
15
|
+
"codex ran out of room in the model's context window",
|
|
16
|
+
"your input exceeds the context window",
|
|
17
|
+
"long threads and multiple compactions",
|
|
18
|
+
];
|
|
19
|
+
export function runStopResumeHook(input) {
|
|
20
|
+
const payload = parseStopPayload(input);
|
|
21
|
+
if (payload === null || payload.stop_hook_active)
|
|
22
|
+
return "";
|
|
23
|
+
if (transcriptShowsContextPressure(payload.transcript_path))
|
|
24
|
+
return "";
|
|
25
|
+
if (boulderContinuationWillFire(payload.cwd, payload.session_id))
|
|
26
|
+
return "";
|
|
27
|
+
const stateDir = ulwLoopDir(payload.cwd, { sessionId: payload.session_id });
|
|
28
|
+
const plan = readPlan(join(stateDir, "goals.json"));
|
|
29
|
+
if (plan === null || plan.aggregateCompletion?.status === "complete")
|
|
30
|
+
return "";
|
|
31
|
+
const goal = resumableGoal(plan);
|
|
32
|
+
if (goal === undefined)
|
|
33
|
+
return "";
|
|
34
|
+
if (!consumeResumeBudget(stateDir, goal.id))
|
|
35
|
+
return "";
|
|
36
|
+
const output = {
|
|
37
|
+
decision: "block",
|
|
38
|
+
reason: renderResumeDirective(plan, goal, payload.session_id),
|
|
39
|
+
};
|
|
40
|
+
return JSON.stringify(output);
|
|
41
|
+
}
|
|
42
|
+
export async function runStopResumeHookCli(stdin, stdout) {
|
|
43
|
+
try {
|
|
44
|
+
const chunks = [];
|
|
45
|
+
for await (const chunk of stdin)
|
|
46
|
+
chunks.push(Buffer.from(chunk));
|
|
47
|
+
const output = runStopResumeHook(JSON.parse(Buffer.concat(chunks).toString("utf8")));
|
|
48
|
+
if (output.length > 0)
|
|
49
|
+
stdout.write(output);
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
if (error instanceof Error)
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function resumableGoal(plan) {
|
|
57
|
+
const active = plan.goals.find((goal) => goal.id === plan.activeGoalId);
|
|
58
|
+
if (active !== undefined && isResumableStatus(active.status))
|
|
59
|
+
return active;
|
|
60
|
+
return plan.goals.find((goal) => isResumableStatus(goal.status));
|
|
61
|
+
}
|
|
62
|
+
function isResumableStatus(status) {
|
|
63
|
+
return status === "pending" || status === "in_progress";
|
|
64
|
+
}
|
|
65
|
+
// Two-strike cap keyed on ledger movement: an unchanged ledger.jsonl line
|
|
66
|
+
// count across resumes means the loop is not progressing. The stuck marker is
|
|
67
|
+
// a separate file — a ledger append would change the count and self-reset.
|
|
68
|
+
function consumeResumeBudget(stateDir, goalId) {
|
|
69
|
+
const ledgerLineCount = countLedgerLines(join(stateDir, "ledger.jsonl"));
|
|
70
|
+
const counterPath = join(stateDir, `auto-resume-${goalId}.json`);
|
|
71
|
+
const previous = readCounter(counterPath);
|
|
72
|
+
const count = previous !== null && previous.ledgerLineCount === ledgerLineCount ? previous.count : 0;
|
|
73
|
+
if (count >= RESUME_CAP) {
|
|
74
|
+
writeFileSync(join(stateDir, `auto-resume-${goalId}.stuck`), `no ledger progress after ${count} resumes\n`);
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
writeFileSync(counterPath, JSON.stringify({ count: count + 1, ledgerLineCount }));
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
function renderResumeDirective(plan, goal, sessionId) {
|
|
81
|
+
const normalized = normalizeUlwLoopSessionId(sessionId);
|
|
82
|
+
const option = normalized !== null && plan.goalsPath.includes(`/${normalized}/`) ? ` --session-id ${normalized}` : "";
|
|
83
|
+
return [
|
|
84
|
+
`The ulw-loop run in this session still has unfinished goals (next: ${goal.id} — ${goal.title}).`,
|
|
85
|
+
"The turn ended before the loop completed. Resume it now:",
|
|
86
|
+
`1. Run \`omo ulw-loop status${option} --json\` to reload the plan, the active goal, and currentAttemptDir.`,
|
|
87
|
+
"2. Continue the active goal's remaining success criteria, recording evidence with record-evidence.",
|
|
88
|
+
`3. Checkpoint through \`omo ulw-loop checkpoint${option}\` when the goal's criteria are proven.`,
|
|
89
|
+
"If the loop is genuinely blocked on the user, checkpoint the goal as blocked with the reason instead.",
|
|
90
|
+
].join("\n");
|
|
91
|
+
}
|
|
92
|
+
function readPlan(goalsPath) {
|
|
93
|
+
try {
|
|
94
|
+
return JSON.parse(readFileSync(goalsPath, "utf8"));
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
if (error instanceof Error)
|
|
98
|
+
return null;
|
|
99
|
+
throw error;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function countLedgerLines(ledgerPath) {
|
|
103
|
+
try {
|
|
104
|
+
return readFileSync(ledgerPath, "utf8").split("\n").filter(Boolean).length;
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
if (error instanceof Error)
|
|
108
|
+
return 0;
|
|
109
|
+
throw error;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
function readCounter(counterPath) {
|
|
113
|
+
try {
|
|
114
|
+
if (!existsSync(counterPath))
|
|
115
|
+
return null;
|
|
116
|
+
const parsed = JSON.parse(readFileSync(counterPath, "utf8"));
|
|
117
|
+
if (typeof parsed["count"] !== "number" || typeof parsed["ledgerLineCount"] !== "number")
|
|
118
|
+
return null;
|
|
119
|
+
return { count: parsed["count"], ledgerLineCount: parsed["ledgerLineCount"] };
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
if (error instanceof Error)
|
|
123
|
+
return null;
|
|
124
|
+
throw error;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
// Local ~10-LOC approximation of start-work-continuation's boulder check (no
|
|
128
|
+
// cross-component import allowed): any continuable work for this session means
|
|
129
|
+
// that hook owns the Stop event, so this one stays silent.
|
|
130
|
+
function boulderContinuationWillFire(cwd, sessionId) {
|
|
131
|
+
try {
|
|
132
|
+
const raw = JSON.parse(readFileSync(join(cwd, ".omo", "boulder.json"), "utf8"));
|
|
133
|
+
const works = raw["works"];
|
|
134
|
+
// The flat legacy shape has no works map: the top level is the single work.
|
|
135
|
+
const entries = typeof works === "object" && works !== null ? Object.values(works) : [raw];
|
|
136
|
+
return entries.some((work) => {
|
|
137
|
+
if (typeof work !== "object" || work === null)
|
|
138
|
+
return false;
|
|
139
|
+
const entry = work;
|
|
140
|
+
const sessionIds = Array.isArray(entry["session_ids"]) ? entry["session_ids"] : [];
|
|
141
|
+
const continuable = entry["status"] === "active" || entry["status"] === "paused";
|
|
142
|
+
return continuable && sessionIds.includes(`codex:${sessionId}`) && boulderPlanHasRemainingTask(cwd, entry);
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
catch (error) {
|
|
146
|
+
if (error instanceof Error)
|
|
147
|
+
return false;
|
|
148
|
+
throw error;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
function transcriptShowsContextPressure(transcriptPath) {
|
|
152
|
+
try {
|
|
153
|
+
const transcript = readFileSync(transcriptPath, "utf8").toLowerCase();
|
|
154
|
+
return CONTEXT_PRESSURE_MARKERS.some((marker) => transcript.includes(marker));
|
|
155
|
+
}
|
|
156
|
+
catch (error) {
|
|
157
|
+
if (error instanceof Error)
|
|
158
|
+
return false;
|
|
159
|
+
throw error;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
// start-work-continuation only fires while its plan checklist has remaining
|
|
163
|
+
// items; a boulder work whose plan is exhausted (or unreadable) leaves the
|
|
164
|
+
// Stop event to this hook.
|
|
165
|
+
function boulderPlanHasRemainingTask(cwd, entry) {
|
|
166
|
+
const activePlan = entry["active_plan"];
|
|
167
|
+
if (typeof activePlan !== "string" || activePlan.trim().length === 0)
|
|
168
|
+
return false;
|
|
169
|
+
const planPath = isAbsolute(activePlan) ? activePlan : join(cwd, activePlan);
|
|
170
|
+
const worktree = entry["worktree_path"];
|
|
171
|
+
const candidates = typeof worktree === "string" && worktree.trim().length > 0 && !isAbsolute(activePlan)
|
|
172
|
+
? [join(isAbsolute(worktree) ? worktree : join(cwd, worktree), activePlan), planPath]
|
|
173
|
+
: [planPath];
|
|
174
|
+
for (const candidate of candidates) {
|
|
175
|
+
try {
|
|
176
|
+
return readFileSync(candidate, "utf8")
|
|
177
|
+
.split(/\r?\n/)
|
|
178
|
+
.some((line) => line.startsWith("- [ ] "));
|
|
179
|
+
}
|
|
180
|
+
catch (error) {
|
|
181
|
+
if (!(error instanceof Error))
|
|
182
|
+
throw error;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
function parseStopPayload(value) {
|
|
188
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
189
|
+
return null;
|
|
190
|
+
const record = value;
|
|
191
|
+
const optionalMessage = record["last_assistant_message"];
|
|
192
|
+
const valid = record["hook_event_name"] === "Stop" &&
|
|
193
|
+
typeof record["session_id"] === "string" &&
|
|
194
|
+
typeof record["turn_id"] === "string" &&
|
|
195
|
+
typeof record["transcript_path"] === "string" &&
|
|
196
|
+
typeof record["cwd"] === "string" &&
|
|
197
|
+
typeof record["model"] === "string" &&
|
|
198
|
+
typeof record["permission_mode"] === "string" &&
|
|
199
|
+
typeof record["stop_hook_active"] === "boolean" &&
|
|
200
|
+
(optionalMessage === undefined || typeof optionalMessage === "string");
|
|
201
|
+
if (!valid)
|
|
202
|
+
return null;
|
|
203
|
+
return {
|
|
204
|
+
session_id: record["session_id"],
|
|
205
|
+
cwd: record["cwd"],
|
|
206
|
+
transcript_path: record["transcript_path"],
|
|
207
|
+
stop_hook_active: record["stop_hook_active"],
|
|
208
|
+
};
|
|
209
|
+
}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
"type": "command",
|
|
8
8
|
"command": "node \"${PLUGIN_ROOT}/dist/cli.js\" hook user-prompt-submit --with-ultrawork",
|
|
9
9
|
"timeout": 10,
|
|
10
|
-
"statusMessage": "(OmO 4.
|
|
10
|
+
"statusMessage": "(OmO 4.17.0) Checking Ulw-Loop Steering"
|
|
11
11
|
}
|
|
12
12
|
]
|
|
13
13
|
}
|
|
@@ -20,7 +20,30 @@
|
|
|
20
20
|
"type": "command",
|
|
21
21
|
"command": "node \"${PLUGIN_ROOT}/dist/cli.js\" hook pre-tool-use",
|
|
22
22
|
"timeout": 5,
|
|
23
|
-
"statusMessage": "(OmO 4.
|
|
23
|
+
"statusMessage": "(OmO 4.17.0) Enforcing Unlimited Ulw-Loop Budget"
|
|
24
|
+
}
|
|
25
|
+
]
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"matcher": "^(spawn_agent|collaborationspawn_agent|collaboration\\.spawn_agent)$",
|
|
29
|
+
"hooks": [
|
|
30
|
+
{
|
|
31
|
+
"type": "command",
|
|
32
|
+
"command": "node \"${PLUGIN_ROOT}/dist/cli.js\" hook pre-tool-use-spawn",
|
|
33
|
+
"timeout": 5,
|
|
34
|
+
"statusMessage": "(OmO 4.17.0) Guarding Ulw-Loop Spawns"
|
|
35
|
+
}
|
|
36
|
+
]
|
|
37
|
+
}
|
|
38
|
+
],
|
|
39
|
+
"Stop": [
|
|
40
|
+
{
|
|
41
|
+
"hooks": [
|
|
42
|
+
{
|
|
43
|
+
"type": "command",
|
|
44
|
+
"command": "node \"${PLUGIN_ROOT}/dist/cli.js\" hook stop",
|
|
45
|
+
"timeout": 10,
|
|
46
|
+
"statusMessage": "(OmO 4.17.0) Checking Ulw-Loop Resume"
|
|
24
47
|
}
|
|
25
48
|
]
|
|
26
49
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@code-yeongyu/codex-ulw-loop",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.17.0",
|
|
4
4
|
"description": "Codex plugin: durable repo-native multi-goal orchestration with embedded success criteria and observable evidence audit.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "npm@11.12.1",
|
|
@@ -23,29 +23,28 @@ This skill is intentionally compact. The full workflow lives in `references/full
|
|
|
23
23
|
- After any compaction or context loss, re-read brief + goals + ledger FIRST plus `omo ulw-loop status --json`, then resume; never re-plan from scratch.
|
|
24
24
|
- If `omo ulw-loop create-goals` says the existing aggregate is already complete, start unrelated new work with a fresh `--session-id <new-id>` instead of steering or forcing the completed default state. Use `--force` only to intentionally overwrite completed evidence.
|
|
25
25
|
- Every success criterion needs observable evidence from a real surface: a channel (terminal/TUI via the xterm.js web terminal, HTTP, browser, computer-use) or, for CLI- or data-shaped criteria, an auxiliary surface (CLI stdout, DB diff, parsed config dump).
|
|
26
|
-
-
|
|
26
|
+
- Evidence is bound to its capture commit; a later fix, rebase, or merge makes it stale — re-run at the current HEAD and re-record, never relabel or regenerate. Record only after cleanup receipts exist.
|
|
27
27
|
- Delegate code edits, test writes, fixes, and QA execution to right-sized Codex subagents when the workflow requires it.
|
|
28
|
-
- Every `
|
|
29
|
-
- Plan and reviewer agents may run for a long time; spawn them in the background, keep doing independent root work, and poll with short `
|
|
28
|
+
- Every `spawn_agent` message starts with `TASK:`, then names `DELIVERABLE`, `SCOPE`, and `VERIFY`; put role and specialty instructions inside `message`; use `fork_turns: "none"` (v1: `fork_context: false`) unless full history is truly required.
|
|
29
|
+
- Plan and reviewer agents may run for a long time; spawn them in the background, keep doing independent root work, and poll with short `wait_agent` cycles. Never use a single long blocking wait for them.
|
|
30
30
|
- For work likely to exceed one wait cycle, require the child to send `WORKING: <task> - <current phase>` before long reading, testing, or review passes, and `BLOCKED: <reason>` only when it cannot progress.
|
|
31
|
-
- Track spawned agent names locally. Use `
|
|
31
|
+
- Track spawned agent names locally. Use `wait_agent` for mailbox signals, not proof of completion. A timeout only means no new mailbox update arrived. Treat a running child as alive.
|
|
32
32
|
- While children run, surface the active subagent count, agent names, and latest `WORKING:` phase.
|
|
33
|
-
- Fallback only when the child is completed without the deliverable, ack-only after
|
|
33
|
+
- Fallback only when the child is completed without the deliverable, ack-only after `followup_task`, explicitly `BLOCKED:`, or no longer running. Then record inconclusive and respawn a smaller `fork_turns: "none"` task with the missing deliverable.
|
|
34
34
|
- Use `git-master` for git-tracked edits: inspect recent and touched-path commit history, then commit each verified work unit atomically in the repository's observed language, scope, and message style with only that unit's files staged.
|
|
35
35
|
|
|
36
36
|
## Codex Tool Mapping
|
|
37
37
|
|
|
38
|
-
|
|
38
|
+
Codex exposes ONE subagent surface per session — check your tool list. GPT-5.6 (sol/terra) get the flat MultiAgentV2 tools (primary); GPT-5.5 and gpt-5.6-luna get the namespaced `multi_agent_v1.*` set (fallback row). The workflow's orchestration examples map to:
|
|
39
39
|
|
|
40
|
-
|
|
|
40
|
+
| Intent | MultiAgentV2 (gpt-5.6 sol/terra) |
|
|
41
41
|
| --- | --- |
|
|
42
|
-
|
|
|
43
|
-
|
|
|
44
|
-
|
|
|
45
|
-
|
|
|
46
|
-
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
Flat `spawn_agent` requiring `task_name` instead (`multi_agent_v2`)? Rewrite rows: add `"task_name"`, `"fork_context":false` → `"fork_turns":"none"`, `wait_agent` takes only `timeout_ms`, no `close_agent` — finished agents end on their own.
|
|
42
|
+
| Spawn a worker | `spawn_agent({"task_name":"<lower_snake_id>","message":"TASK: act as <role>. ...","fork_turns":"none"})` — `task_name`+`message` required; `fork_turns:"none"` = no parent history; do NOT set `agent_type`/`model`/`reasoning_effort` |
|
|
43
|
+
| Re-task an idle worker (wakes it) | `followup_task({"target":"<name>","message":"..."})` |
|
|
44
|
+
| Send context without interrupting | `send_message({"target":"<name>","message":"..."})` |
|
|
45
|
+
| Wait for a mailbox signal | `wait_agent({"timeout_ms":<ms>})` — any live worker; a timeout only means no new update |
|
|
46
|
+
| Enumerate / stop a runaway | `list_agents()` / `interrupt_agent({"target":"<name>"})` — no `close_agent`; finished workers end on their own |
|
|
47
|
+
|
|
48
|
+
V1 fallback (gpt-5.5, gpt-5.6-luna): `multi_agent_v1.spawn_agent({...,"fork_context":false})`, `multi_agent_v1.send_input` (re-task), `multi_agent_v1.wait_agent({"targets":[...],"timeout_ms":...})`, `multi_agent_v1.close_agent`.
|
|
50
49
|
|
|
51
50
|
When translating `load_skills=[...]`, include the requested skill names in the spawned agent's `message`.
|