lazycodex-ai 5.1.12 → 5.1.14
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/dist/cli/index.js +45 -18
- package/dist/cli-node/index.js +45 -18
- package/package.json +1 -1
- package/packages/omo-codex/plugin/.codex-plugin/plugin.json +1 -1
- 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 +4 -4
- package/packages/omo-codex/plugin/components/lsp/dist/cli.js +33 -19
- package/packages/omo-codex/plugin/components/lsp/dist/codex-hook.js +27 -17
- 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/src/codex-hook.ts +36 -18
- package/packages/omo-codex/plugin/components/lsp/test/codex-hook-not-installed.test.ts +203 -0
- package/packages/omo-codex/plugin/components/rules/CHANGELOG.md +1 -0
- package/packages/omo-codex/plugin/components/rules/bundled-rules/hephaestus/gpt-5.5.md +1 -1
- package/packages/omo-codex/plugin/components/rules/bundled-rules/hephaestus/gpt-5.6.md +1 -1
- package/packages/omo-codex/plugin/components/rules/bundled-rules/hephaestus/gpt-6.md +2 -2
- 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/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/package.json +1 -1
- package/packages/omo-codex/plugin/components/ulw-execute-continuation/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/ulw-execute-continuation/package.json +1 -1
- package/packages/omo-codex/plugin/components/ulw-loop/dist/cli.js +89 -28
- package/packages/omo-codex/plugin/components/ulw-loop/dist/registered-agent-roles.d.ts +1 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/registered-agent-roles.js +61 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/spawn-guard.js +2 -1
- package/packages/omo-codex/plugin/components/ulw-loop/dist/spawn-role-guard.d.ts +1 -1
- package/packages/omo-codex/plugin/components/ulw-loop/dist/spawn-role-guard.js +6 -2
- package/packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json +5 -5
- package/packages/omo-codex/plugin/components/ulw-loop/package.json +1 -1
- package/packages/omo-codex/plugin/components/ulw-loop/src/registered-agent-roles.ts +60 -0
- package/packages/omo-codex/plugin/components/ulw-loop/src/spawn-guard.ts +2 -1
- package/packages/omo-codex/plugin/components/ulw-loop/src/spawn-role-guard.ts +8 -2
- package/packages/omo-codex/plugin/components/ulw-loop/test/spawn-role-matrix.test.ts +1 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/spawn-role-registered.test.ts +128 -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-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 +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 +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/skills/browser/SKILL.md +26 -2
- package/packages/omo-codex/plugin/skills/browser/references/commands.md +10 -1
- package/packages/omo-codex/plugin/skills/browser/references/install.md +7 -0
- package/packages/omo-codex/plugin/skills/browser/runtime/omowright/manifest.json +1 -1
- package/packages/omo-codex/plugin/skills/browser/scripts/browser-engine-guard.mjs +506 -0
- package/packages/omo-codex/plugin/skills/browser/scripts/omowright.mjs +29 -2
- package/packages/omo-codex/plugin/test/aggregate-plugin-fixture.mjs +4 -3
- package/packages/omo-codex/plugin/test/aggregate.test.mjs +2 -2
- package/packages/omo-codex/scripts/install-dist/install-local.mjs +2 -2
- package/packages/shared-skills/skills/browser/SKILL.md +26 -2
- package/packages/shared-skills/skills/browser/references/commands.md +10 -1
- package/packages/shared-skills/skills/browser/references/install.md +7 -0
- package/packages/shared-skills/skills/browser/runtime/omowright/manifest.json +1 -1
- package/packages/shared-skills/skills/browser/scripts/browser-engine-guard.mjs +506 -0
- package/packages/shared-skills/skills/browser/scripts/omowright.mjs +29 -2
|
@@ -4360,25 +4360,82 @@ function readAll(stdin) {
|
|
|
4360
4360
|
|
|
4361
4361
|
// components/ulw-loop/src/spawn-guard.ts
|
|
4362
4362
|
import { mkdirSync as mkdirSync3 } from "node:fs";
|
|
4363
|
-
import { join as
|
|
4363
|
+
import { join as join8 } from "node:path";
|
|
4364
|
+
|
|
4365
|
+
// components/ulw-loop/src/registered-agent-roles.ts
|
|
4366
|
+
import { readdirSync as readdirSync3, readFileSync as readFileSync6 } from "node:fs";
|
|
4367
|
+
import { homedir } from "node:os";
|
|
4368
|
+
import { basename, dirname as dirname3, join as join6, resolve as resolve5 } from "node:path";
|
|
4369
|
+
var NAME_LINE = /^\s*name\s*=\s*"([^"\n]+)"\s*(?:#.*)?$/m;
|
|
4370
|
+
var AGENT_TABLE = /^\s*\[agents\.(?:"([^"\n]+)"|([A-Za-z0-9_-]+))\]\s*(?:#.*)?$/gm;
|
|
4371
|
+
function readText(path) {
|
|
4372
|
+
try {
|
|
4373
|
+
return readFileSync6(path, "utf8");
|
|
4374
|
+
} catch {
|
|
4375
|
+
return null;
|
|
4376
|
+
}
|
|
4377
|
+
}
|
|
4378
|
+
function standaloneRoleNames(agentsDir) {
|
|
4379
|
+
let entries;
|
|
4380
|
+
try {
|
|
4381
|
+
entries = readdirSync3(agentsDir);
|
|
4382
|
+
} catch {
|
|
4383
|
+
return [];
|
|
4384
|
+
}
|
|
4385
|
+
const names = [];
|
|
4386
|
+
for (const entry of entries) {
|
|
4387
|
+
if (!entry.endsWith(".toml"))
|
|
4388
|
+
continue;
|
|
4389
|
+
const text = readText(join6(agentsDir, entry));
|
|
4390
|
+
if (text === null)
|
|
4391
|
+
continue;
|
|
4392
|
+
names.push(text.match(NAME_LINE)?.[1] ?? basename(entry, ".toml"));
|
|
4393
|
+
}
|
|
4394
|
+
return names;
|
|
4395
|
+
}
|
|
4396
|
+
function configTableRoleNames(configPath) {
|
|
4397
|
+
const text = readText(configPath);
|
|
4398
|
+
if (text === null)
|
|
4399
|
+
return [];
|
|
4400
|
+
return [...text.matchAll(AGENT_TABLE)].map((match) => match[1] ?? match[2] ?? "").filter(Boolean);
|
|
4401
|
+
}
|
|
4402
|
+
function codexDirs(cwd, env) {
|
|
4403
|
+
const dirs = [resolve5(env["CODEX_HOME"]?.trim() || join6(homedir(), ".codex"))];
|
|
4404
|
+
for (let dir = resolve5(cwd);; dir = dirname3(dir)) {
|
|
4405
|
+
dirs.push(join6(dir, ".codex"));
|
|
4406
|
+
if (dirname3(dir) === dir)
|
|
4407
|
+
break;
|
|
4408
|
+
}
|
|
4409
|
+
return [...new Set(dirs)];
|
|
4410
|
+
}
|
|
4411
|
+
function registeredAgentRoles(cwd, env = process.env) {
|
|
4412
|
+
const names = new Set;
|
|
4413
|
+
for (const dir of codexDirs(cwd, env)) {
|
|
4414
|
+
for (const name of standaloneRoleNames(join6(dir, "agents")))
|
|
4415
|
+
names.add(name);
|
|
4416
|
+
for (const name of configTableRoleNames(join6(dir, "config.toml")))
|
|
4417
|
+
names.add(name);
|
|
4418
|
+
}
|
|
4419
|
+
return names;
|
|
4420
|
+
}
|
|
4364
4421
|
|
|
4365
4422
|
// components/ulw-loop/src/spawn-budget-io.ts
|
|
4366
4423
|
import { randomBytes } from "node:crypto";
|
|
4367
|
-
import { existsSync as existsSync7, readFileSync as
|
|
4368
|
-
import { dirname as
|
|
4424
|
+
import { existsSync as existsSync7, readFileSync as readFileSync7, renameSync as renameSync2, statSync as statSync3, writeFileSync as writeFileSync2 } from "node:fs";
|
|
4425
|
+
import { dirname as dirname4, join as join7 } from "node:path";
|
|
4369
4426
|
function readAdmissionBreaker(sessionId) {
|
|
4370
4427
|
const dataDir = process.env["PLUGIN_DATA"];
|
|
4371
4428
|
if (typeof dataDir !== "string")
|
|
4372
4429
|
return null;
|
|
4373
4430
|
try {
|
|
4374
|
-
const value = JSON.parse(
|
|
4431
|
+
const value = JSON.parse(readFileSync7(join7(dataDir, "spawn-breaker", `${sessionId}.json`), "utf8"));
|
|
4375
4432
|
return typeof value === "object" && value !== null && "reason" in value && typeof value.reason === "string" ? value.reason : "capacity limit";
|
|
4376
4433
|
} catch {
|
|
4377
4434
|
return null;
|
|
4378
4435
|
}
|
|
4379
4436
|
}
|
|
4380
4437
|
function atomicWriteJson(targetPath, data) {
|
|
4381
|
-
const tmp =
|
|
4438
|
+
const tmp = join7(dirname4(targetPath), `.tmp-${randomBytes(6).toString("hex")}`);
|
|
4382
4439
|
writeFileSync2(tmp, JSON.stringify(data));
|
|
4383
4440
|
renameSync2(tmp, targetPath);
|
|
4384
4441
|
}
|
|
@@ -4393,7 +4450,7 @@ function isNonEmptyFile(path) {
|
|
|
4393
4450
|
}
|
|
4394
4451
|
function readCount(counterPath) {
|
|
4395
4452
|
try {
|
|
4396
|
-
const parsed = JSON.parse(
|
|
4453
|
+
const parsed = JSON.parse(readFileSync7(counterPath, "utf8"));
|
|
4397
4454
|
return typeof parsed === "object" && parsed !== null && "count" in parsed && typeof parsed.count === "number" && parsed.count >= 0 ? parsed.count : 0;
|
|
4398
4455
|
} catch (error) {
|
|
4399
4456
|
if (error instanceof Error)
|
|
@@ -4403,7 +4460,7 @@ function readCount(counterPath) {
|
|
|
4403
4460
|
}
|
|
4404
4461
|
function readCounts(counterPath) {
|
|
4405
4462
|
try {
|
|
4406
|
-
const parsed = JSON.parse(
|
|
4463
|
+
const parsed = JSON.parse(readFileSync7(counterPath, "utf8"));
|
|
4407
4464
|
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
|
|
4408
4465
|
return {};
|
|
4409
4466
|
const counts = {};
|
|
@@ -4433,11 +4490,15 @@ var LAZYCODEX_SPAWN_ROLES = new Set([
|
|
|
4433
4490
|
"momus",
|
|
4434
4491
|
"plan"
|
|
4435
4492
|
]);
|
|
4436
|
-
function spawnRoleDenial(input) {
|
|
4493
|
+
function spawnRoleDenial(input, registeredRoles = () => new Set) {
|
|
4437
4494
|
const role = typeof input === "object" && input !== null && "agent_type" in input ? input.agent_type : undefined;
|
|
4438
4495
|
if (typeof role === "string" && LAZYCODEX_SPAWN_ROLES.has(role))
|
|
4439
4496
|
return null;
|
|
4440
|
-
|
|
4497
|
+
const registered = typeof role === "string" && role.trim() !== "" ? registeredRoles() : new Set;
|
|
4498
|
+
if (typeof role === "string" && registered.has(role))
|
|
4499
|
+
return null;
|
|
4500
|
+
const known = [...new Set([...LAZYCODEX_SPAWN_ROLES, ...registered])];
|
|
4501
|
+
return `LazyCodex requires an explicit registered agent_type: ${known.join(", ")}. Received ${JSON.stringify(role) ?? "no agent_type"}. Use the matching role and fork_turns: "none" (V2) or fork_context: false (V1), unless full history is deliberately required. The hook cannot see the tool schema; if agent_type is unavailable, stop and report incompatible role routing rather than spawning a generic agent. Describing a role in message does not select its TOML.`;
|
|
4441
4502
|
}
|
|
4442
4503
|
|
|
4443
4504
|
// components/ulw-loop/src/spawn-guard.ts
|
|
@@ -4461,7 +4522,7 @@ function applySpawnGuards(payload, options = {}) {
|
|
|
4461
4522
|
if (payload.hook_event_name !== "PreToolUse" || !SPAWN_TOOL_TOKENS.has(payload.tool_name))
|
|
4462
4523
|
return "";
|
|
4463
4524
|
if (resolveToolkitSurface() === "lazycodex") {
|
|
4464
|
-
const reason = spawnRoleDenial(payload.tool_input);
|
|
4525
|
+
const reason = spawnRoleDenial(payload.tool_input, () => registeredAgentRoles(payload.cwd));
|
|
4465
4526
|
if (reason !== null)
|
|
4466
4527
|
return deny(reason);
|
|
4467
4528
|
}
|
|
@@ -4514,10 +4575,10 @@ async function runSpawnAdmissionRecorderCli(stdin, stdout) {
|
|
|
4514
4575
|
const dataDir = process.env["PLUGIN_DATA"];
|
|
4515
4576
|
if (typeof dataDir !== "string" || typeof payload["session_id"] !== "string")
|
|
4516
4577
|
return;
|
|
4517
|
-
const markerDir =
|
|
4578
|
+
const markerDir = join8(dataDir, "spawn-breaker");
|
|
4518
4579
|
try {
|
|
4519
4580
|
mkdirSync3(markerDir, { recursive: true });
|
|
4520
|
-
atomicWriteJson(
|
|
4581
|
+
atomicWriteJson(join8(markerDir, `${payload["session_id"]}.json`), {
|
|
4521
4582
|
reason: response,
|
|
4522
4583
|
at: new Date().toISOString()
|
|
4523
4584
|
});
|
|
@@ -4546,7 +4607,7 @@ async function runSpawnGuardCli(stdin, stdout) {
|
|
|
4546
4607
|
}
|
|
4547
4608
|
}
|
|
4548
4609
|
function peekFanOutBudget(stateDir) {
|
|
4549
|
-
const counterPath =
|
|
4610
|
+
const counterPath = join8(stateDir, "spawn-count.json");
|
|
4550
4611
|
const count = readCount(counterPath) + 1;
|
|
4551
4612
|
const limit = fanOutLimit();
|
|
4552
4613
|
if (count <= limit)
|
|
@@ -4554,7 +4615,7 @@ function peekFanOutBudget(stateDir) {
|
|
|
4554
4615
|
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.`;
|
|
4555
4616
|
}
|
|
4556
4617
|
function consumeFanOutBudget(stateDir) {
|
|
4557
|
-
const counterPath =
|
|
4618
|
+
const counterPath = join8(stateDir, "spawn-count.json");
|
|
4558
4619
|
const count = readCount(counterPath) + 1;
|
|
4559
4620
|
atomicWriteJson(counterPath, { count });
|
|
4560
4621
|
const limit = fanOutLimit();
|
|
@@ -4569,7 +4630,7 @@ function consumeReviewSpawnBudget(payload, plan, stateDir) {
|
|
|
4569
4630
|
const goal = plan.goals.find((candidate) => candidate.id === plan.activeGoalId) ?? plan.goals.find((candidate) => isFinalRunCompletionCandidate(plan, candidate));
|
|
4570
4631
|
if (goal === undefined)
|
|
4571
4632
|
return null;
|
|
4572
|
-
const counterPath =
|
|
4633
|
+
const counterPath = join8(stateDir, "review-spawn-counts.json");
|
|
4573
4634
|
const limit = reviewSpawnLimit();
|
|
4574
4635
|
const counts = readCounts(counterPath);
|
|
4575
4636
|
const key = `${agentType}:${goal.id}:a${goal.attempt}`;
|
|
@@ -4594,13 +4655,13 @@ function missingGateArtifact(payload, plan) {
|
|
|
4594
4655
|
const attemptDir = ulwLoopAttemptEvidenceDir(goal.id, goal.attempt, scope);
|
|
4595
4656
|
for (const name of requiredArtifacts) {
|
|
4596
4657
|
const relative = `${attemptDir}/${name}`;
|
|
4597
|
-
if (!isNonEmptyFile(
|
|
4658
|
+
if (!isNonEmptyFile(join8(payload.cwd, relative)))
|
|
4598
4659
|
return relative;
|
|
4599
4660
|
}
|
|
4600
4661
|
return null;
|
|
4601
4662
|
}
|
|
4602
4663
|
const manualQa = `.omo/evidence/${goal.id}-manual-qa.md`;
|
|
4603
|
-
return isNonEmptyFile(
|
|
4664
|
+
return isNonEmptyFile(join8(payload.cwd, manualQa)) ? null : manualQa;
|
|
4604
4665
|
}
|
|
4605
4666
|
function isGateReviewerSpawn(toolInput) {
|
|
4606
4667
|
const agentType = reviewAgentType(toolInput);
|
|
@@ -4684,8 +4745,8 @@ function readPlan(repoRoot, sessionId) {
|
|
|
4684
4745
|
}
|
|
4685
4746
|
|
|
4686
4747
|
// components/ulw-loop/src/stop-resume-hook.ts
|
|
4687
|
-
import { existsSync as existsSync8, readFileSync as
|
|
4688
|
-
import { isAbsolute as isAbsolute3, join as
|
|
4748
|
+
import { existsSync as existsSync8, readFileSync as readFileSync8, writeFileSync as writeFileSync3 } from "node:fs";
|
|
4749
|
+
import { isAbsolute as isAbsolute3, join as join9, resolve as resolve6, sep as sep3 } from "node:path";
|
|
4689
4750
|
var RESUME_CAP = 2;
|
|
4690
4751
|
var CONTEXT_PRESSURE_MARKERS2 = [
|
|
4691
4752
|
"context compacted",
|
|
@@ -4753,8 +4814,8 @@ function consumeResumeBudgetLocked(lockPath, stateDir, goalId) {
|
|
|
4753
4814
|
}
|
|
4754
4815
|
function consumeResumeBudget(stateDir, goalId) {
|
|
4755
4816
|
const ledgerLineCount = readLedgerAt(stateDir).length;
|
|
4756
|
-
const counterPath =
|
|
4757
|
-
const stuckPath =
|
|
4817
|
+
const counterPath = resolve6(stateDir, `auto-resume-${goalId}.json`);
|
|
4818
|
+
const stuckPath = resolve6(stateDir, `auto-resume-${goalId}.stuck`);
|
|
4758
4819
|
if (!isInsideDir(stateDir, counterPath) || !isInsideDir(stateDir, stuckPath))
|
|
4759
4820
|
return false;
|
|
4760
4821
|
const previous = readCounter(counterPath);
|
|
@@ -4768,7 +4829,7 @@ function consumeResumeBudget(stateDir, goalId) {
|
|
|
4768
4829
|
return true;
|
|
4769
4830
|
}
|
|
4770
4831
|
function isInsideDir(dir, candidate) {
|
|
4771
|
-
return candidate.startsWith(
|
|
4832
|
+
return candidate.startsWith(resolve6(dir) + sep3);
|
|
4772
4833
|
}
|
|
4773
4834
|
function renderResumeDirective(plan, goal, sessionId) {
|
|
4774
4835
|
const normalized = normalizeUlwLoopSessionId(sessionId);
|
|
@@ -4796,7 +4857,7 @@ function readCounter(counterPath) {
|
|
|
4796
4857
|
try {
|
|
4797
4858
|
if (!existsSync8(counterPath))
|
|
4798
4859
|
return null;
|
|
4799
|
-
const parsed = JSON.parse(
|
|
4860
|
+
const parsed = JSON.parse(readFileSync8(counterPath, "utf8"));
|
|
4800
4861
|
if (typeof parsed["count"] !== "number" || typeof parsed["ledgerLineCount"] !== "number")
|
|
4801
4862
|
return null;
|
|
4802
4863
|
return { count: parsed["count"], ledgerLineCount: parsed["ledgerLineCount"] };
|
|
@@ -4808,7 +4869,7 @@ function readCounter(counterPath) {
|
|
|
4808
4869
|
}
|
|
4809
4870
|
function boulderContinuationWillFire(cwd, sessionId) {
|
|
4810
4871
|
try {
|
|
4811
|
-
const raw = JSON.parse(
|
|
4872
|
+
const raw = JSON.parse(readFileSync8(join9(cwd, ".omo", "boulder.json"), "utf8"));
|
|
4812
4873
|
const works = raw["works"];
|
|
4813
4874
|
const entries = typeof works === "object" && works !== null ? Object.values(works) : [raw];
|
|
4814
4875
|
return entries.some((work) => {
|
|
@@ -4827,7 +4888,7 @@ function boulderContinuationWillFire(cwd, sessionId) {
|
|
|
4827
4888
|
}
|
|
4828
4889
|
function transcriptShowsContextPressure(transcriptPath) {
|
|
4829
4890
|
try {
|
|
4830
|
-
const transcript =
|
|
4891
|
+
const transcript = readFileSync8(transcriptPath, "utf8").toLowerCase();
|
|
4831
4892
|
return CONTEXT_PRESSURE_MARKERS2.some((marker) => transcript.includes(marker));
|
|
4832
4893
|
} catch (error) {
|
|
4833
4894
|
if (error instanceof Error)
|
|
@@ -4839,12 +4900,12 @@ function boulderPlanHasChecklist(cwd, entry) {
|
|
|
4839
4900
|
const activePlan = entry["active_plan"];
|
|
4840
4901
|
if (typeof activePlan !== "string" || activePlan.trim().length === 0)
|
|
4841
4902
|
return false;
|
|
4842
|
-
const planPath = isAbsolute3(activePlan) ? activePlan :
|
|
4903
|
+
const planPath = isAbsolute3(activePlan) ? activePlan : join9(cwd, activePlan);
|
|
4843
4904
|
const worktree = entry["worktree_path"];
|
|
4844
|
-
const candidates = typeof worktree === "string" && worktree.trim().length > 0 && !isAbsolute3(activePlan) ? [
|
|
4905
|
+
const candidates = typeof worktree === "string" && worktree.trim().length > 0 && !isAbsolute3(activePlan) ? [join9(isAbsolute3(worktree) ? worktree : join9(cwd, worktree), activePlan), planPath] : [planPath];
|
|
4845
4906
|
for (const candidate of candidates) {
|
|
4846
4907
|
try {
|
|
4847
|
-
return
|
|
4908
|
+
return readFileSync8(candidate, "utf8").split(/\r?\n/).some((line) => line.startsWith("- [ ] ") || line.startsWith("- [x] ") || line.startsWith("- [X] "));
|
|
4848
4909
|
} catch (error) {
|
|
4849
4910
|
if (!(error instanceof Error))
|
|
4850
4911
|
throw error;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function registeredAgentRoles(cwd: string, env?: NodeJS.ProcessEnv): ReadonlySet<string>;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { readdirSync, readFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
4
|
+
// Codex registers a role from a standalone `agents/*.toml` (keyed by its `name`, else the
|
|
5
|
+
// file stem) or from an `[agents.<name>]` table in config.toml. It reads both under
|
|
6
|
+
// CODEX_HOME and under a project's `.codex/`. Codex still validates the role itself (trust,
|
|
7
|
+
// required fields); this only answers "does a role file declare this name".
|
|
8
|
+
const NAME_LINE = /^\s*name\s*=\s*"([^"\n]+)"\s*(?:#.*)?$/m;
|
|
9
|
+
const AGENT_TABLE = /^\s*\[agents\.(?:"([^"\n]+)"|([A-Za-z0-9_-]+))\]\s*(?:#.*)?$/gm;
|
|
10
|
+
function readText(path) {
|
|
11
|
+
try {
|
|
12
|
+
return readFileSync(path, "utf8");
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function standaloneRoleNames(agentsDir) {
|
|
19
|
+
let entries;
|
|
20
|
+
try {
|
|
21
|
+
entries = readdirSync(agentsDir);
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return [];
|
|
25
|
+
}
|
|
26
|
+
const names = [];
|
|
27
|
+
for (const entry of entries) {
|
|
28
|
+
if (!entry.endsWith(".toml"))
|
|
29
|
+
continue;
|
|
30
|
+
const text = readText(join(agentsDir, entry));
|
|
31
|
+
if (text === null)
|
|
32
|
+
continue;
|
|
33
|
+
names.push(text.match(NAME_LINE)?.[1] ?? basename(entry, ".toml"));
|
|
34
|
+
}
|
|
35
|
+
return names;
|
|
36
|
+
}
|
|
37
|
+
function configTableRoleNames(configPath) {
|
|
38
|
+
const text = readText(configPath);
|
|
39
|
+
if (text === null)
|
|
40
|
+
return [];
|
|
41
|
+
return [...text.matchAll(AGENT_TABLE)].map((match) => match[1] ?? match[2] ?? "").filter(Boolean);
|
|
42
|
+
}
|
|
43
|
+
function codexDirs(cwd, env) {
|
|
44
|
+
const dirs = [resolve(env["CODEX_HOME"]?.trim() || join(homedir(), ".codex"))];
|
|
45
|
+
for (let dir = resolve(cwd);; dir = dirname(dir)) {
|
|
46
|
+
dirs.push(join(dir, ".codex"));
|
|
47
|
+
if (dirname(dir) === dir)
|
|
48
|
+
break;
|
|
49
|
+
}
|
|
50
|
+
return [...new Set(dirs)];
|
|
51
|
+
}
|
|
52
|
+
export function registeredAgentRoles(cwd, env = process.env) {
|
|
53
|
+
const names = new Set();
|
|
54
|
+
for (const dir of codexDirs(cwd, env)) {
|
|
55
|
+
for (const name of standaloneRoleNames(join(dir, "agents")))
|
|
56
|
+
names.add(name);
|
|
57
|
+
for (const name of configTableRoleNames(join(dir, "config.toml")))
|
|
58
|
+
names.add(name);
|
|
59
|
+
}
|
|
60
|
+
return names;
|
|
61
|
+
}
|
|
@@ -4,6 +4,7 @@ import { parsePreToolUsePayload } from "./codex-hook.js";
|
|
|
4
4
|
import { isFinalRunCompletionCandidate } from "./goal-status.js";
|
|
5
5
|
import { ulwLoopAttemptEvidenceDir, ulwLoopDir, ulwLoopStateLockPath } from "./paths.js";
|
|
6
6
|
import { readUlwLoopPlanSync } from "./plan-io.js";
|
|
7
|
+
import { registeredAgentRoles } from "./registered-agent-roles.js";
|
|
7
8
|
import { atomicWriteJson, isNonEmptyFile, readAdmissionBreaker, readCount, readCounts } from "./spawn-budget-io.js";
|
|
8
9
|
import { spawnRoleDenial } from "./spawn-role-guard.js";
|
|
9
10
|
import { isStateLockTimeout, withStateLockSync } from "./state-lock.js";
|
|
@@ -31,7 +32,7 @@ export function applySpawnGuards(payload, options = {}) {
|
|
|
31
32
|
if (payload.hook_event_name !== "PreToolUse" || !SPAWN_TOOL_TOKENS.has(payload.tool_name))
|
|
32
33
|
return "";
|
|
33
34
|
if (resolveToolkitSurface() === "lazycodex") {
|
|
34
|
-
const reason = spawnRoleDenial(payload.tool_input);
|
|
35
|
+
const reason = spawnRoleDenial(payload.tool_input, () => registeredAgentRoles(payload.cwd));
|
|
35
36
|
if (reason !== null)
|
|
36
37
|
return deny(reason);
|
|
37
38
|
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export declare const LAZYCODEX_SPAWN_ROLES: ReadonlySet<string>;
|
|
2
|
-
export declare function spawnRoleDenial(input: unknown): string | null;
|
|
2
|
+
export declare function spawnRoleDenial(input: unknown, registeredRoles?: () => ReadonlySet<string>): string | null;
|
|
@@ -14,9 +14,13 @@ export const LAZYCODEX_SPAWN_ROLES = new Set([
|
|
|
14
14
|
"momus",
|
|
15
15
|
"plan",
|
|
16
16
|
]);
|
|
17
|
-
export function spawnRoleDenial(input) {
|
|
17
|
+
export function spawnRoleDenial(input, registeredRoles = () => new Set()) {
|
|
18
18
|
const role = typeof input === "object" && input !== null && "agent_type" in input ? input.agent_type : undefined;
|
|
19
19
|
if (typeof role === "string" && LAZYCODEX_SPAWN_ROLES.has(role))
|
|
20
20
|
return null;
|
|
21
|
-
|
|
21
|
+
const registered = typeof role === "string" && role.trim() !== "" ? registeredRoles() : new Set();
|
|
22
|
+
if (typeof role === "string" && registered.has(role))
|
|
23
|
+
return null;
|
|
24
|
+
const known = [...new Set([...LAZYCODEX_SPAWN_ROLES, ...registered])];
|
|
25
|
+
return `LazyCodex requires an explicit registered agent_type: ${known.join(", ")}. Received ${JSON.stringify(role) ?? "no agent_type"}. Use the matching role and fork_turns: "none" (V2) or fork_context: false (V1), unless full history is deliberately required. The hook cannot see the tool schema; if agent_type is unavailable, stop and report incompatible role routing rather than spawning a generic agent. Describing a role in message does not select its TOML.`;
|
|
22
26
|
}
|
|
@@ -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 5.1.
|
|
10
|
+
"statusMessage": "(OmO 5.1.14) Checking Ulw-Loop Steering"
|
|
11
11
|
}
|
|
12
12
|
]
|
|
13
13
|
}
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"type": "command",
|
|
21
21
|
"command": "node \"${PLUGIN_ROOT}/dist/cli.js\" hook post-tool-use-spawn",
|
|
22
22
|
"timeout": 5,
|
|
23
|
-
"statusMessage": "(OmO 5.1.
|
|
23
|
+
"statusMessage": "(OmO 5.1.14) Recording Spawn Admission"
|
|
24
24
|
}
|
|
25
25
|
]
|
|
26
26
|
}
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"type": "command",
|
|
34
34
|
"command": "node \"${PLUGIN_ROOT}/dist/cli.js\" hook pre-tool-use",
|
|
35
35
|
"timeout": 5,
|
|
36
|
-
"statusMessage": "(OmO 5.1.
|
|
36
|
+
"statusMessage": "(OmO 5.1.14) Enforcing Unlimited Ulw-Loop Budget"
|
|
37
37
|
}
|
|
38
38
|
]
|
|
39
39
|
},
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"type": "command",
|
|
45
45
|
"command": "node \"${PLUGIN_ROOT}/dist/cli.js\" hook pre-tool-use-spawn",
|
|
46
46
|
"timeout": 5,
|
|
47
|
-
"statusMessage": "(OmO 5.1.
|
|
47
|
+
"statusMessage": "(OmO 5.1.14) Guarding Ulw-Loop Spawns"
|
|
48
48
|
}
|
|
49
49
|
]
|
|
50
50
|
}
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
"type": "command",
|
|
57
57
|
"command": "node \"${PLUGIN_ROOT}/dist/cli.js\" hook stop",
|
|
58
58
|
"timeout": 10,
|
|
59
|
-
"statusMessage": "(OmO 5.1.
|
|
59
|
+
"statusMessage": "(OmO 5.1.14) Checking Ulw-Loop Resume"
|
|
60
60
|
}
|
|
61
61
|
]
|
|
62
62
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@code-yeongyu/codex-ulw-loop",
|
|
3
|
-
"version": "5.1.
|
|
3
|
+
"version": "5.1.14",
|
|
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",
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { readdirSync, readFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
4
|
+
|
|
5
|
+
// Codex registers a role from a standalone `agents/*.toml` (keyed by its `name`, else the
|
|
6
|
+
// file stem) or from an `[agents.<name>]` table in config.toml. It reads both under
|
|
7
|
+
// CODEX_HOME and under a project's `.codex/`. Codex still validates the role itself (trust,
|
|
8
|
+
// required fields); this only answers "does a role file declare this name".
|
|
9
|
+
|
|
10
|
+
const NAME_LINE = /^\s*name\s*=\s*"([^"\n]+)"\s*(?:#.*)?$/m;
|
|
11
|
+
const AGENT_TABLE = /^\s*\[agents\.(?:"([^"\n]+)"|([A-Za-z0-9_-]+))\]\s*(?:#.*)?$/gm;
|
|
12
|
+
|
|
13
|
+
function readText(path: string): string | null {
|
|
14
|
+
try {
|
|
15
|
+
return readFileSync(path, "utf8");
|
|
16
|
+
} catch {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function standaloneRoleNames(agentsDir: string): string[] {
|
|
22
|
+
let entries: string[];
|
|
23
|
+
try {
|
|
24
|
+
entries = readdirSync(agentsDir);
|
|
25
|
+
} catch {
|
|
26
|
+
return [];
|
|
27
|
+
}
|
|
28
|
+
const names: string[] = [];
|
|
29
|
+
for (const entry of entries) {
|
|
30
|
+
if (!entry.endsWith(".toml")) continue;
|
|
31
|
+
const text = readText(join(agentsDir, entry));
|
|
32
|
+
if (text === null) continue;
|
|
33
|
+
names.push(text.match(NAME_LINE)?.[1] ?? basename(entry, ".toml"));
|
|
34
|
+
}
|
|
35
|
+
return names;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function configTableRoleNames(configPath: string): string[] {
|
|
39
|
+
const text = readText(configPath);
|
|
40
|
+
if (text === null) return [];
|
|
41
|
+
return [...text.matchAll(AGENT_TABLE)].map((match) => match[1] ?? match[2] ?? "").filter(Boolean);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function codexDirs(cwd: string, env: NodeJS.ProcessEnv): string[] {
|
|
45
|
+
const dirs = [resolve(env["CODEX_HOME"]?.trim() || join(homedir(), ".codex"))];
|
|
46
|
+
for (let dir = resolve(cwd); ; dir = dirname(dir)) {
|
|
47
|
+
dirs.push(join(dir, ".codex"));
|
|
48
|
+
if (dirname(dir) === dir) break;
|
|
49
|
+
}
|
|
50
|
+
return [...new Set(dirs)];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function registeredAgentRoles(cwd: string, env: NodeJS.ProcessEnv = process.env): ReadonlySet<string> {
|
|
54
|
+
const names = new Set<string>();
|
|
55
|
+
for (const dir of codexDirs(cwd, env)) {
|
|
56
|
+
for (const name of standaloneRoleNames(join(dir, "agents"))) names.add(name);
|
|
57
|
+
for (const name of configTableRoleNames(join(dir, "config.toml"))) names.add(name);
|
|
58
|
+
}
|
|
59
|
+
return names;
|
|
60
|
+
}
|
|
@@ -6,6 +6,7 @@ import { parsePreToolUsePayload } from "./codex-hook.js";
|
|
|
6
6
|
import { isFinalRunCompletionCandidate } from "./goal-status.js";
|
|
7
7
|
import { ulwLoopAttemptEvidenceDir, ulwLoopDir, ulwLoopStateLockPath } from "./paths.js";
|
|
8
8
|
import { readUlwLoopPlanSync } from "./plan-io.js";
|
|
9
|
+
import { registeredAgentRoles } from "./registered-agent-roles.js";
|
|
9
10
|
import { atomicWriteJson, isNonEmptyFile, readAdmissionBreaker, readCount, readCounts } from "./spawn-budget-io.js";
|
|
10
11
|
import { spawnRoleDenial } from "./spawn-role-guard.js";
|
|
11
12
|
import { isStateLockTimeout, type StateLockOptions, withStateLockSync } from "./state-lock.js";
|
|
@@ -47,7 +48,7 @@ export interface SpawnGuardOptions {
|
|
|
47
48
|
export function applySpawnGuards(payload: PreToolUsePayload, options: SpawnGuardOptions = {}): string {
|
|
48
49
|
if (payload.hook_event_name !== "PreToolUse" || !SPAWN_TOOL_TOKENS.has(payload.tool_name)) return "";
|
|
49
50
|
if (resolveToolkitSurface() === "lazycodex") {
|
|
50
|
-
const reason = spawnRoleDenial(payload.tool_input);
|
|
51
|
+
const reason = spawnRoleDenial(payload.tool_input, () => registeredAgentRoles(payload.cwd));
|
|
51
52
|
if (reason !== null) return deny(reason);
|
|
52
53
|
}
|
|
53
54
|
return applySpawnBudgetGuards(payload, options);
|
|
@@ -15,8 +15,14 @@ export const LAZYCODEX_SPAWN_ROLES: ReadonlySet<string> = new Set([
|
|
|
15
15
|
"plan",
|
|
16
16
|
]);
|
|
17
17
|
|
|
18
|
-
export function spawnRoleDenial(
|
|
18
|
+
export function spawnRoleDenial(
|
|
19
|
+
input: unknown,
|
|
20
|
+
registeredRoles: () => ReadonlySet<string> = () => new Set(),
|
|
21
|
+
): string | null {
|
|
19
22
|
const role = typeof input === "object" && input !== null && "agent_type" in input ? input.agent_type : undefined;
|
|
20
23
|
if (typeof role === "string" && LAZYCODEX_SPAWN_ROLES.has(role)) return null;
|
|
21
|
-
|
|
24
|
+
const registered = typeof role === "string" && role.trim() !== "" ? registeredRoles() : new Set<string>();
|
|
25
|
+
if (typeof role === "string" && registered.has(role)) return null;
|
|
26
|
+
const known = [...new Set([...LAZYCODEX_SPAWN_ROLES, ...registered])];
|
|
27
|
+
return `LazyCodex requires an explicit registered agent_type: ${known.join(", ")}. Received ${JSON.stringify(role) ?? "no agent_type"}. Use the matching role and fork_turns: "none" (V2) or fork_context: false (V1), unless full history is deliberately required. The hook cannot see the tool schema; if agent_type is unavailable, stop and report incompatible role routing rather than spawning a generic agent. Describing a role in message does not select its TOML.`;
|
|
22
28
|
}
|
|
@@ -11,6 +11,7 @@ beforeEach(async () => {
|
|
|
11
11
|
cwd = await mkdtemp(join(tmpdir(), "spawn-role-matrix-"));
|
|
12
12
|
vi.stubEnv("OMO_AGENT_TOOLKIT_SURFACE", "lazycodex");
|
|
13
13
|
vi.stubEnv("PLUGIN_DATA", join(cwd, "data"));
|
|
14
|
+
vi.stubEnv("CODEX_HOME", join(cwd, "codex-home"));
|
|
14
15
|
});
|
|
15
16
|
afterEach(async () => {
|
|
16
17
|
vi.unstubAllEnvs();
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
5
|
+
import { applySpawnGuards } from "../src/spawn-guard.js";
|
|
6
|
+
|
|
7
|
+
let root: string;
|
|
8
|
+
let project: string;
|
|
9
|
+
let codexHome: string;
|
|
10
|
+
|
|
11
|
+
beforeEach(async () => {
|
|
12
|
+
root = await mkdtemp(join(tmpdir(), "spawn-role-registered-"));
|
|
13
|
+
project = join(root, "project");
|
|
14
|
+
codexHome = join(root, "codex-home");
|
|
15
|
+
await mkdir(join(project, "src", "deep"), { recursive: true });
|
|
16
|
+
vi.stubEnv("OMO_AGENT_TOOLKIT_SURFACE", "lazycodex");
|
|
17
|
+
vi.stubEnv("PLUGIN_DATA", join(root, "data"));
|
|
18
|
+
vi.stubEnv("CODEX_HOME", codexHome);
|
|
19
|
+
});
|
|
20
|
+
afterEach(async () => {
|
|
21
|
+
vi.unstubAllEnvs();
|
|
22
|
+
await rm(root, { recursive: true, force: true });
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
async function role(dir: string, file: string, name?: string): Promise<void> {
|
|
26
|
+
await mkdir(dir, { recursive: true });
|
|
27
|
+
const header = name === undefined ? "" : `name = "${name}"\n`;
|
|
28
|
+
await writeFile(join(dir, file), `${header}description = "custom role"\ndeveloper_instructions = "Do the work."\n`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function spawn(cwd: string, input: Record<string, unknown>): string {
|
|
32
|
+
return applySpawnGuards({
|
|
33
|
+
cwd,
|
|
34
|
+
hook_event_name: "PreToolUse",
|
|
35
|
+
model: "test",
|
|
36
|
+
permission_mode: "default",
|
|
37
|
+
session_id: "registered",
|
|
38
|
+
tool_name: "spawn_agent",
|
|
39
|
+
tool_input: {
|
|
40
|
+
task_name: "check",
|
|
41
|
+
message: "Perform the assigned read-only analysis.",
|
|
42
|
+
fork_turns: "none",
|
|
43
|
+
...input,
|
|
44
|
+
},
|
|
45
|
+
tool_use_id: "call",
|
|
46
|
+
transcript_path: null,
|
|
47
|
+
turn_id: "turn",
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function denial(output: string): string | null {
|
|
52
|
+
if (output === "") return null;
|
|
53
|
+
const parsed: { hookSpecificOutput: { permissionDecision: string; permissionDecisionReason: string } } =
|
|
54
|
+
JSON.parse(output);
|
|
55
|
+
expect(parsed.hookSpecificOutput.permissionDecision).toBe("deny");
|
|
56
|
+
return parsed.hookSpecificOutput.permissionDecisionReason;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
describe("#given custom roles registered outside the bundle", () => {
|
|
60
|
+
describe("#when a project registers them in .codex/agents", () => {
|
|
61
|
+
it("#then each registered role spawns, including from a nested working directory", async () => {
|
|
62
|
+
const agents = join(project, ".codex", "agents");
|
|
63
|
+
for (const name of ["cashflow_planner", "debt_planner", "wealth_planner", "finance_reviewer"])
|
|
64
|
+
await role(agents, `${name}.toml`, name);
|
|
65
|
+
|
|
66
|
+
for (const name of ["cashflow_planner", "debt_planner", "wealth_planner", "finance_reviewer"]) {
|
|
67
|
+
expect(denial(spawn(project, { agent_type: name }))).toBeNull();
|
|
68
|
+
expect(denial(spawn(join(project, "src", "deep"), { agent_type: name }))).toBeNull();
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("#then the role is keyed by its declared name, not the file name", async () => {
|
|
73
|
+
await role(join(project, ".codex", "agents"), "planner-file.toml", "wealth_planner");
|
|
74
|
+
|
|
75
|
+
expect(denial(spawn(project, { agent_type: "wealth_planner" }))).toBeNull();
|
|
76
|
+
expect(denial(spawn(project, { agent_type: "planner-file" }))).toContain('Received "planner-file"');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("#then a role file without a name field registers under its file name", async () => {
|
|
80
|
+
await role(join(project, ".codex", "agents"), "cashflow_planner.toml");
|
|
81
|
+
|
|
82
|
+
expect(denial(spawn(project, { agent_type: "cashflow_planner" }))).toBeNull();
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe("#when they are registered under CODEX_HOME", () => {
|
|
87
|
+
it("#then a standalone agents/*.toml role and a config.toml [agents.<name>] role both spawn", async () => {
|
|
88
|
+
await role(join(codexHome, "agents"), "finance_reviewer.toml", "finance_reviewer");
|
|
89
|
+
await writeFile(
|
|
90
|
+
join(codexHome, "config.toml"),
|
|
91
|
+
'[agents]\nmax_threads = 4\n\n[agents.debt_planner]\nconfig_file = "./agents/debt.toml"\n',
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
expect(denial(spawn(project, { agent_type: "finance_reviewer" }))).toBeNull();
|
|
95
|
+
expect(denial(spawn(project, { agent_type: "debt_planner" }))).toBeNull();
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
describe("#given the guard's purpose of never spawning an unregistered or generic agent", () => {
|
|
101
|
+
it("#when the role has no role file anywhere #then it is still denied and the message lists the registered roles", async () => {
|
|
102
|
+
await role(join(project, ".codex", "agents"), "cashflow_planner.toml", "cashflow_planner");
|
|
103
|
+
|
|
104
|
+
const reason = denial(spawn(project, { agent_type: "worker" }));
|
|
105
|
+
|
|
106
|
+
expect(reason).toContain('Received "worker"');
|
|
107
|
+
expect(reason).toContain("cashflow_planner");
|
|
108
|
+
expect(reason).toContain("lazycodex-worker-medium");
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("#when agent_type is missing #then it is denied even with custom roles registered", async () => {
|
|
112
|
+
await role(join(project, ".codex", "agents"), "cashflow_planner.toml", "cashflow_planner");
|
|
113
|
+
|
|
114
|
+
expect(denial(spawn(project, {}))).toContain("Received no agent_type");
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("#when a role is registered only in a different project #then this project still denies it", async () => {
|
|
118
|
+
const other = join(root, "other-project");
|
|
119
|
+
await role(join(other, ".codex", "agents"), "cashflow_planner.toml", "cashflow_planner");
|
|
120
|
+
|
|
121
|
+
expect(denial(spawn(project, { agent_type: "cashflow_planner" }))).toContain('Received "cashflow_planner"');
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("#when the agents directory is unreadable or absent #then bundled roles still spawn", async () => {
|
|
125
|
+
expect(denial(spawn(project, { agent_type: "lazycodex-worker-medium" }))).toBeNull();
|
|
126
|
+
expect(denial(spawn(project, { agent_type: "explorer" }))).toBeNull();
|
|
127
|
+
});
|
|
128
|
+
});
|