lazycodex-ai 5.0.0-beta.48 → 5.0.0-beta.49
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/README.ja.md +35 -27
- package/README.ko.md +35 -27
- package/README.md +19 -34
- package/README.ru.md +33 -28
- package/README.zh-cn.md +31 -27
- package/dist/cli/index.js +19 -15
- package/dist/cli-node/index.js +19 -15
- package/package.json +1 -1
- package/packages/omo-codex/plugin/.codex-plugin/plugin.json +2 -2
- 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/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/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/lazycodex-executor-verify/package.json +1 -1
- package/packages/omo-codex/plugin/components/lsp/dist/.omo-runtime-manifest.json +2 -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/rules/hooks/hooks.json +4 -4
- package/packages/omo-codex/plugin/components/rules/package.json +1 -1
- 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/dist/cli.js +11 -10
- 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/src/directive-content.ts +1 -1
- package/packages/omo-codex/plugin/components/ulw-execute-continuation/AGENTS.md +3 -3
- package/packages/omo-codex/plugin/components/ulw-execute-continuation/README.md +3 -3
- package/packages/omo-codex/plugin/components/ulw-execute-continuation/directive.md +4 -4
- package/packages/omo-codex/plugin/components/ulw-execute-continuation/dist/cli.js +1 -1
- package/packages/omo-codex/plugin/components/ulw-execute-continuation/hooks/hooks.json +1 -13
- package/packages/omo-codex/plugin/components/ulw-execute-continuation/package.json +1 -1
- package/packages/omo-codex/plugin/components/ulw-execute-continuation/src/codex-hook.ts +1 -1
- package/packages/omo-codex/plugin/components/ulw-execute-continuation/src/types.ts +1 -1
- package/packages/omo-codex/plugin/components/ulw-execute-continuation/test/cli.test.ts +3 -7
- package/packages/omo-codex/plugin/components/ulw-execute-continuation/test/codex-hook.test.ts +16 -0
- package/packages/omo-codex/plugin/components/ulw-loop/README.md +2 -2
- package/packages/omo-codex/plugin/components/ulw-loop/directive.md +11 -10
- package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint-template.js +8 -15
- package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint.js +2 -2
- package/packages/omo-codex/plugin/components/ulw-loop/dist/cli-output.d.ts +1 -1
- package/packages/omo-codex/plugin/components/ulw-loop/dist/cli-output.js +1 -1
- package/packages/omo-codex/plugin/components/ulw-loop/dist/cli.js +88 -67
- package/packages/omo-codex/plugin/components/ulw-loop/dist/codex-goal-instruction.js +3 -4
- package/packages/omo-codex/plugin/components/ulw-loop/dist/domain-types.d.ts +1 -1
- package/packages/omo-codex/plugin/components/ulw-loop/dist/quality-gate-fields.js +1 -1
- package/packages/omo-codex/plugin/components/ulw-loop/dist/quality-gate.js +13 -16
- package/packages/omo-codex/plugin/components/ulw-loop/dist/spawn-guard.d.ts +2 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/spawn-guard.js +47 -26
- package/packages/omo-codex/plugin/components/ulw-loop/dist/status-next-actions.js +2 -2
- package/packages/omo-codex/plugin/components/ulw-loop/dist/surface.d.ts +1 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/surface.js +14 -4
- package/packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json +17 -4
- package/packages/omo-codex/plugin/components/ulw-loop/package.json +2 -2
- package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/references/full-workflow.md +1 -1
- package/packages/omo-codex/plugin/components/ulw-loop/src/checkpoint-template.ts +8 -14
- package/packages/omo-codex/plugin/components/ulw-loop/src/checkpoint.ts +2 -2
- package/packages/omo-codex/plugin/components/ulw-loop/src/cli-output.ts +1 -1
- package/packages/omo-codex/plugin/components/ulw-loop/src/cli.ts +5 -1
- package/packages/omo-codex/plugin/components/ulw-loop/src/codex-goal-instruction.ts +3 -4
- package/packages/omo-codex/plugin/components/ulw-loop/src/domain-types.ts +1 -1
- package/packages/omo-codex/plugin/components/ulw-loop/src/quality-gate-fields.ts +4 -1
- package/packages/omo-codex/plugin/components/ulw-loop/src/quality-gate.ts +21 -16
- package/packages/omo-codex/plugin/components/ulw-loop/src/spawn-guard.ts +52 -25
- package/packages/omo-codex/plugin/components/ulw-loop/src/status-next-actions.ts +2 -2
- package/packages/omo-codex/plugin/components/ulw-loop/src/surface.ts +16 -4
- package/packages/omo-codex/plugin/components/ulw-loop/test/checkpoint-final.test.ts +11 -12
- package/packages/omo-codex/plugin/components/ulw-loop/test/quality-gate-fields-messages.test.ts +98 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/quality-gate-lazycodex-surface.test.ts +93 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/quality-gate-roles.test.ts +9 -4
- package/packages/omo-codex/plugin/components/ulw-loop/test/quality-gate.test.ts +10 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/spawn-guard.test.ts +70 -7
- 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-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/post-tool-use-recording-spawn-admission.json +18 -0
- 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 +1 -1
- 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-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-ulw-execute-continuation.json +1 -1
- package/packages/omo-codex/plugin/hooks/stop-checking-ulw-loop-resume.json +1 -1
- package/packages/omo-codex/plugin/hooks/subagent-stop-verifying-lazycodex-executor-evidence.json +1 -1
- 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/package-lock.json +12 -12
- package/packages/omo-codex/plugin/package.json +1 -1
- package/packages/omo-codex/plugin/scripts/sync-skills.mjs +61 -33
- package/packages/omo-codex/plugin/skills/init-deep/SKILL.md +4 -2
- package/packages/omo-codex/plugin/skills/refactor/SKILL.md +4 -2
- package/packages/omo-codex/plugin/skills/remove-ai-slops/SKILL.md +4 -2
- package/packages/omo-codex/plugin/skills/review-work/SKILL.md +13 -3
- package/packages/omo-codex/plugin/skills/ultrawork/SKILL.md +11 -10
- package/packages/omo-codex/plugin/skills/ulw-execute/SKILL.md +11 -6
- package/packages/omo-codex/plugin/skills/ulw-loop/SKILL.md +24 -0
- package/packages/omo-codex/plugin/skills/ulw-loop/references/full-workflow.md +1 -1
- package/packages/omo-codex/plugin/skills/ulw-research/SKILL.md +4 -2
- package/packages/omo-codex/plugin/skills/visual-qa/SKILL.md +4 -2
- package/packages/omo-codex/plugin/test/aggregate-hooks.test.mjs +8 -4
- package/packages/omo-codex/plugin/test/component-hook-contract-cases.mjs +62 -0
- package/packages/omo-codex/plugin/test/hook-status-message.test.mjs +3 -3
- package/packages/omo-codex/plugin/test/sync-skills-codex-compatibility.test.mjs +23 -1
- package/packages/omo-codex/plugin/test/sync-skills-test-support.mjs +36 -16
- package/packages/omo-codex/scripts/install-dist/install-local.mjs +2 -2
- package/packages/prompts-core/prompts/ultrawork/codex.md +11 -10
- package/packages/omo-codex/plugin/hooks/subagent-stop-checking-ulw-execute-continuation.json +0 -17
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { randomBytes } from "node:crypto";
|
|
2
|
-
import { existsSync,
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
4
|
import { parsePreToolUsePayload } from "./codex-hook.js";
|
|
5
5
|
import { isFinalRunCompletionCandidate } from "./goal-status.js";
|
|
@@ -10,7 +10,7 @@ import { GATE_REVIEWER_AGENT_NAMES, REVIEWER_ROLES_BY_SURFACE, resolveToolkitSur
|
|
|
10
10
|
// hook token from codex-rs hook_names.rs; collaboration.spawn_agent = the
|
|
11
11
|
// dotted token observed live in the task-1 probe (hook-tool-tokens.txt).
|
|
12
12
|
const SPAWN_TOOL_TOKENS = new Set(["spawn_agent", "collaborationspawn_agent", "collaboration.spawn_agent"]);
|
|
13
|
-
const DEFAULT_FANOUT_LIMIT =
|
|
13
|
+
export const DEFAULT_FANOUT_LIMIT = 24;
|
|
14
14
|
const DEFAULT_REVIEW_SPAWN_LIMIT = 3;
|
|
15
15
|
const GATE_MESSAGE_PATTERN = /lazycodex-gate-reviewer|omo-senpi-gate-reviewer|final gate review/i;
|
|
16
16
|
const REVIEW_AGENT_TYPES = [
|
|
@@ -22,6 +22,9 @@ const REVIEW_AGENT_TYPE_SET = new Set(REVIEW_AGENT_TYPES);
|
|
|
22
22
|
export function applySpawnGuards(payload, options = {}) {
|
|
23
23
|
if (payload.hook_event_name !== "PreToolUse" || !SPAWN_TOOL_TOKENS.has(payload.tool_name))
|
|
24
24
|
return "";
|
|
25
|
+
const breaker = readAdmissionBreaker(payload.session_id);
|
|
26
|
+
if (breaker !== null)
|
|
27
|
+
return deny(`Subagent admission failed earlier in this session (${breaker}). Do not spawn more workers or reviewers; report the capacity block and wait for the user.`);
|
|
25
28
|
const scope = { sessionId: payload.session_id };
|
|
26
29
|
const stateDir = ulwLoopDir(payload.cwd, scope);
|
|
27
30
|
const plan = readPlan(join(stateDir, "goals.json"));
|
|
@@ -43,7 +46,7 @@ function evaluateGuards(payload, plan, stateDir) {
|
|
|
43
46
|
return deny(fanOutPeek);
|
|
44
47
|
const missingArtifact = missingGateArtifact(payload, plan);
|
|
45
48
|
if (missingArtifact !== null)
|
|
46
|
-
return deny(`
|
|
49
|
+
return deny(`record manual QA first; gate audits its artifacts: missing ${missingArtifact}`);
|
|
47
50
|
const reviewDenial = consumeReviewSpawnBudget(payload, plan, stateDir);
|
|
48
51
|
if (reviewDenial !== null)
|
|
49
52
|
return deny(reviewDenial);
|
|
@@ -52,6 +55,32 @@ function evaluateGuards(payload, plan, stateDir) {
|
|
|
52
55
|
return deny(fanOutDenial);
|
|
53
56
|
return "";
|
|
54
57
|
}
|
|
58
|
+
export async function runSpawnAdmissionRecorderCli(stdin, stdout) {
|
|
59
|
+
const chunks = [];
|
|
60
|
+
for await (const chunk of stdin)
|
|
61
|
+
chunks.push(Buffer.from(chunk));
|
|
62
|
+
try {
|
|
63
|
+
const payload = JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
64
|
+
const response = typeof payload["tool_response"] === "string"
|
|
65
|
+
? payload["tool_response"]
|
|
66
|
+
: JSON.stringify(payload["tool_response"] ?? "");
|
|
67
|
+
if (!/too many active cells|AgentLimitReached|max_threads|max_concurrent_threads_per_session/i.test(response))
|
|
68
|
+
return;
|
|
69
|
+
const dataDir = process.env["PLUGIN_DATA"];
|
|
70
|
+
if (typeof dataDir !== "string" || typeof payload["session_id"] !== "string")
|
|
71
|
+
return;
|
|
72
|
+
const markerDir = join(dataDir, "spawn-breaker");
|
|
73
|
+
mkdirSync(markerDir, { recursive: true });
|
|
74
|
+
atomicWriteJson(join(markerDir, `${payload["session_id"]}.json`), {
|
|
75
|
+
reason: response,
|
|
76
|
+
at: new Date().toISOString(),
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
/* malformed hook input is ignored */
|
|
81
|
+
}
|
|
82
|
+
void stdout;
|
|
83
|
+
}
|
|
55
84
|
export async function runSpawnGuardCli(stdin, stdout) {
|
|
56
85
|
try {
|
|
57
86
|
const chunks = [];
|
|
@@ -120,8 +149,7 @@ function missingGateArtifact(payload, plan) {
|
|
|
120
149
|
if (!goal.successCriteria.every((criterion) => criterion.status === "pass"))
|
|
121
150
|
return null;
|
|
122
151
|
const scope = { sessionId: payload.session_id };
|
|
123
|
-
const
|
|
124
|
-
const requiredArtifacts = surface === "omo-senpi" ? [`${goal.id}-manual-qa.md`] : [`${goal.id}-code-review.md`, `${goal.id}-manual-qa.md`];
|
|
152
|
+
const requiredArtifacts = [`${goal.id}-manual-qa.md`];
|
|
125
153
|
if (plan.evidenceLayoutVersion === 2) {
|
|
126
154
|
const attemptDir = ulwLoopAttemptEvidenceDir(goal.id, goal.attempt, scope);
|
|
127
155
|
for (const name of requiredArtifacts) {
|
|
@@ -131,17 +159,8 @@ function missingGateArtifact(payload, plan) {
|
|
|
131
159
|
}
|
|
132
160
|
return null;
|
|
133
161
|
}
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
return flatReport;
|
|
137
|
-
if (surface === "omo-senpi") {
|
|
138
|
-
const manualQa = `.omo/evidence/${goal.id}-manual-qa.md`;
|
|
139
|
-
return isNonEmptyFile(join(payload.cwd, manualQa)) ? null : manualQa;
|
|
140
|
-
}
|
|
141
|
-
// v1 manual-QA approximation: any other non-empty evidence file counts.
|
|
142
|
-
if (!hasOtherEvidenceFile(join(payload.cwd, ".omo", "evidence"), `${goal.id}-code-review.md`))
|
|
143
|
-
return `.omo/evidence/${goal.id}-manual-qa.md`;
|
|
144
|
-
return null;
|
|
162
|
+
const manualQa = `.omo/evidence/${goal.id}-manual-qa.md`;
|
|
163
|
+
return isNonEmptyFile(join(payload.cwd, manualQa)) ? null : manualQa;
|
|
145
164
|
}
|
|
146
165
|
function isGateReviewerSpawn(toolInput) {
|
|
147
166
|
const agentType = reviewAgentType(toolInput);
|
|
@@ -212,6 +231,18 @@ function deny(reason) {
|
|
|
212
231
|
},
|
|
213
232
|
})}\n`;
|
|
214
233
|
}
|
|
234
|
+
function readAdmissionBreaker(sessionId) {
|
|
235
|
+
const dataDir = process.env["PLUGIN_DATA"];
|
|
236
|
+
if (typeof dataDir !== "string")
|
|
237
|
+
return null;
|
|
238
|
+
try {
|
|
239
|
+
const value = JSON.parse(readFileSync(join(dataDir, "spawn-breaker", `${sessionId}.json`), "utf8"));
|
|
240
|
+
return typeof value.reason === "string" ? value.reason : "capacity limit";
|
|
241
|
+
}
|
|
242
|
+
catch {
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
215
246
|
function fanOutLimit() {
|
|
216
247
|
const raw = process.env["OMO_SPAWN_FANOUT_LIMIT"];
|
|
217
248
|
if (raw === undefined)
|
|
@@ -236,16 +267,6 @@ function isNonEmptyFile(path) {
|
|
|
236
267
|
throw error;
|
|
237
268
|
}
|
|
238
269
|
}
|
|
239
|
-
function hasOtherEvidenceFile(evidenceDir, excludedName) {
|
|
240
|
-
try {
|
|
241
|
-
return readdirSync(evidenceDir).some((name) => name !== excludedName && isNonEmptyFile(join(evidenceDir, name)));
|
|
242
|
-
}
|
|
243
|
-
catch (error) {
|
|
244
|
-
if (error instanceof Error)
|
|
245
|
-
return false;
|
|
246
|
-
throw error;
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
270
|
function readCount(counterPath) {
|
|
250
271
|
try {
|
|
251
272
|
const parsed = JSON.parse(readFileSync(counterPath, "utf8"));
|
|
@@ -25,9 +25,9 @@ function activeGoalActions(plan, active) {
|
|
|
25
25
|
];
|
|
26
26
|
if (hasAllCriteriaPass(active) && isFinalRunCompletionCandidate(plan, active))
|
|
27
27
|
return [
|
|
28
|
-
`${active.id} passes every criterion and is the final story: update_goal complete, then checkpoint --print-template to build the final quality gate.`,
|
|
28
|
+
`${active.id} passes every criterion and is the final story: update_goal complete, then checkpoint --print-template to build the final quality gate. Use status --json's currentAttemptDir for all quality-gate artifacts.`,
|
|
29
29
|
];
|
|
30
30
|
return [
|
|
31
|
-
`${active.id} passes every criterion: close it with \`omo-agent-toolkit ulw-loop checkpoint --goal-id ${active.id} --status complete --evidence "<proof>" --codex-goal-json '<get_goal json>'
|
|
31
|
+
`${active.id} passes every criterion: close it with \`omo-agent-toolkit ulw-loop checkpoint --goal-id ${active.id} --status complete --evidence "<proof>" --codex-goal-json '<get_goal json>'\`. Put quality-gate artifacts under the currentAttemptDir shown by status --json.`,
|
|
32
32
|
];
|
|
33
33
|
}
|
|
@@ -8,6 +8,7 @@ export declare const REVIEWER_ROLES_BY_SURFACE: Readonly<Record<UlwLoopToolkitSu
|
|
|
8
8
|
export declare const GATE_REVIEWER_AGENT_NAMES: ReadonlySet<string>;
|
|
9
9
|
export type UlwLoopGateSection = "codeReview" | "manualQa" | "gateReview" | "iteration" | "criteriaCoverage";
|
|
10
10
|
export declare const REQUIRED_GATE_SECTIONS_BY_SURFACE: Readonly<Record<UlwLoopToolkitSurface, readonly UlwLoopGateSection[]>>;
|
|
11
|
+
export declare const OPTIONAL_GATE_SECTIONS_BY_SURFACE: Readonly<Record<UlwLoopToolkitSurface, readonly UlwLoopGateSection[]>>;
|
|
11
12
|
export declare const GATE_SECTION_BY_ACCEPTOR: Readonly<Record<UlwLoopToolkitSurface, Readonly<Partial<Record<UlwLoopGateSection, readonly string[]>>>>>;
|
|
12
13
|
export declare function reviewerRolesFor(surface: UlwLoopToolkitSurface): UlwLoopReviewerRoles;
|
|
13
14
|
export declare const SURFACE_MARKER_FILENAME = "surface.json";
|
|
@@ -15,14 +15,24 @@ export const REVIEWER_ROLES_BY_SURFACE = {
|
|
|
15
15
|
};
|
|
16
16
|
export const GATE_REVIEWER_AGENT_NAMES = new Set(Object.values(REVIEWER_ROLES_BY_SURFACE).map((roles) => roles.gateReview));
|
|
17
17
|
export const REQUIRED_GATE_SECTIONS_BY_SURFACE = {
|
|
18
|
-
lazycodex: ["
|
|
18
|
+
lazycodex: ["manualQa", "gateReview", "iteration", "criteriaCoverage"],
|
|
19
19
|
"omo-senpi": ["manualQa", "gateReview", "iteration", "criteriaCoverage"],
|
|
20
20
|
};
|
|
21
|
+
export const OPTIONAL_GATE_SECTIONS_BY_SURFACE = {
|
|
22
|
+
lazycodex: ["codeReview"],
|
|
23
|
+
"omo-senpi": [],
|
|
24
|
+
};
|
|
21
25
|
export const GATE_SECTION_BY_ACCEPTOR = {
|
|
22
26
|
lazycodex: {
|
|
23
|
-
codeReview: [REVIEWER_ROLES_BY_SURFACE.lazycodex.codeReview],
|
|
24
|
-
manualQa: [REVIEWER_ROLES_BY_SURFACE.lazycodex.manualQa],
|
|
25
|
-
gateReview: [
|
|
27
|
+
codeReview: [REVIEWER_ROLES_BY_SURFACE.lazycodex.codeReview, "main-session"],
|
|
28
|
+
manualQa: [REVIEWER_ROLES_BY_SURFACE.lazycodex.manualQa, "main-session"],
|
|
29
|
+
gateReview: [
|
|
30
|
+
REVIEWER_ROLES_BY_SURFACE.lazycodex.gateReview,
|
|
31
|
+
"category:deep",
|
|
32
|
+
"category:unspecified-high",
|
|
33
|
+
"category:unspecified-low",
|
|
34
|
+
"main-session",
|
|
35
|
+
],
|
|
26
36
|
},
|
|
27
37
|
"omo-senpi": {
|
|
28
38
|
manualQa: ["main-session"],
|
|
@@ -7,7 +7,20 @@
|
|
|
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 5.0.0-beta.
|
|
10
|
+
"statusMessage": "(OmO 5.0.0-beta.49) Checking Ulw-Loop Steering"
|
|
11
|
+
}
|
|
12
|
+
]
|
|
13
|
+
}
|
|
14
|
+
],
|
|
15
|
+
"PostToolUse": [
|
|
16
|
+
{
|
|
17
|
+
"matcher": "^(spawn_agent|collaborationspawn_agent|collaboration\\.spawn_agent)$",
|
|
18
|
+
"hooks": [
|
|
19
|
+
{
|
|
20
|
+
"type": "command",
|
|
21
|
+
"command": "node \"${PLUGIN_ROOT}/dist/cli.js\" hook post-tool-use-spawn",
|
|
22
|
+
"timeout": 5,
|
|
23
|
+
"statusMessage": "(OmO 5.0.0-beta.49) Recording Spawn Admission"
|
|
11
24
|
}
|
|
12
25
|
]
|
|
13
26
|
}
|
|
@@ -20,7 +33,7 @@
|
|
|
20
33
|
"type": "command",
|
|
21
34
|
"command": "node \"${PLUGIN_ROOT}/dist/cli.js\" hook pre-tool-use",
|
|
22
35
|
"timeout": 5,
|
|
23
|
-
"statusMessage": "(OmO 5.0.0-beta.
|
|
36
|
+
"statusMessage": "(OmO 5.0.0-beta.49) Enforcing Unlimited Ulw-Loop Budget"
|
|
24
37
|
}
|
|
25
38
|
]
|
|
26
39
|
},
|
|
@@ -31,7 +44,7 @@
|
|
|
31
44
|
"type": "command",
|
|
32
45
|
"command": "node \"${PLUGIN_ROOT}/dist/cli.js\" hook pre-tool-use-spawn",
|
|
33
46
|
"timeout": 5,
|
|
34
|
-
"statusMessage": "(OmO 5.0.0-beta.
|
|
47
|
+
"statusMessage": "(OmO 5.0.0-beta.49) Guarding Ulw-Loop Spawns"
|
|
35
48
|
}
|
|
36
49
|
]
|
|
37
50
|
}
|
|
@@ -43,7 +56,7 @@
|
|
|
43
56
|
"type": "command",
|
|
44
57
|
"command": "node \"${PLUGIN_ROOT}/dist/cli.js\" hook stop",
|
|
45
58
|
"timeout": 10,
|
|
46
|
-
"statusMessage": "(OmO 5.0.0-beta.
|
|
59
|
+
"statusMessage": "(OmO 5.0.0-beta.49) Checking Ulw-Loop Resume"
|
|
47
60
|
}
|
|
48
61
|
]
|
|
49
62
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@code-yeongyu/codex-ulw-loop",
|
|
3
|
-
"version": "5.0.0-beta.
|
|
3
|
+
"version": "5.0.0-beta.49",
|
|
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",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"CHANGELOG.md"
|
|
39
39
|
],
|
|
40
40
|
"scripts": {
|
|
41
|
-
"build": "tsc -p tsconfig.build.json",
|
|
41
|
+
"build": "tsc -p tsconfig.build.json && bun build src/cli.ts --target node --format esm --outfile dist/cli.js",
|
|
42
42
|
"test": "vitest --run",
|
|
43
43
|
"test:watch": "vitest",
|
|
44
44
|
"typecheck": "tsc --noEmit",
|
package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/references/full-workflow.md
CHANGED
|
@@ -198,7 +198,7 @@ omo-agent-toolkit ulw-loop checkpoint --goal-id <g> --status complete --evidence
|
|
|
198
198
|
omo-agent-toolkit ulw-loop complete-goals
|
|
199
199
|
```
|
|
200
200
|
|
|
201
|
-
The lazycodex gate
|
|
201
|
+
The lazycodex gate requires `manualQa`, `gateReview`, `iteration`, and `criteriaCoverage`; `codeReview` is optional. Self-review defaults to `main-session`, while gate review may use `main-session` or an approved `category:*` acceptor. Spawn reviewer lanes only when strict review is explicitly requested.
|
|
202
202
|
|
|
203
203
|
## Final Quality Gate
|
|
204
204
|
Trigger only for the final aggregate goal after every criterion in every goal is `pass`.
|
|
@@ -31,7 +31,7 @@ function gateTemplate(surface: UlwLoopToolkitSurface, base: string): Record<stri
|
|
|
31
31
|
},
|
|
32
32
|
];
|
|
33
33
|
const manualQa = {
|
|
34
|
-
by:
|
|
34
|
+
by: "main-session",
|
|
35
35
|
status: "passed",
|
|
36
36
|
evidence: "<replace:manual QA evidence>",
|
|
37
37
|
surfaceEvidence: [
|
|
@@ -68,7 +68,7 @@ function gateTemplate(surface: UlwLoopToolkitSurface, base: string): Record<stri
|
|
|
68
68
|
const common = {
|
|
69
69
|
manualQa,
|
|
70
70
|
gateReview: {
|
|
71
|
-
by: surface === "omo-senpi" ? "category:deep" : "
|
|
71
|
+
by: surface === "omo-senpi" ? "category:deep" : "main-session",
|
|
72
72
|
recommendation: "APPROVE",
|
|
73
73
|
reportPath: artifactPath(base, "gate-review.md"),
|
|
74
74
|
evidence: "<replace:gate review evidence>",
|
|
@@ -90,18 +90,7 @@ function gateTemplate(surface: UlwLoopToolkitSurface, base: string): Record<stri
|
|
|
90
90
|
adversarialClassesCovered: ["<replace:adversarial class>"],
|
|
91
91
|
},
|
|
92
92
|
};
|
|
93
|
-
|
|
94
|
-
return {
|
|
95
|
-
codeReview: {
|
|
96
|
-
by: "lazycodex-code-reviewer",
|
|
97
|
-
recommendation: "APPROVE",
|
|
98
|
-
codeQualityStatus: "CLEAR",
|
|
99
|
-
reportPath: artifactPath(base, "code-review.md"),
|
|
100
|
-
evidence: "<replace:code review evidence>",
|
|
101
|
-
blockers: [],
|
|
102
|
-
},
|
|
103
|
-
...common,
|
|
104
|
-
};
|
|
93
|
+
return common;
|
|
105
94
|
}
|
|
106
95
|
|
|
107
96
|
export async function checkpointTemplate(
|
|
@@ -121,6 +110,11 @@ export async function checkpointTemplate(
|
|
|
121
110
|
"Fill every <replace:...> value with plausible non-empty evidence and use real, non-empty artifact files.",
|
|
122
111
|
'Passing codex-goal-json example: {"goal":{"objective":"<plan codexObjective verbatim>","status":"complete"}}.',
|
|
123
112
|
'Passing quality-gate-json example requires gateReview {"by":"category:deep","recommendation":"APPROVE","evidence":"review passed","reportPath":"<attemptDir>/gate-review.md","blockers":[],"notes":[]}, manualQa.artifactRefs objects, iteration, and criteriaCoverage.',
|
|
113
|
+
...(resolveToolkitSurface() === "lazycodex"
|
|
114
|
+
? [
|
|
115
|
+
"Self-review defaults: manualQa.by and gateReview.by are main-session. Alternatives: manualQa.by accepts lazycodex-qa-executor; gateReview.by accepts lazycodex-gate-reviewer, category:deep, category:unspecified-high, or category:unspecified-low. Optional codeReview.by accepts lazycodex-code-reviewer or main-session.",
|
|
116
|
+
]
|
|
117
|
+
: []),
|
|
124
118
|
...(hasAttempt ? [] : ["This plan is evidence-layout v1; artifacts go under .omo/evidence/."]),
|
|
125
119
|
].join(" ");
|
|
126
120
|
return {
|
|
@@ -126,7 +126,7 @@ function buildLedger(
|
|
|
126
126
|
codexGoal: unknown,
|
|
127
127
|
aggregateCompletion: UlwLoopAggregateCompletion | undefined,
|
|
128
128
|
): UlwLoopLedgerEntry {
|
|
129
|
-
const watch = qualityGate?.surface === "lazycodex" && qualityGate.codeReview
|
|
129
|
+
const watch = qualityGate?.surface === "lazycodex" && qualityGate.codeReview?.codeQualityStatus === "WATCH";
|
|
130
130
|
const entry: UlwLoopLedgerEntry = {
|
|
131
131
|
at: now,
|
|
132
132
|
kind: ledgerKind(args.status, goal, aggregateCompletion),
|
|
@@ -134,7 +134,7 @@ function buildLedger(
|
|
|
134
134
|
status: goal.status,
|
|
135
135
|
evidence:
|
|
136
136
|
watch && qualityGate.surface === "lazycodex"
|
|
137
|
-
? `${args.evidence} | codeQuality=WATCH: ${qualityGate.codeReview
|
|
137
|
+
? `${args.evidence} | codeQuality=WATCH: ${qualityGate.codeReview?.evidence ?? ""}`
|
|
138
138
|
: args.evidence,
|
|
139
139
|
};
|
|
140
140
|
if (codexGoal !== undefined) entry.codexGoal = codexGoal;
|
|
@@ -16,7 +16,7 @@ export const ULW_LOOP_HELP = `Usage:
|
|
|
16
16
|
omo-agent-toolkit ulw-loop add-goal --title "..." --objective "..." [--json]
|
|
17
17
|
omo-agent-toolkit ulw-loop record-review-blockers --goal-id <id> --title "..." --objective "..." --evidence "..." --codex-goal-json <...> [--json]
|
|
18
18
|
|
|
19
|
-
Every state subcommand needs a session scope: [--session-id <id>] or the session env (OMO_ULW_LOOP_SESSION_ID / CODEX_SESSION_ID / CODEX_THREAD_ID / PI_SESSION_ID); state lives under .omo/ulw-loop/<id>/ and the unscoped root is never used implicitly.
|
|
19
|
+
Every state subcommand needs a session scope: [--session-id <id>] or the session env (OMO_ULW_LOOP_SESSION_ID / CODEX_SESSION_ID / CODEX_THREAD_ID / PI_SESSION_ID); state lives under .omo/ulw-loop/<id>/ and the unscoped root is never used implicitly. status --json exposes the currentAttemptDir; put all quality-gate artifacts under it.
|
|
20
20
|
Every subcommand accepts --help | -h to print its own usage line.`;
|
|
21
21
|
|
|
22
22
|
export function subcommandHelp(subcommand: string): string {
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { isUlwLoopSubcommand, ulwLoopCommand } from "./cli-commands.js";
|
|
3
3
|
import { ULW_LOOP_HELP } from "./cli-output.js";
|
|
4
4
|
import { runPreToolUseGoalBudgetGuardCli, runUlwLoopHookCli } from "./codex-hook.js";
|
|
5
|
-
import { runSpawnGuardCli } from "./spawn-guard.js";
|
|
5
|
+
import { runSpawnAdmissionRecorderCli, runSpawnGuardCli } from "./spawn-guard.js";
|
|
6
6
|
import { runStopResumeHookCli } from "./stop-resume-hook.js";
|
|
7
7
|
|
|
8
8
|
async function main(): Promise<number> {
|
|
@@ -29,6 +29,10 @@ async function main(): Promise<number> {
|
|
|
29
29
|
await runStopResumeHookCli(process.stdin, process.stdout);
|
|
30
30
|
return 0;
|
|
31
31
|
}
|
|
32
|
+
if (sub === "post-tool-use-spawn") {
|
|
33
|
+
await runSpawnAdmissionRecorderCli(process.stdin, process.stdout);
|
|
34
|
+
return 0;
|
|
35
|
+
}
|
|
32
36
|
if (sub === "pre-tool-use-spawn") {
|
|
33
37
|
await runSpawnGuardCli(process.stdin, process.stdout);
|
|
34
38
|
return 0;
|
|
@@ -131,12 +131,11 @@ function finalSection(
|
|
|
131
131
|
const blockerCommand = `omo-agent-toolkit ulw-loop record-review-blockers${option} --goal-id ${goal.id} --title "Resolve final code-review blockers" --objective "<blocker-resolution objective>" --evidence "<review findings>" --codex-goal-json "<active get_goal JSON or path>"`;
|
|
132
132
|
const checkpointCommand = `omo-agent-toolkit ulw-loop checkpoint${option} --goal-id ${goal.id} --status complete --evidence "<targeted verification/manualQa/gateReview evidence>" --codex-goal-json "<fresh complete get_goal JSON or path>" --quality-gate-json "<quality gate JSON or path>"`;
|
|
133
133
|
return joinLines([
|
|
134
|
-
"Final story —
|
|
134
|
+
"Final story — self-review and manual QA are the default; use the quality gate before update_goal:",
|
|
135
135
|
"- Run targeted verification for changed behavior.",
|
|
136
136
|
"- Confirm every manualQa artifact path exists and has non-zero size.",
|
|
137
|
-
`-
|
|
138
|
-
|
|
139
|
-
"- Require clean codeReview, manualQa, gateReview, iteration, and criteriaCoverage. criteriaCoverage must summarize originalIntent, desiredOutcome, and userOutcomeReview; counts alone are not approval.",
|
|
137
|
+
`- Run manual QA yourself and write its artifact under currentAttemptDir. Only if the user explicitly demands strict, rigorous, or high-accuracy review, spawn ${roles.gateReview}, optionally also ${roles.codeReview} and ${roles.manualQa}; otherwise set manualQa.by and gateReview.by to "main-session".`,
|
|
138
|
+
"- Require passed manualQa, approved gateReview, passed iteration, and complete criteriaCoverage; include codeReview only when strict review was requested. criteriaCoverage must summarize originalIntent, desiredOutcome, and userOutcomeReview; counts alone are not approval.",
|
|
140
139
|
"- On a reviewer REJECT, fix only the cited blockers, rerun the affected verification/Manual-QA, and re-review the delta at most TWICE; if blockers remain, record them and surface to the user.",
|
|
141
140
|
"- If codeQualityStatus is WATCH, include the WATCH notes verbatim in your final user-facing message.",
|
|
142
141
|
"- If any reviewer is blocked/inconclusive or the quality gate is not clean, do not call update_goal. Record blocker work first:",
|
|
@@ -141,7 +141,7 @@ interface UlwLoopQualityGateCommon {
|
|
|
141
141
|
|
|
142
142
|
export interface UlwLoopQualityGateLazycodex extends UlwLoopQualityGateCommon {
|
|
143
143
|
readonly surface: "lazycodex";
|
|
144
|
-
readonly codeReview
|
|
144
|
+
readonly codeReview?: {
|
|
145
145
|
readonly by: string;
|
|
146
146
|
readonly recommendation: "APPROVE";
|
|
147
147
|
readonly codeQualityStatus: "CLEAR" | "WATCH";
|
|
@@ -61,7 +61,10 @@ export function isRecord(value: unknown): value is Record<string, unknown> {
|
|
|
61
61
|
}
|
|
62
62
|
export function section(value: unknown, field: string): Record<string, unknown> {
|
|
63
63
|
if (isRecord(value)) return value;
|
|
64
|
-
invalid(
|
|
64
|
+
invalid(
|
|
65
|
+
`Final quality gate is missing ${field} evidence (accepted input: a bare object with manualQa/gateReview/iteration/criteriaCoverage and optional codeReview, or the same object under a top-level "qualityGate" key).`,
|
|
66
|
+
field,
|
|
67
|
+
);
|
|
65
68
|
return {};
|
|
66
69
|
}
|
|
67
70
|
export function textField(value: unknown, field: string): string {
|
|
@@ -20,7 +20,12 @@ import {
|
|
|
20
20
|
withQualityGateCollector,
|
|
21
21
|
} from "./quality-gate-fields.js";
|
|
22
22
|
import { adversarialVerdict, codeQualityStatusField, passedVerdict } from "./quality-gate-verdicts.js";
|
|
23
|
-
import {
|
|
23
|
+
import {
|
|
24
|
+
GATE_SECTION_BY_ACCEPTOR,
|
|
25
|
+
OPTIONAL_GATE_SECTIONS_BY_SURFACE,
|
|
26
|
+
REQUIRED_GATE_SECTIONS_BY_SURFACE,
|
|
27
|
+
type UlwLoopToolkitSurface,
|
|
28
|
+
} from "./surface.js";
|
|
24
29
|
import type { UlwLoopManualQaArtifactRef, UlwLoopQualityGate } from "./types.js";
|
|
25
30
|
|
|
26
31
|
export {
|
|
@@ -42,20 +47,10 @@ export interface ValidateQualityGateOptions {
|
|
|
42
47
|
readonly reviewerSurface?: UlwLoopToolkitSurface;
|
|
43
48
|
}
|
|
44
49
|
|
|
45
|
-
function reviewerRoleField(value: unknown, expected: string, field: string): string {
|
|
46
|
-
if (typeof value !== "string" || value.trim() === "") {
|
|
47
|
-
textField(value, field);
|
|
48
|
-
return expected;
|
|
49
|
-
}
|
|
50
|
-
const actual = textField(value, field);
|
|
51
|
-
if (actual !== expected) invalid(`${field} must be ${expected}.`, field);
|
|
52
|
-
return expected;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
50
|
function reviewerAcceptorField(
|
|
56
51
|
value: unknown,
|
|
57
52
|
surface: UlwLoopToolkitSurface,
|
|
58
|
-
sectionName: "manualQa" | "gateReview",
|
|
53
|
+
sectionName: "codeReview" | "manualQa" | "gateReview",
|
|
59
54
|
): string {
|
|
60
55
|
const field = `${sectionName}.by`;
|
|
61
56
|
const accepted = GATE_SECTION_BY_ACCEPTOR[surface][sectionName];
|
|
@@ -75,19 +70,28 @@ export function validateQualityGate(input: unknown, opts?: ValidateQualityGateOp
|
|
|
75
70
|
|
|
76
71
|
function validateQualityGateUncollected(input: unknown, opts?: ValidateQualityGateOptions): UlwLoopQualityGate {
|
|
77
72
|
const surface = opts?.reviewerSurface ?? "lazycodex";
|
|
78
|
-
const
|
|
73
|
+
const raw = input as Record<string, unknown>;
|
|
74
|
+
const gate = section(
|
|
75
|
+
raw && typeof raw === "object" && "qualityGate" in raw ? raw["qualityGate"] : input,
|
|
76
|
+
"qualityGate",
|
|
77
|
+
);
|
|
78
|
+
for (const name of REQUIRED_GATE_SECTIONS_BY_SURFACE[surface]) section(gate[name], name);
|
|
79
|
+
for (const name of Object.keys(gate)) {
|
|
80
|
+
if (name === "codeReview" && !OPTIONAL_GATE_SECTIONS_BY_SURFACE[surface].includes(name as never))
|
|
81
|
+
invalid("omo-senpi gate has no codeReview lane.", name);
|
|
82
|
+
}
|
|
79
83
|
if (surface === "omo-senpi" && gate["codeReview"] !== undefined)
|
|
80
84
|
invalid("omo-senpi gate has no codeReview lane.", "codeReview");
|
|
81
85
|
const manualQa = section(gate["manualQa"], "manualQa");
|
|
82
86
|
const gateReview = section(gate["gateReview"], "gateReview");
|
|
83
87
|
const iteration = section(gate["iteration"], "iteration");
|
|
84
88
|
const coverage = section(gate["criteriaCoverage"], "criteriaCoverage");
|
|
85
|
-
const codeReview =
|
|
89
|
+
const codeReview = gate["codeReview"] !== undefined ? section(gate["codeReview"], "codeReview") : undefined;
|
|
86
90
|
const manualQaBy = reviewerAcceptorField(manualQa["by"], surface, "manualQa");
|
|
87
91
|
const gateReviewBy = reviewerAcceptorField(gateReview["by"], surface, "gateReview");
|
|
88
92
|
const manualQaEvidence = textField(manualQa["evidence"], "manualQa.evidence");
|
|
89
93
|
const gateReviewEvidence = textField(gateReview["evidence"], "gateReview.evidence");
|
|
90
|
-
|
|
94
|
+
|
|
91
95
|
const totalCriteria = numberField(coverage["totalCriteria"], "criteriaCoverage.totalCriteria");
|
|
92
96
|
const passCount = numberField(coverage["passCount"], "criteriaCoverage.passCount");
|
|
93
97
|
if (!isPoisoned("criteriaCoverage.passCount") && passCount < totalCriteria)
|
|
@@ -133,13 +137,14 @@ function validateQualityGateUncollected(input: unknown, opts?: ValidateQualityGa
|
|
|
133
137
|
},
|
|
134
138
|
};
|
|
135
139
|
if (surface === "omo-senpi") return { surface, ...common };
|
|
140
|
+
if (codeReview === undefined) return { surface, ...common };
|
|
136
141
|
const codeReportPath = textField(codeReview["reportPath"], "codeReview.reportPath");
|
|
137
142
|
checkFile(codeReportPath, "codeReview.reportPath", opts);
|
|
138
143
|
return {
|
|
139
144
|
surface,
|
|
140
145
|
...common,
|
|
141
146
|
codeReview: {
|
|
142
|
-
by: "
|
|
147
|
+
by: reviewerAcceptorField(codeReview["by"], surface, "codeReview"),
|
|
143
148
|
recommendation: literal(codeReview["recommendation"], "APPROVE", "codeReview.recommendation"),
|
|
144
149
|
codeQualityStatus: codeQualityStatusField(codeReview["codeQualityStatus"], "codeReview.codeQualityStatus"),
|
|
145
150
|
reportPath: codeReportPath,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { randomBytes } from "node:crypto";
|
|
2
|
-
import { existsSync,
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
4
|
|
|
5
5
|
import type { PreToolUsePayload } from "./codex-hook.js";
|
|
@@ -19,7 +19,7 @@ import type { UlwLoopPlan } from "./types.js";
|
|
|
19
19
|
// hook token from codex-rs hook_names.rs; collaboration.spawn_agent = the
|
|
20
20
|
// dotted token observed live in the task-1 probe (hook-tool-tokens.txt).
|
|
21
21
|
const SPAWN_TOOL_TOKENS = new Set(["spawn_agent", "collaborationspawn_agent", "collaboration.spawn_agent"]);
|
|
22
|
-
const DEFAULT_FANOUT_LIMIT =
|
|
22
|
+
export const DEFAULT_FANOUT_LIMIT = 24;
|
|
23
23
|
const DEFAULT_REVIEW_SPAWN_LIMIT = 3;
|
|
24
24
|
const GATE_MESSAGE_PATTERN = /lazycodex-gate-reviewer|omo-senpi-gate-reviewer|final gate review/i;
|
|
25
25
|
const REVIEW_AGENT_TYPES = [
|
|
@@ -35,6 +35,11 @@ export interface SpawnGuardOptions {
|
|
|
35
35
|
|
|
36
36
|
export function applySpawnGuards(payload: PreToolUsePayload, options: SpawnGuardOptions = {}): string {
|
|
37
37
|
if (payload.hook_event_name !== "PreToolUse" || !SPAWN_TOOL_TOKENS.has(payload.tool_name)) return "";
|
|
38
|
+
const breaker = readAdmissionBreaker(payload.session_id);
|
|
39
|
+
if (breaker !== null)
|
|
40
|
+
return deny(
|
|
41
|
+
`Subagent admission failed earlier in this session (${breaker}). Do not spawn more workers or reviewers; report the capacity block and wait for the user.`,
|
|
42
|
+
);
|
|
38
43
|
const scope = { sessionId: payload.session_id } as const;
|
|
39
44
|
const stateDir = ulwLoopDir(payload.cwd, scope);
|
|
40
45
|
const plan = readPlan(join(stateDir, "goals.json"));
|
|
@@ -59,7 +64,7 @@ function evaluateGuards(payload: PreToolUsePayload, plan: UlwLoopPlan, stateDir:
|
|
|
59
64
|
if (fanOutPeek !== null) return deny(fanOutPeek);
|
|
60
65
|
const missingArtifact = missingGateArtifact(payload, plan);
|
|
61
66
|
if (missingArtifact !== null)
|
|
62
|
-
return deny(`
|
|
67
|
+
return deny(`record manual QA first; gate audits its artifacts: missing ${missingArtifact}`);
|
|
63
68
|
const reviewDenial = consumeReviewSpawnBudget(payload, plan, stateDir);
|
|
64
69
|
if (reviewDenial !== null) return deny(reviewDenial);
|
|
65
70
|
const fanOutDenial = consumeFanOutBudget(stateDir);
|
|
@@ -67,6 +72,34 @@ function evaluateGuards(payload: PreToolUsePayload, plan: UlwLoopPlan, stateDir:
|
|
|
67
72
|
return "";
|
|
68
73
|
}
|
|
69
74
|
|
|
75
|
+
export async function runSpawnAdmissionRecorderCli(
|
|
76
|
+
stdin: NodeJS.ReadableStream,
|
|
77
|
+
stdout: NodeJS.WritableStream,
|
|
78
|
+
): Promise<void> {
|
|
79
|
+
const chunks: Buffer[] = [];
|
|
80
|
+
for await (const chunk of stdin) chunks.push(Buffer.from(chunk));
|
|
81
|
+
try {
|
|
82
|
+
const payload = JSON.parse(Buffer.concat(chunks).toString("utf8")) as Record<string, unknown>;
|
|
83
|
+
const response =
|
|
84
|
+
typeof payload["tool_response"] === "string"
|
|
85
|
+
? payload["tool_response"]
|
|
86
|
+
: JSON.stringify(payload["tool_response"] ?? "");
|
|
87
|
+
if (!/too many active cells|AgentLimitReached|max_threads|max_concurrent_threads_per_session/i.test(response))
|
|
88
|
+
return;
|
|
89
|
+
const dataDir = process.env["PLUGIN_DATA"];
|
|
90
|
+
if (typeof dataDir !== "string" || typeof payload["session_id"] !== "string") return;
|
|
91
|
+
const markerDir = join(dataDir, "spawn-breaker");
|
|
92
|
+
mkdirSync(markerDir, { recursive: true });
|
|
93
|
+
atomicWriteJson(join(markerDir, `${payload["session_id"]}.json`), {
|
|
94
|
+
reason: response,
|
|
95
|
+
at: new Date().toISOString(),
|
|
96
|
+
});
|
|
97
|
+
} catch {
|
|
98
|
+
/* malformed hook input is ignored */
|
|
99
|
+
}
|
|
100
|
+
void stdout;
|
|
101
|
+
}
|
|
102
|
+
|
|
70
103
|
export async function runSpawnGuardCli(stdin: NodeJS.ReadableStream, stdout: NodeJS.WritableStream): Promise<void> {
|
|
71
104
|
try {
|
|
72
105
|
const chunks: Buffer[] = [];
|
|
@@ -128,9 +161,7 @@ function missingGateArtifact(payload: PreToolUsePayload, plan: UlwLoopPlan): str
|
|
|
128
161
|
if (goal === undefined || goal.status === "complete") return null;
|
|
129
162
|
if (!goal.successCriteria.every((criterion) => criterion.status === "pass")) return null;
|
|
130
163
|
const scope = { sessionId: payload.session_id } as const;
|
|
131
|
-
const
|
|
132
|
-
const requiredArtifacts =
|
|
133
|
-
surface === "omo-senpi" ? [`${goal.id}-manual-qa.md`] : [`${goal.id}-code-review.md`, `${goal.id}-manual-qa.md`];
|
|
164
|
+
const requiredArtifacts = [`${goal.id}-manual-qa.md`];
|
|
134
165
|
if (plan.evidenceLayoutVersion === 2) {
|
|
135
166
|
const attemptDir = ulwLoopAttemptEvidenceDir(goal.id, goal.attempt, scope);
|
|
136
167
|
for (const name of requiredArtifacts) {
|
|
@@ -139,16 +170,8 @@ function missingGateArtifact(payload: PreToolUsePayload, plan: UlwLoopPlan): str
|
|
|
139
170
|
}
|
|
140
171
|
return null;
|
|
141
172
|
}
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
if (surface === "omo-senpi") {
|
|
145
|
-
const manualQa = `.omo/evidence/${goal.id}-manual-qa.md`;
|
|
146
|
-
return isNonEmptyFile(join(payload.cwd, manualQa)) ? null : manualQa;
|
|
147
|
-
}
|
|
148
|
-
// v1 manual-QA approximation: any other non-empty evidence file counts.
|
|
149
|
-
if (!hasOtherEvidenceFile(join(payload.cwd, ".omo", "evidence"), `${goal.id}-code-review.md`))
|
|
150
|
-
return `.omo/evidence/${goal.id}-manual-qa.md`;
|
|
151
|
-
return null;
|
|
173
|
+
const manualQa = `.omo/evidence/${goal.id}-manual-qa.md`;
|
|
174
|
+
return isNonEmptyFile(join(payload.cwd, manualQa)) ? null : manualQa;
|
|
152
175
|
}
|
|
153
176
|
|
|
154
177
|
function isGateReviewerSpawn(toolInput: unknown): boolean {
|
|
@@ -216,6 +239,19 @@ function deny(reason: string): string {
|
|
|
216
239
|
})}\n`;
|
|
217
240
|
}
|
|
218
241
|
|
|
242
|
+
function readAdmissionBreaker(sessionId: string): string | null {
|
|
243
|
+
const dataDir = process.env["PLUGIN_DATA"];
|
|
244
|
+
if (typeof dataDir !== "string") return null;
|
|
245
|
+
try {
|
|
246
|
+
const value = JSON.parse(readFileSync(join(dataDir, "spawn-breaker", `${sessionId}.json`), "utf8")) as {
|
|
247
|
+
reason?: unknown;
|
|
248
|
+
};
|
|
249
|
+
return typeof value.reason === "string" ? value.reason : "capacity limit";
|
|
250
|
+
} catch {
|
|
251
|
+
return null;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
219
255
|
function fanOutLimit(): number {
|
|
220
256
|
const raw = process.env["OMO_SPAWN_FANOUT_LIMIT"];
|
|
221
257
|
if (raw === undefined) return DEFAULT_FANOUT_LIMIT;
|
|
@@ -239,15 +275,6 @@ function isNonEmptyFile(path: string): boolean {
|
|
|
239
275
|
}
|
|
240
276
|
}
|
|
241
277
|
|
|
242
|
-
function hasOtherEvidenceFile(evidenceDir: string, excludedName: string): boolean {
|
|
243
|
-
try {
|
|
244
|
-
return readdirSync(evidenceDir).some((name) => name !== excludedName && isNonEmptyFile(join(evidenceDir, name)));
|
|
245
|
-
} catch (error) {
|
|
246
|
-
if (error instanceof Error) return false;
|
|
247
|
-
throw error;
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
|
|
251
278
|
function readCount(counterPath: string): number {
|
|
252
279
|
try {
|
|
253
280
|
const parsed = JSON.parse(readFileSync(counterPath, "utf8")) as Record<string, unknown>;
|
|
@@ -28,9 +28,9 @@ function activeGoalActions(plan: UlwLoopPlan, active: UlwLoopItem): readonly str
|
|
|
28
28
|
];
|
|
29
29
|
if (hasAllCriteriaPass(active) && isFinalRunCompletionCandidate(plan, active))
|
|
30
30
|
return [
|
|
31
|
-
`${active.id} passes every criterion and is the final story: update_goal complete, then checkpoint --print-template to build the final quality gate.`,
|
|
31
|
+
`${active.id} passes every criterion and is the final story: update_goal complete, then checkpoint --print-template to build the final quality gate. Use status --json's currentAttemptDir for all quality-gate artifacts.`,
|
|
32
32
|
];
|
|
33
33
|
return [
|
|
34
|
-
`${active.id} passes every criterion: close it with \`omo-agent-toolkit ulw-loop checkpoint --goal-id ${active.id} --status complete --evidence "<proof>" --codex-goal-json '<get_goal json>'
|
|
34
|
+
`${active.id} passes every criterion: close it with \`omo-agent-toolkit ulw-loop checkpoint --goal-id ${active.id} --status complete --evidence "<proof>" --codex-goal-json '<get_goal json>'\`. Put quality-gate artifacts under the currentAttemptDir shown by status --json.`,
|
|
35
35
|
];
|
|
36
36
|
}
|