limn-review 0.1.151 → 0.1.153
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.md +9 -6
- package/dist/ai/auto-install.js +81 -0
- package/dist/ai/check-tool.js +6 -0
- package/dist/ai/codex/config.js +1 -0
- package/dist/ai/codex/hook-rule.js +31 -4
- package/dist/ai/codex/kinds/story-build.js +2 -0
- package/dist/ai/codex/workspace-mcp-main.js +2 -2
- package/dist/ai/codex/workspace-mcp.js +42 -10
- package/dist/ai/credentials.js +1 -1
- package/dist/ai/effort.js +3 -2
- package/dist/ai/provider-setting.js +5 -0
- package/dist/ai/provider.js +1 -0
- package/dist/ai/providers/claude.js +41 -17
- package/dist/ai/providers/codex.js +13 -3
- package/dist/ai/result-schemas.js +9 -0
- package/dist/ai/runtime.js +81 -10
- package/dist/api/plugins/rate-limit.js +59 -15
- package/dist/api/routes/ai-usage-report.js +49 -0
- package/dist/api/routes/conflicts.js +16 -2
- package/dist/api/routes/dependency-merge.js +67 -1
- package/dist/api/routes/me.js +8 -5
- package/dist/api/routes/storage.js +9 -0
- package/dist/app.js +9 -0
- package/dist/auth/account.js +1 -7
- package/dist/cli.js +17 -2
- package/dist/coding/agent.js +252 -42
- package/dist/coding/ai-fix/checks-preview.js +58 -0
- package/dist/coding/ai-fix/manager.js +51 -9
- package/dist/coding/ai-fix/persist.js +31 -0
- package/dist/coding/ai-fix/prompts.js +41 -5
- package/dist/coding/ai-fix/review-seed.js +20 -6
- package/dist/coding/ai-fix/routes.js +67 -11
- package/dist/coding/ai-resolve/run.js +2 -1
- package/dist/coding/checks/deny-scan.js +75 -0
- package/dist/coding/checks/discover.js +541 -0
- package/dist/coding/checks/docker.js +38 -0
- package/dist/coding/checks/env.js +88 -0
- package/dist/coding/checks/hooks.js +91 -0
- package/dist/coding/checks/index.js +8 -0
- package/dist/coding/checks/local.js +40 -0
- package/dist/coding/checks/sandbox.js +206 -0
- package/dist/coding/checks/session.js +518 -0
- package/dist/coding/checks/spawn.js +123 -0
- package/dist/coding/checks/spec.js +140 -0
- package/dist/coding/checks/types.js +6 -0
- package/dist/coding/git-ops.js +75 -2
- package/dist/coding/git.js +5 -2
- package/dist/coding/merge.js +2 -1
- package/dist/coding/story-build/base-refs.js +120 -0
- package/dist/coding/story-build/build-prompts.js +178 -0
- package/dist/coding/story-build/build.js +332 -0
- package/dist/coding/story-build/candidates.js +273 -0
- package/dist/coding/story-build/checkouts.js +66 -0
- package/dist/coding/story-build/coverage.js +167 -0
- package/dist/coding/story-build/docs-runner.js +178 -0
- package/dist/coding/story-build/docs.js +333 -0
- package/dist/coding/story-build/inputs.js +119 -0
- package/dist/coding/story-build/limits.js +22 -0
- package/dist/coding/story-build/manager.js +999 -0
- package/dist/coding/story-build/persist.js +280 -0
- package/dist/coding/story-build/plan-agent.js +190 -0
- package/dist/coding/story-build/plan.js +338 -0
- package/dist/coding/story-build/prompts.js +147 -0
- package/dist/coding/story-build/push.js +326 -0
- package/dist/coding/story-build/routes-build.js +151 -0
- package/dist/coding/story-build/routes-docs.js +113 -0
- package/dist/coding/story-build/routes.js +295 -0
- package/dist/coding/story-build/schema.js +53 -0
- package/dist/coding/story-build/seams.js +29 -0
- package/dist/config.js +24 -3
- package/dist/db/ai-usage-report.js +607 -0
- package/dist/db/blast-radius-query.js +5 -29
- package/dist/db/code-loc.js +4 -0
- package/dist/db/erase-account.js +25 -1
- package/dist/db/export-account.js +74 -0
- package/dist/db/migrations/0146_worktree_ledger.sql +30 -0
- package/dist/db/migrations/0147_dependency_automation.sql +41 -0
- package/dist/db/migrations/0148_ai_fix_checks.sql +5 -0
- package/dist/db/migrations/0149_story_builds.sql +116 -0
- package/dist/db/migrations/meta/_journal.json +28 -0
- package/dist/db/queries.js +41 -3
- package/dist/db/retention.js +5 -0
- package/dist/db/schema.sqlite.js +235 -5
- package/dist/db/sprint-builds.js +72 -0
- package/dist/db/sprint-graph-fold.js +12 -3
- package/dist/db/sprint-graph.js +5 -0
- package/dist/db/sprint-ticket-checks.js +5 -1
- package/dist/db/triage.js +6 -0
- package/dist/github/mutations.js +27 -0
- package/dist/github/repo-files.js +26 -0
- package/dist/index.js +17 -4
- package/dist/merge/dependency-automation.js +662 -0
- package/dist/merge/dependency-policy.js +22 -1
- package/dist/merge/dependency-rules.js +246 -0
- package/dist/platform/annotations/schema.sqlite.js +3 -3
- package/dist/platform/bots/benchmark-routes.js +1 -1
- package/dist/platform/context.js +0 -3
- package/dist/platform/index.js +3 -5
- package/dist/platform/llm/gate.js +2 -2
- package/dist/platform/retention.js +2 -2
- package/dist/platform/slack/pending-blocks.js +31 -16
- package/dist/platform/slack/report.js +34 -28
- package/dist/platform/slack/send.js +8 -1
- package/dist/platform/slack/signals.js +25 -9
- package/dist/platform/slack/tones.js +89 -0
- package/dist/reference-docs/store.js +24 -3
- package/dist/reference-docs/wiki/build.js +1 -1
- package/dist/reference-docs/wiki/confluence.js +141 -27
- package/dist/reference-docs/wiki/credentials.js +12 -0
- package/dist/reference-docs/wiki/github.js +444 -0
- package/dist/reference-docs/wiki/notion.js +10 -0
- package/dist/reference-docs/wiki/register.js +3 -0
- package/dist/reference-docs/wiki/routes.js +251 -8
- package/dist/reference-docs/wiki/source.js +20 -2
- package/dist/review/agent-context.js +2 -0
- package/dist/review/agentic.js +16 -0
- package/dist/review/ci-review/manager.js +2 -1
- package/dist/review/claude-review/chat.js +3 -2
- package/dist/review/claude-review/manager.js +63 -9
- package/dist/review/claude-review/persist.js +26 -5
- package/dist/review/claude-review/replies-check.js +2 -1
- package/dist/review/claude-review/thread-check.js +2 -1
- package/dist/review/clone-hygiene.js +18 -59
- package/dist/review/clone-manager.js +46 -4
- package/dist/review/diagrams/agent.js +13 -6
- package/dist/review/diagrams/fingerprint.js +4 -2
- package/dist/review/diagrams/manager.js +56 -18
- package/dist/review/diagrams/persist.js +41 -16
- package/dist/review/diagrams/precheck.js +100 -0
- package/dist/review/diagrams/prompts.js +30 -24
- package/dist/review/diagrams/routes.js +6 -2
- package/dist/review/diagrams/schema.js +30 -23
- package/dist/review/diagrams/validate.js +155 -145
- package/dist/review/llm.js +1 -1
- package/dist/review/story-docs/agent.js +50 -12
- package/dist/review/story-docs/build-prompts.js +153 -0
- package/dist/review/story-docs/build-validate.js +162 -0
- package/dist/review/story-docs/schema.js +32 -1
- package/dist/review/ticket-review/manager.js +4 -3
- package/dist/review/worktree-gc-policy.js +111 -0
- package/dist/review/worktree-gc.js +576 -0
- package/dist/review/worktree-ledger.js +67 -0
- package/dist/review/worktree-owner.js +11 -0
- package/dist/review/write-deny.js +170 -0
- package/dist/shared/ai-usage-report.js +93 -0
- package/dist/shared/blast-radius.js +0 -3
- package/dist/shared/dependency-merge.js +20 -0
- package/dist/shared/index.js +3 -0
- package/dist/shared/reference-docs-wiki.js +77 -9
- package/dist/shared/repo-status.js +97 -0
- package/dist/shared/sequence-diagrams.js +81 -3
- package/dist/shared/story-build.js +215 -0
- package/dist/shared/types.js +2 -0
- package/dist/sync/hydrate-detail.js +1 -1
- package/dist/sync/scheduler.js +9 -3
- package/package.json +3 -3
- package/public/assets/AiFixTab-DcXh_zAo.js +3 -0
- package/public/assets/ClaudeReviewTab-DLH3LipL.js +20 -0
- package/public/assets/{PrBotBehaviourTab-BamwN7zI.js → PrBotBehaviourTab-BzsRHxmq.js} +1 -1
- package/public/assets/index-DoDcUqdp.js +88 -0
- package/public/assets/index-iiO08SBA.css +1 -0
- package/public/index.html +2 -2
- package/dist/platform/annotations/prompts.js +0 -84
- package/dist/platform/annotations/routes.js +0 -86
- package/dist/platform/annotations/runner.js +0 -238
- package/dist/platform/annotations/targets.js +0 -203
- package/public/assets/AiFixTab-CT7boeou.js +0 -3
- package/public/assets/ClaudeReviewTab-3dS4-wd-.js +0 -20
- package/public/assets/index-DZWulOIE.js +0 -85
- package/public/assets/index-YhOJvNKA.css +0 -1
package/README.md
CHANGED
|
@@ -44,10 +44,12 @@ there is nothing to set up. An Anthropic API key is the alternative: set
|
|
|
44
44
|
`ANTHROPIC_API_KEY`. Limn stores no key and charges nothing for it. They can
|
|
45
45
|
run on Codex instead (below).
|
|
46
46
|
|
|
47
|
-
- **
|
|
48
|
-
|
|
49
|
-
`~/.limn/ai-runtime`, at the exact versions this release was
|
|
50
|
-
package itself stays small
|
|
47
|
+
- **Claude and Codex work out of the box.** The first time Limn starts, it
|
|
48
|
+
downloads both in the background (Claude about 110 MB, Codex about 360 MB
|
|
49
|
+
unpacked) into `~/.limn/ai-runtime`, at the exact versions this release was
|
|
50
|
+
tested with. The package itself stays small. Signing in is the only step.
|
|
51
|
+
`limn ai install codex` runs the same download by hand;
|
|
52
|
+
`LIMN_AI_AUTO_INSTALL=false` skips the automatic one.
|
|
51
53
|
- Set `LIMN_CLAUDE_PATH` to the `claude` you already have to use it instead of
|
|
52
54
|
the bundled copy (the download then skips it).
|
|
53
55
|
- The reviewer reads code. It cannot edit files, run commands or reach the web.
|
|
@@ -62,12 +64,13 @@ run on Codex instead (below).
|
|
|
62
64
|
Every AI feature can run on Codex, on your own ChatGPT plan. One provider runs
|
|
63
65
|
everything at a time.
|
|
64
66
|
|
|
67
|
+
Codex comes with Limn. Sign in from **Settings → AI**, or:
|
|
68
|
+
|
|
65
69
|
```bash
|
|
66
|
-
limn ai install codex # adds the Codex CLI to the AI download
|
|
67
70
|
limn ai login codex # sign in with ChatGPT (add --device-auth on a headless machine)
|
|
68
71
|
```
|
|
69
72
|
|
|
70
|
-
|
|
73
|
+
Limn keeps its own
|
|
71
74
|
Codex sign-in in `~/.limn/codex`; your `~/.codex` is not read or changed. Codex
|
|
72
75
|
with an OpenAI API key is not available yet.
|
|
73
76
|
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// THE FIRST-BOOT AI DOWNLOAD — Claude and Codex come with Limn, without a click.
|
|
2
|
+
//
|
|
3
|
+
// The SDKs and the Codex CLI stay OUT of the npm manifest (a fast `npx limn-review`; the guardrails in
|
|
4
|
+
// scripts/build-release.mjs are unchanged), so on a boot that finds either runtime missing, stale or
|
|
5
|
+
// without its native program, Limn downloads what is missing in the background — the same
|
|
6
|
+
// single-flight `installAiRuntime` that `limn ai install codex` and the SPA's Retry run. ⚠ A Claude
|
|
7
|
+
// that is already ready is never fetched again or swapped (`['codex']` alone: the existing install
|
|
8
|
+
// adding Codex). After it, signing in to each provider is the only step.
|
|
9
|
+
//
|
|
10
|
+
// ⚠ IT NEVER DELAYS THE SERVER. `index.ts` calls `startAiAutoInstall` AFTER `listen`, with `void`;
|
|
11
|
+
// nothing awaits it. `/api/me` reads `runtime: 'installing'` (and the current step in
|
|
12
|
+
// `runtimeMessage`) the moment it starts, because `install()` claims its slot synchronously.
|
|
13
|
+
//
|
|
14
|
+
// ⚠ A FAILURE IS RETRIED ONCE, a few minutes later (a laptop waking on a dead network), then left for
|
|
15
|
+
// the reader's Retry, `limn ai install`, or the next boot — never a loop. A Codex-only failure is
|
|
16
|
+
// Codex's alone (ai/runtime.ts): Claude stays set up.
|
|
17
|
+
//
|
|
18
|
+
// Skipped when AI is off (LIMN_AI_DISABLED=true) or LIMN_AI_AUTO_INSTALL=false.
|
|
19
|
+
import { config } from '../config.js';
|
|
20
|
+
import { getAiRuntimeStatus, getCodexRuntimeStatus, installAiRuntime, } from './runtime.js';
|
|
21
|
+
/** How long after a failed automatic download the one automatic retry waits. */
|
|
22
|
+
export const AUTO_INSTALL_RETRY_MS = 3 * 60_000;
|
|
23
|
+
/**
|
|
24
|
+
* The providers a download must bring, or null when nothing is missing. ['codex'] alone over a ready
|
|
25
|
+
* Claude downloads Codex only (ai/runtime.ts): an existing install neither fetches nor swaps Claude.
|
|
26
|
+
*/
|
|
27
|
+
export function autoInstallProviders(claude, codex) {
|
|
28
|
+
// Already running (a `limn ai install` started by the same process cannot be, but a Retry can).
|
|
29
|
+
if (claude.runtime === 'installing' || codex.runtime === 'installing')
|
|
30
|
+
return null;
|
|
31
|
+
const needClaude = claude.runtime !== 'ready';
|
|
32
|
+
const needCodex = codex.runtime !== 'ready';
|
|
33
|
+
if (!needClaude && !needCodex)
|
|
34
|
+
return null;
|
|
35
|
+
if (!needCodex)
|
|
36
|
+
return ['claude'];
|
|
37
|
+
return needClaude ? ['claude', 'codex'] : ['codex'];
|
|
38
|
+
}
|
|
39
|
+
/** The testable body: one attempt, and on failure ONE scheduled retry. */
|
|
40
|
+
export function runAutoInstall(deps, attempt = 1) {
|
|
41
|
+
if (!deps.enabled)
|
|
42
|
+
return;
|
|
43
|
+
const providers = autoInstallProviders(deps.claude(), deps.codex());
|
|
44
|
+
if (!providers)
|
|
45
|
+
return;
|
|
46
|
+
const names = providers.map((p) => (p === 'codex' ? 'Codex' : 'Claude')).join(' and ');
|
|
47
|
+
deps.log.info(`ai: downloading ${names} in the background`);
|
|
48
|
+
void deps
|
|
49
|
+
.install(providers)
|
|
50
|
+
.then((r) => {
|
|
51
|
+
if (r.ok) {
|
|
52
|
+
deps.log.info(`ai: ${names} downloaded — sign in to use ${providers.length > 1 ? 'them' : 'it'}`);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
deps.log.warn(`ai: the download did not finish: ${r.message}`);
|
|
56
|
+
if (attempt === 1)
|
|
57
|
+
deps.schedule(() => runAutoInstall(deps, 2), AUTO_INSTALL_RETRY_MS);
|
|
58
|
+
})
|
|
59
|
+
.catch(() => {
|
|
60
|
+
/* installAiRuntime never throws; belt and braces for the boot */
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
/** Start the first-boot download (a no-op when nothing is missing). Never throws, never awaited. */
|
|
64
|
+
export function startAiAutoInstall(log) {
|
|
65
|
+
try {
|
|
66
|
+
runAutoInstall({
|
|
67
|
+
enabled: config.aiEnabled && config.aiAutoInstall,
|
|
68
|
+
claude: getAiRuntimeStatus,
|
|
69
|
+
codex: getCodexRuntimeStatus,
|
|
70
|
+
install: (providers) => installAiRuntime(undefined, { providers }),
|
|
71
|
+
schedule: (fn, ms) => {
|
|
72
|
+
setTimeout(fn, ms).unref?.();
|
|
73
|
+
},
|
|
74
|
+
log,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
catch (err) {
|
|
78
|
+
log.warn(`ai: the background download could not start: ${err.message}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
//# sourceMappingURL=auto-install.js.map
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// The project-checks tool's names (ai/provider.ts `AgentCheckTool`), in a module of their own so the
|
|
2
|
+
// providers can import them without a cycle through the registry.
|
|
3
|
+
/** The checks tool's server and name on Claude (`mcp__checks__run_check`). */
|
|
4
|
+
export const CHECKS_TOOL_SERVER = 'checks';
|
|
5
|
+
export const RUN_CHECK_TOOL = 'run_check';
|
|
6
|
+
//# sourceMappingURL=check-tool.js.map
|
package/dist/ai/codex/config.js
CHANGED
|
@@ -209,6 +209,7 @@ export function mcpConfig(mcp) {
|
|
|
209
209
|
...c(`${base}.args`, mcp.args),
|
|
210
210
|
...(mcp.env && Object.keys(mcp.env).length > 0 ? c(`${base}.env`, mcp.env) : []),
|
|
211
211
|
...c(`${base}.default_tools_approval_mode`, mcp.default_tools_approval_mode),
|
|
212
|
+
...(mcp.tool_timeout_sec != null ? c(`${base}.tool_timeout_sec`, mcp.tool_timeout_sec) : []),
|
|
212
213
|
];
|
|
213
214
|
}
|
|
214
215
|
/**
|
|
@@ -7,8 +7,13 @@
|
|
|
7
7
|
// paths inside the run's WRITE roots. A read-only run has none, so every write is denied. A patch
|
|
8
8
|
// is read for its `*** Add File:` / `*** Update File:` / `*** Delete File:` / `*** Move to:`
|
|
9
9
|
// headers; a write naming NO path we can find is denied (fail closed).
|
|
10
|
+
// • On a STORY BUILD (`roots.protect`) a write inside the write roots is ALSO denied when it touches
|
|
11
|
+
// the build's deny-list (review/write-deny.ts: CI / git / hook / package-manager config, lockfiles,
|
|
12
|
+
// agent settings) — the same rule the Claude hook applies.
|
|
10
13
|
// • `mcp__workspace__*` (Limn's own server) is allowed when every `path` it names is inside a READ
|
|
11
|
-
// root (the server checks again)
|
|
14
|
+
// root (the server checks again) — except `mcp__workspace__run_check` (the project's own checks,
|
|
15
|
+
// coding/checks/), allowed ONLY when the run carries checks (`roots.checks`) and the call names a
|
|
16
|
+
// check by a plain string; the server runs only names from its frozen list.
|
|
12
17
|
// • Any OTHER MCP tool is denied: only Limn's server may run (a user's own servers never load —
|
|
13
18
|
// Limn's CODEX_HOME carries none — and this keeps it so).
|
|
14
19
|
// • ⚠ AN ALLOW-LIST, NOT A DENY-LIST. The hook's matcher is `.*` (hook.ts), so it sees EVERY tool
|
|
@@ -20,12 +25,14 @@
|
|
|
20
25
|
// workspace server use — and tested with `isWithin`.
|
|
21
26
|
import { isAbsolute } from 'node:path';
|
|
22
27
|
import { isWithin, realPathFor, resolveReal } from '../../review/path-guard.js';
|
|
28
|
+
import { protectedWriteFor } from '../../review/write-deny.js';
|
|
23
29
|
export const WRITE_TOOLS = new Set(['apply_patch', 'Edit', 'Write', 'MultiEdit', 'NotebookEdit']);
|
|
24
30
|
export const WORKSPACE_TOOL_PREFIX = 'mcp__workspace__';
|
|
25
31
|
/** Built-ins that read and write nothing on disk; every other unlisted tool is denied. */
|
|
26
32
|
export const HARMLESS_TOOLS = new Set(['update_plan']);
|
|
27
33
|
const PATH_KEYS = ['file_path', 'path', 'notebook_path'];
|
|
28
34
|
const PATCH_HEADER = /^\s*\*\*\*\s*(Add File|Update File|Delete File|Move to)\s*:\s*(.*?)\s*$/;
|
|
35
|
+
export const RUN_CHECK_TOOL_NAME = 'mcp__workspace__run_check';
|
|
29
36
|
const deny = (reason) => ({ allow: false, reason: `Limn path guard: ${reason}` });
|
|
30
37
|
/** Every string anywhere in `v` (the patch text may sit under any key, or be the input itself). */
|
|
31
38
|
function strings(v, out = [], depth = 0) {
|
|
@@ -118,9 +125,22 @@ export function decideCodexToolCall(toolName, toolInput, roots, cwd) {
|
|
|
118
125
|
if (real == null || !writeRoots.some((r) => isWithin(r, real))) {
|
|
119
126
|
return deny(`refused: ${raw} is outside the folders this run may change.`);
|
|
120
127
|
}
|
|
128
|
+
if (roots.protect === true) {
|
|
129
|
+
const why = protectedWriteFor(raw, from, writeRoots);
|
|
130
|
+
if (why)
|
|
131
|
+
return deny(`refused: ${why}`);
|
|
132
|
+
}
|
|
121
133
|
}
|
|
122
134
|
return { allow: true };
|
|
123
135
|
}
|
|
136
|
+
if (toolName === RUN_CHECK_TOOL_NAME) {
|
|
137
|
+
if (roots.checks !== true)
|
|
138
|
+
return deny('refused: this run may not run checks.');
|
|
139
|
+
const name = toolInput && typeof toolInput === 'object' ? toolInput.name : undefined;
|
|
140
|
+
if (typeof name !== 'string' || name === '' || name.length > 120)
|
|
141
|
+
return deny('refused: name the check to run.');
|
|
142
|
+
return { allow: true };
|
|
143
|
+
}
|
|
124
144
|
if (toolName.startsWith('mcp__')) {
|
|
125
145
|
if (!toolName.startsWith(WORKSPACE_TOOL_PREFIX))
|
|
126
146
|
return deny(`refused: only Limn's workspace tools may run, not ${toolName}.`);
|
|
@@ -144,7 +164,12 @@ export function decideCodexToolCall(toolName, toolInput, roots, cwd) {
|
|
|
144
164
|
/** The roots as they travel to the hook process (the `LIMN_CODEX_GUARD` environment variable). */
|
|
145
165
|
export const GUARD_ENV = 'LIMN_CODEX_GUARD';
|
|
146
166
|
export function encodeGuardRoots(roots) {
|
|
147
|
-
return JSON.stringify({
|
|
167
|
+
return JSON.stringify({
|
|
168
|
+
read: roots.read,
|
|
169
|
+
write: roots.write,
|
|
170
|
+
...(roots.checks ? { checks: true } : {}),
|
|
171
|
+
...(roots.protect ? { protect: true } : {}),
|
|
172
|
+
});
|
|
148
173
|
}
|
|
149
174
|
/** Null when the variable is missing or malformed — the hook then denies everything it sees. */
|
|
150
175
|
export function decodeGuardRoots(raw) {
|
|
@@ -154,9 +179,11 @@ export function decodeGuardRoots(raw) {
|
|
|
154
179
|
const v = JSON.parse(raw);
|
|
155
180
|
if (!v || typeof v !== 'object')
|
|
156
181
|
return null;
|
|
157
|
-
const { read, write } = v;
|
|
182
|
+
const { read, write, checks, protect } = v;
|
|
158
183
|
const ok = (a) => Array.isArray(a) && a.every((x) => typeof x === 'string' && isAbsolute(x));
|
|
159
|
-
|
|
184
|
+
if (!ok(read) || !ok(write))
|
|
185
|
+
return null;
|
|
186
|
+
return { read, write, ...(checks === true ? { checks: true } : {}), ...(protect === true ? { protect: true } : {}) };
|
|
160
187
|
}
|
|
161
188
|
catch {
|
|
162
189
|
return null;
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
// release install that carries no AI package, and from dev under tsx.
|
|
4
4
|
import { parseServerArgv, serveWorkspaceMcp } from './workspace-mcp.js';
|
|
5
5
|
try {
|
|
6
|
-
const { read, logFile } = parseServerArgv(process.argv.slice(2));
|
|
7
|
-
await serveWorkspaceMcp({ read, logFile, input: process.stdin, output: process.stdout });
|
|
6
|
+
const { read, logFile, checksSpec } = parseServerArgv(process.argv.slice(2));
|
|
7
|
+
await serveWorkspaceMcp({ read, logFile, checksSpec, input: process.stdin, output: process.stdout });
|
|
8
8
|
}
|
|
9
9
|
catch (e) {
|
|
10
10
|
process.stderr.write(`limn workspace MCP server: ${e instanceof Error ? e.message : String(e)}\n`);
|
|
@@ -20,6 +20,7 @@ import { appendFileSync } from 'node:fs';
|
|
|
20
20
|
import { createInterface } from 'node:readline';
|
|
21
21
|
import { helperCommand } from './entry.js';
|
|
22
22
|
import { createWorkspaceTools, ToolRefusal, WORKSPACE_TOOL_DEFS } from './workspace-files.js';
|
|
23
|
+
import { CheckToolRunner, loadCheckSpec, runCheckDescription, runCheckInputSchema } from '../../coding/checks/spec.js';
|
|
23
24
|
export const WORKSPACE_MCP_ID = 'workspace';
|
|
24
25
|
const SERVER_INFO = { name: 'limn-workspace', version: '1.0.0' };
|
|
25
26
|
const DEFAULT_PROTOCOL = '2025-06-18';
|
|
@@ -27,6 +28,8 @@ const MAX_LINE_CHARS = 1024 * 1024;
|
|
|
27
28
|
/**
|
|
28
29
|
* The `mcp_servers.workspace` entry for one run: Node + the entry script + `--read <root>` per read
|
|
29
30
|
* root. `opts.logFile` makes the server append one JSON line per tool call (tests, the live check).
|
|
31
|
+
* `opts.checks` adds the project's own checks (`run_check`, coding/checks/spec.ts): the server reads
|
|
32
|
+
* that frozen list ONCE at start-up, and Codex is told a call may take up to `timeoutSec`.
|
|
30
33
|
*/
|
|
31
34
|
export function workspaceMcpConfig(roots, opts = {}) {
|
|
32
35
|
if (roots.read.length === 0)
|
|
@@ -37,29 +40,49 @@ export function workspaceMcpConfig(roots, opts = {}) {
|
|
|
37
40
|
argv.push('--read', r);
|
|
38
41
|
if (opts.logFile)
|
|
39
42
|
argv.push('--log', opts.logFile);
|
|
40
|
-
|
|
43
|
+
if (opts.checks)
|
|
44
|
+
argv.push('--checks', opts.checks.specFile);
|
|
45
|
+
return {
|
|
46
|
+
id: WORKSPACE_MCP_ID,
|
|
47
|
+
command,
|
|
48
|
+
args: argv,
|
|
49
|
+
env: {},
|
|
50
|
+
default_tools_approval_mode: 'approve',
|
|
51
|
+
...(opts.checks ? { tool_timeout_sec: Math.ceil(opts.checks.timeoutSec + 30) } : {}),
|
|
52
|
+
};
|
|
41
53
|
}
|
|
42
|
-
/** Parse the entry's argv (`--read <root>`…, `--log <file>`). */
|
|
54
|
+
/** Parse the entry's argv (`--read <root>`…, `--log <file>`, `--checks <spec file>`). */
|
|
43
55
|
export function parseServerArgv(argv) {
|
|
44
56
|
const read = [];
|
|
45
57
|
let logFile = null;
|
|
58
|
+
let checksSpec = null;
|
|
46
59
|
for (let i = 0; i < argv.length; i++) {
|
|
47
60
|
const flag = argv[i];
|
|
48
61
|
const value = argv[i + 1];
|
|
49
|
-
if ((flag === '--read' || flag === '--log') && value !== undefined) {
|
|
62
|
+
if ((flag === '--read' || flag === '--log' || flag === '--checks') && value !== undefined) {
|
|
50
63
|
if (flag === '--read')
|
|
51
64
|
read.push(value);
|
|
52
|
-
else
|
|
65
|
+
else if (flag === '--log')
|
|
53
66
|
logFile = value;
|
|
67
|
+
else
|
|
68
|
+
checksSpec = value;
|
|
54
69
|
i++;
|
|
55
70
|
}
|
|
56
71
|
else
|
|
57
72
|
throw new Error(`workspace MCP server: unexpected argument ${JSON.stringify(flag)}`);
|
|
58
73
|
}
|
|
59
|
-
return { read, logFile };
|
|
74
|
+
return { read, logFile, checksSpec };
|
|
60
75
|
}
|
|
61
|
-
/**
|
|
62
|
-
export function
|
|
76
|
+
/** The tool definitions: the three file tools, plus `run_check` when the run carries checks. */
|
|
77
|
+
export function toolDefs(checks) {
|
|
78
|
+
if (!checks || checks.names.length === 0)
|
|
79
|
+
return WORKSPACE_TOOL_DEFS;
|
|
80
|
+
return [
|
|
81
|
+
...WORKSPACE_TOOL_DEFS,
|
|
82
|
+
{ name: 'run_check', description: runCheckDescription(checks.names), inputSchema: runCheckInputSchema(checks.names) },
|
|
83
|
+
];
|
|
84
|
+
}
|
|
85
|
+
export function handleMessage(tools, msg, log, checks = null) {
|
|
63
86
|
if (!msg || typeof msg !== 'object' || Array.isArray(msg)) {
|
|
64
87
|
return { jsonrpc: '2.0', id: null, error: { code: -32600, message: 'Invalid request' } };
|
|
65
88
|
}
|
|
@@ -87,7 +110,7 @@ export function handleMessage(tools, msg, log) {
|
|
|
87
110
|
case 'ping':
|
|
88
111
|
return { jsonrpc: '2.0', id, result: {} };
|
|
89
112
|
case 'tools/list':
|
|
90
|
-
return { jsonrpc: '2.0', id, result: { tools:
|
|
113
|
+
return { jsonrpc: '2.0', id, result: { tools: toolDefs(checks) } };
|
|
91
114
|
case 'resources/list':
|
|
92
115
|
return { jsonrpc: '2.0', id, result: { resources: [] } };
|
|
93
116
|
case 'resources/templates/list':
|
|
@@ -96,6 +119,12 @@ export function handleMessage(tools, msg, log) {
|
|
|
96
119
|
return { jsonrpc: '2.0', id, result: { prompts: [] } };
|
|
97
120
|
case 'tools/call': {
|
|
98
121
|
const name = typeof params.name === 'string' ? params.name : '';
|
|
122
|
+
if (name === 'run_check' && checks) {
|
|
123
|
+
return checks.call(params.arguments).then((reply) => {
|
|
124
|
+
log({ tool: name, args: params.arguments, ok: !reply.isError });
|
|
125
|
+
return { jsonrpc: '2.0', id, result: { ...(reply.isError ? { isError: true } : {}), content: [{ type: 'text', text: reply.text }] } };
|
|
126
|
+
}, () => ({ jsonrpc: '2.0', id, result: { isError: true, content: [{ type: 'text', text: 'The check could not run.' }] } }));
|
|
127
|
+
}
|
|
99
128
|
try {
|
|
100
129
|
const text = tools.call(name, params.arguments);
|
|
101
130
|
log({ tool: name, args: params.arguments, ok: true });
|
|
@@ -114,6 +143,7 @@ export function handleMessage(tools, msg, log) {
|
|
|
114
143
|
/** Serve newline-delimited JSON-RPC on `input`/`output` until `input` ends. */
|
|
115
144
|
export function serveWorkspaceMcp(opts) {
|
|
116
145
|
const tools = createWorkspaceTools(opts.read);
|
|
146
|
+
const checks = opts.checksSpec ? new CheckToolRunner(loadCheckSpec(opts.checksSpec)) : null;
|
|
117
147
|
const log = (entry) => {
|
|
118
148
|
if (!opts.logFile)
|
|
119
149
|
return;
|
|
@@ -141,8 +171,10 @@ export function serveWorkspaceMcp(opts) {
|
|
|
141
171
|
send({ jsonrpc: '2.0', id: null, error: { code: -32700, message: 'Parse error' } });
|
|
142
172
|
return;
|
|
143
173
|
}
|
|
144
|
-
const reply = handleMessage(tools, msg, log);
|
|
145
|
-
if (reply)
|
|
174
|
+
const reply = handleMessage(tools, msg, log, checks);
|
|
175
|
+
if (reply instanceof Promise)
|
|
176
|
+
void reply.then(send);
|
|
177
|
+
else if (reply)
|
|
146
178
|
send(reply);
|
|
147
179
|
});
|
|
148
180
|
return new Promise((resolve) => rl.on('close', () => resolve()));
|
package/dist/ai/credentials.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Fix, the ticket / CI reviews, diagrams, sprint paths, the docs build and chat, resolve with
|
|
3
3
|
// Claude, every chat) AND the one-shot completions (review/llm.ts `cheapComplete`, which the
|
|
4
4
|
// sprint briefs/themes, story labels, docs ranking, sprint Ask Claude and every platform feature —
|
|
5
|
-
// reports, synthesis, work-plan wording, pointers, bot themes, the
|
|
5
|
+
// reports, synthesis, work-plan wording, pointers, bot themes, the Slack
|
|
6
6
|
// report — all go through).
|
|
7
7
|
//
|
|
8
8
|
// 1. THE LOCAL CLAUDE CODE LOGIN (default) — a `claude` session on disk or
|
package/dist/ai/effort.js
CHANGED
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
// Neither ⇒ DEFAULT (null). No DB import here, for the same reason as ai/provider-setting.ts.
|
|
8
8
|
//
|
|
9
9
|
// WHO IT REACHES (`agenticRunEffort`): the PR review (lead, specialists, follow-up re-reviews), AI Fix
|
|
10
|
-
// and Plan first,
|
|
10
|
+
// and Plan first, and "Implement story"'s plan and build passes (`story_plan`, `story_build`; owner
|
|
11
|
+
// decision 7, docs/STORY-BUILD.md), on BOTH providers — and NOTHING ELSE. REVIEW_EFFORT and REVIEW_DIFF_ONLY_EFFORT reach
|
|
11
12
|
// exactly the same runs as the Settings control (REVIEW_EFFORT IS the control, locked), so "Set by
|
|
12
13
|
// REVIEW_EFFORT" under it is the whole story. Every other agent kind (CI review, ticket review,
|
|
13
14
|
// diagrams, Check threads, chats, Resolve with Claude) runs at its provider's own fixed depth
|
|
@@ -89,7 +90,7 @@ export function diffOnlyEffortAt(level, atDefault) {
|
|
|
89
90
|
return higherOf(stepDownEffort(level), lowerOf(level, atDefault));
|
|
90
91
|
}
|
|
91
92
|
/**
|
|
92
|
-
* THE EFFORT of a review / AI Fix / Plan first run on `provider`: the chosen level on a worktree run;
|
|
93
|
+
* THE EFFORT of a review / AI Fix / Plan first / story plan / story build run on `provider`: the chosen level on a worktree run;
|
|
93
94
|
* diff-only, REVIEW_DIFF_ONLY_EFFORT when set (at any level, on both providers), else
|
|
94
95
|
* `diffOnlyEffortAt`; Default ⇒ `defaultRunEffort`. Read once per run, at its start; the run stores
|
|
95
96
|
* what it used.
|
|
@@ -8,6 +8,7 @@ import { codexCredentialRunnable, resolveAiCredential, resolveCodexCredential }
|
|
|
8
8
|
import { CODEX_DOCS_KINDS } from './codex/kinds/docs.js';
|
|
9
9
|
import { CODEX_FIX_KINDS } from './codex/kinds/fix.js';
|
|
10
10
|
import { CODEX_REVIEW_KINDS } from './codex/kinds/review.js';
|
|
11
|
+
import { CODEX_STORY_BUILD_KINDS } from './codex/kinds/story-build.js';
|
|
11
12
|
import { CODEX_TICKETS_KINDS } from './codex/kinds/tickets.js';
|
|
12
13
|
export const AI_PROVIDER_ENV = 'LIMN_AI_PROVIDER';
|
|
13
14
|
/**
|
|
@@ -59,6 +60,8 @@ export const CODEX_AGENT_KINDS = new Set([
|
|
|
59
60
|
...CODEX_FIX_KINDS,
|
|
60
61
|
...CODEX_TICKETS_KINDS,
|
|
61
62
|
...CODEX_DOCS_KINDS,
|
|
63
|
+
// "Implement story" (listed from its contract commit — see that file's header).
|
|
64
|
+
...CODEX_STORY_BUILD_KINDS,
|
|
62
65
|
]);
|
|
63
66
|
/** Can Codex run this agent kind in this version? (`allow` is for tests.) */
|
|
64
67
|
export function codexKindReady(kind, allow = CODEX_AGENT_KINDS) {
|
|
@@ -79,6 +82,8 @@ const KIND_WORDS = {
|
|
|
79
82
|
story_docs: 'checks against docs',
|
|
80
83
|
docs_build: 'the docs library',
|
|
81
84
|
docs_chat: 'the docs chat',
|
|
85
|
+
story_plan: 'story plans',
|
|
86
|
+
story_build: 'story builds',
|
|
82
87
|
};
|
|
83
88
|
/** Every agent kind (the KIND_WORDS keys, so a new kind cannot be missed: the Record is exhaustive). */
|
|
84
89
|
export const ALL_AGENT_KINDS = Object.keys(KIND_WORDS);
|
package/dist/ai/provider.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { CLAUDE_MODEL_FOR_ROLE, claudeProvider } from './providers/claude.js';
|
|
2
2
|
import { CODEX_MODEL_FOR_ROLE as CODEX_MODEL_FOR_ROLE_SRC, codexProvider } from './providers/codex.js';
|
|
3
3
|
import { activeProviderId, codexRunnable } from './provider-setting.js';
|
|
4
|
+
export { CHECKS_TOOL_SERVER, RUN_CHECK_TOOL } from './check-tool.js';
|
|
4
5
|
// ---- the registry -------------------------------------------------------------------------------
|
|
5
6
|
export { AI_PROVIDER_ENV, aiProviderEnvSetting, aiProviderSetting, aiProviderSettingStatus, setStoredAiProviderSetting, storedAiProviderSetting, CODEX_AGENT_KINDS, CODEX_NOT_AVAILABLE_MESSAGE, codexKindNotAvailableMessage, codexKindReady, codexRunnable, ALL_AGENT_KINDS, ALL_RUN_KINDS, activeProviderId, autoReadiness, codexRunKindReady, pickAutoProvider, } from './provider-setting.js';
|
|
6
7
|
export { CODEX_MODEL_FOR_ROLE } from './providers/codex.js';
|
|
@@ -8,23 +8,26 @@
|
|
|
8
8
|
// CLAUDE.md or skills), `ISOLATED_MCP` (no claude.ai connectors), the LIMN_CLAUDE_PATH
|
|
9
9
|
// executable and the credential ladder's environment (`agentAuthOptions()`);
|
|
10
10
|
// • ⚠ THE PATH GUARD as the FIRST PreToolUse hook — `createPathGuard(cwd, roots, {readOnlyRoots})`
|
|
11
|
-
// — then `ownToolsOnlyHook(<the run's own server>)`, then
|
|
12
|
-
//
|
|
13
|
-
//
|
|
11
|
+
// — then `ownToolsOnlyHook(<the run's own server>)`, then (a story build, `spec.protectWrites`) the
|
|
12
|
+
// write deny-list (review/write-deny.ts), then the run's extra hooks (the specialist cap, the docs
|
|
13
|
+
// build's sections-write guard). Under bypassPermissions a PreToolUse deny is the only thing that
|
|
14
|
+
// confines an absolute path, and the prompt is attacker-authored;
|
|
14
15
|
// • `additionalDirectories` = roots + read-only roots, only when there are any (the SDK adds one
|
|
15
16
|
// `--add-dir` per entry, so an empty list and no list run the same CLI);
|
|
16
17
|
// • the result tools as ONE in-process MCP server (`createSdkMcpServer`, version 1.0.0), each
|
|
17
18
|
// handler's reply as one text block (+ `isError` only when the handler set it).
|
|
18
19
|
// The SDK module comes from `loadAgentSdk()`, whose `query` is TAPPED (ai/runtime.ts `tapQuery`), so
|
|
19
20
|
// every message still reaches the spend budget's observer.
|
|
20
|
-
import { loadAgentSdk, loadAnthropicSdk, claudeExecutableOptions, getAiRuntimeStatus } from '../runtime.js';
|
|
21
|
+
import { loadAgentSdk, loadAnthropicSdk, loadZod, claudeExecutableOptions, getAiRuntimeStatus } from '../runtime.js';
|
|
21
22
|
import { agentAuthOptions, agentSdkEnv, resolveAiCredential } from '../credentials.js';
|
|
22
23
|
import { ISOLATED_MCP, ownToolsOnlyHook } from '../agent-isolation.js';
|
|
23
24
|
import { config } from '../../config.js';
|
|
24
25
|
import { NO_CLAUDE_AUTH_MESSAGE } from '../../review/auth.js';
|
|
25
26
|
import { sdkModelOptions } from '../../review/model-options.js';
|
|
26
27
|
import { createPathGuard, pathGuardHook } from '../../review/path-guard.js';
|
|
28
|
+
import { protectedWriteHook } from '../../review/write-deny.js';
|
|
27
29
|
import { recordUsage, sumModelUsage, sumUsageMap } from '../../review/usage.js';
|
|
30
|
+
import { CHECKS_TOOL_SERVER, RUN_CHECK_TOOL } from '../check-tool.js';
|
|
28
31
|
import { AI_MODEL_FOR_ROLE } from '../../shared/index.js';
|
|
29
32
|
import { foldRateLimitEvent, parseUsageProbe } from './claude-usage.js';
|
|
30
33
|
import { assistantText, describeFixBlocks, describeReviewBlocks } from './claude-activity.js';
|
|
@@ -42,21 +45,40 @@ async function runAgent(spec) {
|
|
|
42
45
|
// From the AI runtime (ai/runtime.ts) — downloaded on first use for an npm install. A missing
|
|
43
46
|
// runtime throws here, into the caller's catch, as it always did.
|
|
44
47
|
const { createSdkMcpServer, query, tool } = await loadAgentSdk();
|
|
45
|
-
const mcpServers =
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
48
|
+
const mcpServers = {};
|
|
49
|
+
if (spec.tools) {
|
|
50
|
+
mcpServers[spec.tools.server] = createSdkMcpServer({
|
|
51
|
+
name: spec.tools.server,
|
|
52
|
+
version: '1.0.0',
|
|
53
|
+
tools: spec.tools.tools.map((t) => tool(t.name, t.description, t.shape, async (a) => {
|
|
54
|
+
const reply = await t.handle(a);
|
|
55
|
+
return {
|
|
56
|
+
content: [{ type: 'text', text: reply.text }],
|
|
57
|
+
...(reply.isError !== undefined ? { isError: reply.isError } : {}),
|
|
58
|
+
};
|
|
59
|
+
})),
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
// THE PROJECT'S CHECKS (`mcp__checks__run_check`): a second in-process server, present only when the
|
|
63
|
+
// run carries them. Its one argument is an enum of the frozen names — nothing else reaches the runner.
|
|
64
|
+
const checks = spec.checks;
|
|
65
|
+
if (checks && checks.names.length > 0) {
|
|
66
|
+
const z = await loadZod();
|
|
67
|
+
mcpServers[CHECKS_TOOL_SERVER] = createSdkMcpServer({
|
|
68
|
+
name: CHECKS_TOOL_SERVER,
|
|
69
|
+
version: '1.0.0',
|
|
70
|
+
tools: [
|
|
71
|
+
tool(RUN_CHECK_TOOL, checks.description, { name: z.enum(checks.names).describe('The check to run.') }, async (a) => {
|
|
72
|
+
const reply = await checks.run(a);
|
|
52
73
|
return {
|
|
53
74
|
content: [{ type: 'text', text: reply.text }],
|
|
54
75
|
...(reply.isError !== undefined ? { isError: reply.isError } : {}),
|
|
55
76
|
};
|
|
56
|
-
})
|
|
57
|
-
|
|
58
|
-
}
|
|
59
|
-
|
|
77
|
+
}),
|
|
78
|
+
],
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
const ownServers = Object.keys(mcpServers);
|
|
60
82
|
const guard = createPathGuard(spec.cwd, spec.roots, { readOnlyRoots: spec.readOnlyRoots });
|
|
61
83
|
const extraDirs = [...spec.roots, ...spec.readOnlyRoots];
|
|
62
84
|
const c = spec.claude;
|
|
@@ -82,7 +104,9 @@ async function runAgent(spec) {
|
|
|
82
104
|
hooks: {
|
|
83
105
|
PreToolUse: [
|
|
84
106
|
pathGuardHook(guard),
|
|
85
|
-
ownToolsOnlyHook(
|
|
107
|
+
ownToolsOnlyHook(ownServers),
|
|
108
|
+
// The build's write deny-list (review/write-deny.ts), only on a run that asks for it.
|
|
109
|
+
...(spec.protectWrites && spec.writeRoots.length > 0 ? [protectedWriteHook(spec.writeRoots, spec.cwd)] : []),
|
|
86
110
|
...(c.extraPreToolUse ?? []),
|
|
87
111
|
],
|
|
88
112
|
},
|
|
@@ -90,7 +114,7 @@ async function runAgent(spec) {
|
|
|
90
114
|
// Don't inherit the host's .claude settings / CLAUDE.md / skills.
|
|
91
115
|
settingSources: [],
|
|
92
116
|
...ISOLATED_MCP,
|
|
93
|
-
...(
|
|
117
|
+
...(ownServers.length > 0 ? { mcpServers: mcpServers } : {}),
|
|
94
118
|
abortController: spec.abortController,
|
|
95
119
|
// LIMN_CLAUDE_PATH → the user's own `claude` instead of the SDK's bundled binary.
|
|
96
120
|
...claudeExecutableOptions(),
|
|
@@ -491,11 +491,20 @@ export async function runAgentUnchecked(spec) {
|
|
|
491
491
|
const z = await loadZod();
|
|
492
492
|
if (resultTool)
|
|
493
493
|
toolSchema = shapeToJsonSchema(z, resultTool.shape);
|
|
494
|
-
|
|
494
|
+
// THE PROJECT'S CHECKS (`mcp__workspace__run_check`, coding/checks/): the server reads Limn's frozen
|
|
495
|
+
// list once at start-up, and the guard lets the tool through ONLY on a run that carries checks.
|
|
496
|
+
const checks = spec.checks && spec.checks.names.length > 0 ? spec.checks : null;
|
|
497
|
+
const mcp = workspaceMcpConfig(roots, checks ? { checks: { specFile: checks.specFile, timeoutSec: checks.timeoutSec } } : {});
|
|
495
498
|
// THE GUARD IS NOT OPTIONAL. Its trust entry is written into Limn's own home first, then Codex is
|
|
496
499
|
// asked whether it will RUN the hook (and the hook must refuse a canary call). An untrusted hook is
|
|
497
500
|
// skipped SILENTLY by Codex, so a run whose guard is not proven never starts — read-only or not.
|
|
498
|
-
|
|
501
|
+
// A story build (`protectWrites`) also carries the write deny-list into the guard (hook-rule.ts).
|
|
502
|
+
const { hook, home } = await trustedGuard({
|
|
503
|
+
read: roots.read,
|
|
504
|
+
write: roots.write,
|
|
505
|
+
...(checks ? { checks: true } : {}),
|
|
506
|
+
...(spec.protectWrites && roots.write.length > 0 ? { protect: true } : {}),
|
|
507
|
+
});
|
|
499
508
|
armCodexThreadSweeps(home);
|
|
500
509
|
const before = isWrite ? await Promise.all(roots.write.map(gitSnapshot)) : [];
|
|
501
510
|
return withScratch(async (scratch) => {
|
|
@@ -506,7 +515,8 @@ export async function runAgentUnchecked(spec) {
|
|
|
506
515
|
}
|
|
507
516
|
const args = buildExecArgs({ cwd, writeRoots: roots.write, ...(spec.system ? { system: spec.system } : {}) }, { home, mcp, hook, schemaPath, model, effort });
|
|
508
517
|
const env = codexExecEnv(credential, hook);
|
|
509
|
-
|
|
518
|
+
// A run that may run the project's checks gets longer: each may take up to its own limit.
|
|
519
|
+
const timeoutMs = spec.codex?.timeoutMs ?? (checks ? DEFAULT_AGENT_TIMEOUT_MS * 2 : DEFAULT_AGENT_TIMEOUT_MS);
|
|
510
520
|
// THE NEUTRAL STREAM (ai/provider.ts): the status-line kinds, then the step the event makes (its
|
|
511
521
|
// activity line, its text, the run's tokens so far — turn.completed carries the thread's total,
|
|
512
522
|
// which for a run and its retry is the run's own).
|
|
@@ -14,12 +14,18 @@ export const RESULT_TOOL_NAMES = [
|
|
|
14
14
|
'submit_design',
|
|
15
15
|
'submit_paths',
|
|
16
16
|
'submit_alignment',
|
|
17
|
+
// The story docs read's BUILD mode ("Implement story" → docs consideration).
|
|
18
|
+
'submit_build_docs',
|
|
17
19
|
'submit_explanations',
|
|
18
20
|
'record_sections',
|
|
19
21
|
// The docs build's closing call (beside record_sections): a result too.
|
|
20
22
|
'finish_document',
|
|
21
23
|
// The docs build on CODEX: record_sections + finish_document as ONE final answer (Phase 3).
|
|
22
24
|
'submit_document',
|
|
25
|
+
// "Implement story": the read-only plan pass (coding/story-build/schema.ts).
|
|
26
|
+
'submit_story_plan',
|
|
27
|
+
// "Implement story": one repository's build (coding/story-build/build.ts) — the `submit_fix` shape.
|
|
28
|
+
'submit_build',
|
|
23
29
|
];
|
|
24
30
|
// Each builder is imported LAZILY: an agent's schema module is reached only when its schema is asked
|
|
25
31
|
// for, never at import time of this registry.
|
|
@@ -36,10 +42,13 @@ const BUILDERS = {
|
|
|
36
42
|
submit_design: async (z) => (await import('../review/diagrams/schema.js')).buildSubmitDesignShape(z),
|
|
37
43
|
submit_paths: async (z) => (await import('../review/sprint-paths/schema.js')).buildSubmitPathsShape(z),
|
|
38
44
|
submit_alignment: async (z) => (await import('../review/story-docs/schema.js')).buildSubmitAlignmentShape(z),
|
|
45
|
+
submit_build_docs: async (z) => (await import('../review/story-docs/schema.js')).buildSubmitBuildDocsShape(z),
|
|
39
46
|
submit_explanations: async (z) => (await import('../review/claude-review/chat-explain.js')).buildSubmitExplanationsShape(z),
|
|
40
47
|
record_sections: async (z) => (await import('../reference-docs/schema.js')).buildRecordSectionsShape(z),
|
|
41
48
|
finish_document: async (z) => (await import('../reference-docs/schema.js')).buildFinishShape(z),
|
|
42
49
|
submit_document: async (z) => (await import('../reference-docs/schema.js')).buildSubmitDocumentShape(z),
|
|
50
|
+
submit_story_plan: async (z) => (await import('../coding/story-build/schema.js')).buildSubmitStoryPlanShape(z),
|
|
51
|
+
submit_build: async (z) => (await import('../coding/schema.js')).buildSubmitFixShape(z),
|
|
43
52
|
};
|
|
44
53
|
/** The raw zod shape the Claude tool `name` is registered with. `z` defaults to the AI runtime's. */
|
|
45
54
|
export async function resultToolShape(name, opts = {}, z) {
|