@cspeach/cli 1.1.18 → 1.1.20
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 +3 -4
- package/dist/agent/anthropic-provider.js +30 -10
- package/dist/agent/cache-keepalive.js +162 -0
- package/dist/agent/cold-prune.js +116 -0
- package/dist/agent/loop.js +804 -159
- package/dist/agent/provider-shape.js +263 -0
- package/dist/agent/providers/ai-hub-provider.js +17 -2
- package/dist/agent/providers/byok-provider.js +33 -3
- package/dist/agent/providers/local-provider.js +12 -2
- package/dist/agent/repair-partial.js +125 -7
- package/dist/agent/summarise-via-provider.js +6 -1
- package/dist/agent/system-prompt.js +38 -0
- package/dist/agent/tool-dispatch.js +66 -21
- package/dist/agent/tool-loading-pin.js +100 -0
- package/dist/agent/turn-error-ux.js +77 -26
- package/dist/agent/turn-stream.js +25 -3
- package/dist/approvals/adt-type.js +27 -0
- package/dist/approvals/advisory-prompt.js +48 -0
- package/dist/approvals/advisory-render.js +25 -11
- package/dist/approvals/approval-prompt.js +74 -24
- package/dist/approvals/jwt.js +2 -1
- package/dist/approvals/render.js +64 -36
- package/dist/approvals/risk-floor.js +53 -2
- package/dist/auth/api-key.js +30 -16
- package/dist/auth/keychain.js +0 -0
- package/dist/auth/me.js +25 -7
- package/dist/cli.js +36 -6
- package/dist/commands/auto-compact.js +33 -16
- package/dist/commands/compact.js +49 -11
- package/dist/commands/config-set.js +16 -3
- package/dist/commands/config-show.js +18 -5
- package/dist/commands/cost.js +54 -35
- package/dist/commands/help.js +88 -50
- package/dist/commands/login.js +28 -19
- package/dist/commands/logout.js +7 -3
- package/dist/commands/plan-audit.js +1 -0
- package/dist/commands/plan-chain.js +71 -77
- package/dist/commands/plan-continue.js +1 -0
- package/dist/commands/plan-resume.js +20 -12
- package/dist/commands/whoami.js +23 -9
- package/dist/config/loader.js +70 -5
- package/dist/cost/cost-log.js +62 -2
- package/dist/cost/pricing.js +11 -6
- package/dist/doctor/checks/forge-rules.js +13 -3
- package/dist/doctor/checks/keychain.js +6 -4
- package/dist/doctor/run.js +101 -20
- package/dist/index.js +5 -2
- package/dist/lib/graceful-exit.js +56 -0
- package/dist/lib/piped-prompt.js +65 -0
- package/dist/lib/spill-labels.js +13 -0
- package/dist/lock-contention.js +2 -2
- package/dist/models/resolve.js +93 -2
- package/dist/models/server-config.js +158 -3
- package/dist/one-shot.js +150 -45
- package/dist/projects/answer-blockers.js +6 -1
- package/dist/projects/handover-md.js +15 -14
- package/dist/projects/image-attachments.js +15 -2
- package/dist/projects/plan-run.js +97 -54
- package/dist/projects/promote-command.js +11 -2
- package/dist/projects/save-command.js +3 -2
- package/dist/projects/status.js +2 -1
- package/dist/projects/yes-no.js +12 -0
- package/dist/renderer/abap-inline.js +18 -14
- package/dist/renderer/answer-trim.js +69 -0
- package/dist/renderer/banners.js +8 -8
- package/dist/renderer/brand-settled.js +19 -0
- package/dist/renderer/change-card.js +156 -0
- package/dist/renderer/checklist-format.js +130 -0
- package/dist/renderer/color-mode.js +45 -0
- package/dist/renderer/error-detail.js +104 -0
- package/dist/renderer/fence-state.js +46 -0
- package/dist/renderer/footer-line.js +100 -0
- package/dist/renderer/glyphs.js +25 -0
- package/dist/renderer/goodbye.js +38 -0
- package/dist/renderer/legacy-palette.js +41 -0
- package/dist/renderer/look.js +30 -0
- package/dist/renderer/markdown.js +299 -92
- package/dist/renderer/notice-log.js +72 -0
- package/dist/renderer/notice-shape.js +48 -0
- package/dist/renderer/notices.js +40 -8
- package/dist/renderer/panel-rows.js +16 -0
- package/dist/renderer/pipeline.js +66 -0
- package/dist/renderer/progress-chatter.js +23 -17
- package/dist/renderer/rendering-mode.js +34 -0
- package/dist/renderer/routing-line.js +14 -0
- package/dist/renderer/sanitize.js +12 -0
- package/dist/renderer/severity.js +2 -7
- package/dist/renderer/startup-lines.js +154 -0
- package/dist/renderer/status-footer.js +52 -34
- package/dist/renderer/steering-echo.js +41 -0
- package/dist/renderer/syntax.js +31 -12
- package/dist/renderer/tables.js +5 -1
- package/dist/renderer/theme.js +44 -0
- package/dist/renderer/thinking-heartbeat.js +33 -2
- package/dist/renderer/todo-block.js +10 -49
- package/dist/renderer/tool-labels.js +269 -0
- package/dist/renderer/tool-widget.js +245 -51
- package/dist/renderer/trace.js +42 -0
- package/dist/renderer/transcript-flow.js +132 -0
- package/dist/renderer/tty.js +34 -1
- package/dist/renderer/ui-width.js +55 -0
- package/dist/renderer/verify-chain.js +4 -2
- package/dist/renderer/widget-fallback.js +58 -65
- package/dist/repl/bracketed-paste.js +7 -1
- package/dist/repl/builtin-commands.js +19 -8
- package/dist/repl/credential-handover-gate.js +28 -0
- package/dist/repl/current-transport.js +13 -0
- package/dist/repl/early-line-buffer.js +5 -2
- package/dist/repl/file-picker.js +54 -12
- package/dist/repl/ink-stdin-guard.js +66 -0
- package/dist/repl/inquirer-guard.js +59 -16
- package/dist/repl/inquirer-theme.js +27 -33
- package/dist/repl/paste-marker.js +19 -0
- package/dist/repl/plan-turn-end.js +20 -0
- package/dist/repl/post-turn-status.js +43 -11
- package/dist/repl/reroute-turn.js +21 -0
- package/dist/repl/reset-tty-stdin.js +44 -0
- package/dist/repl/restore-guard.js +22 -0
- package/dist/repl/resume-standalone.js +63 -0
- package/dist/repl/rule8-detector.js +11 -3
- package/dist/repl/safety-confirm.js +161 -93
- package/dist/repl/session-spend-line.js +8 -4
- package/dist/repl/themed-prompts.js +19 -0
- package/dist/repl/ui-look-command.js +75 -0
- package/dist/repl/update-method-preview-hook.js +48 -5
- package/dist/repl.js +863 -339
- package/dist/rewind/cli.js +3 -2
- package/dist/rewind/format.js +14 -9
- package/dist/rewind/restore.js +11 -0
- package/dist/router/classifier.js +47 -4
- package/dist/router/intent-extractor.js +24 -8
- package/dist/router/piped-routing.js +23 -0
- package/dist/router/resume-routing.js +16 -0
- package/dist/router/routing-failure.js +57 -0
- package/dist/sap/first-run-choice.js +1 -1
- package/dist/sap/onboarding.js +3 -2
- package/dist/sap/standalone-onboarding.js +1 -1
- package/dist/sap/system-info.js +4 -2
- package/dist/sap/unreachable.js +28 -0
- package/dist/sap-errors/clean-error-text.js +182 -0
- package/dist/session/interrupt-reason.js +39 -0
- package/dist/session/recap.js +36 -24
- package/dist/session/repin-model.js +18 -0
- package/dist/session/resume.js +104 -36
- package/dist/session/store.js +84 -28
- package/dist/session/user-prompt.js +21 -0
- package/dist/skill-catalog.js +7 -0
- package/dist/skills/bundled-skills.js +76 -69
- package/dist/skills/preamble.js +75 -0
- package/dist/skills/source-manifest.js +11 -1
- package/dist/standards/standards-init.js +1 -1
- package/dist/test-helpers/answer-any.js +46 -0
- package/dist/tools/approval.js +27 -11
- package/dist/tools/ask-question.js +47 -5
- package/dist/tools/filesystem/file-read.js +11 -1
- package/dist/tools/filesystem/file-write.js +19 -2
- package/dist/tools/fiori/fe-extend.js +16 -1
- package/dist/tools/fiori/fe-scaffold.js +105 -3
- package/dist/tools/fiori/html-escapes.js +28 -0
- package/dist/tools/fiori/metadata/audit.js +605 -0
- package/dist/tools/fiori/metadata/references.js +104 -0
- package/dist/tools/fiori/metadata/types.js +8 -0
- package/dist/tools/fiori/preview/app-guard.js +215 -0
- package/dist/tools/fiori/preview/env.js +193 -0
- package/dist/tools/fiori/preview/readiness.js +127 -0
- package/dist/tools/fiori/preview/registry.js +412 -0
- package/dist/tools/fiori/preview/start.js +469 -0
- package/dist/tools/fiori/samples/loader.js +20 -5
- package/dist/tools/fiori/scaffold.js +18 -6
- package/dist/tools/fiori/smoke/app-driver-page.js +372 -0
- package/dist/tools/fiori/smoke/app-driver.js +100 -0
- package/dist/tools/fiori/smoke/assertions.js +161 -24
- package/dist/tools/fiori/smoke/audit-columns.js +21 -0
- package/dist/tools/fiori/smoke/batch.js +273 -0
- package/dist/tools/fiori/smoke/draft-safety.js +241 -0
- package/dist/tools/fiori/smoke/draft-smoke.js +473 -0
- package/dist/tools/fiori/smoke/driver.js +171 -4
- package/dist/tools/fiori/smoke/evidence.js +35 -0
- package/dist/tools/fiori/smoke/labels-codes.js +207 -0
- package/dist/tools/fiori/smoke/nav-error.js +26 -0
- package/dist/tools/fiori/smoke/run-smoke.js +155 -12
- package/dist/tools/fiori/tools.js +651 -7
- package/dist/tools/fiori/ui5-version.js +16 -0
- package/dist/tools/index.js +8 -0
- package/dist/tools/local-build.js +6 -0
- package/dist/tools/result-spill.js +238 -0
- package/dist/tools/sap-read.js +74 -15
- package/dist/tools/sap-write.js +122 -10
- package/dist/tools/shell/shell_exec.js +39 -12
- package/dist/tools/subagent/adt-serial.js +33 -0
- package/dist/tools/subagent/agent_run.js +2 -0
- package/dist/tools/subagent/read_agent.js +178 -0
- package/dist/tools/subagent/reader-prompt.js +48 -0
- package/dist/tools/syntax-state.js +67 -0
- package/dist/tools/todo.js +26 -19
- package/dist/tools/tool-loading.js +255 -0
- package/dist/tools/tool-output-read.js +117 -0
- package/dist/tools/transport-fit.js +462 -0
- package/dist/tools/transport-resolution.js +3 -1
- package/dist/tools/transport.js +39 -8
- package/dist/tools/verify.js +184 -8
- package/dist/ui/app.js +284 -146
- package/dist/ui/approval-modal.js +41 -15
- package/dist/ui/ask-answer-text.js +17 -0
- package/dist/ui/body.js +25 -6
- package/dist/ui/card-slot.js +98 -0
- package/dist/ui/checklist.js +9 -0
- package/dist/ui/coaching-picker-classic.js +8 -5
- package/dist/ui/command-palette.js +6 -4
- package/dist/ui/confirm-request.js +18 -0
- package/dist/ui/context-grid.js +2 -1
- package/dist/ui/file-palette.js +6 -4
- package/dist/ui/files-card-emitter.js +19 -0
- package/dist/ui/footer-line.js +16 -0
- package/dist/ui/footer.js +105 -104
- package/dist/ui/input-wrap.js +122 -0
- package/dist/ui/interrupt.js +103 -0
- package/dist/ui/key-burst.js +117 -0
- package/dist/ui/line-resolution.js +4 -3
- package/dist/ui/live-area.js +71 -0
- package/dist/ui/login-banner.js +54 -43
- package/dist/ui/rewind-panel.js +76 -24
- package/dist/ui/role-props.js +47 -0
- package/dist/ui/sap-state-store.js +2 -1
- package/dist/ui/session-timeline.js +49 -3
- package/dist/ui/skill-picker.js +5 -47
- package/dist/ui/text-input.js +128 -24
- package/dist/ui/todo-emitter.js +19 -0
- package/dist/ui/turn-status-emitter.js +65 -2
- package/dist/ui/turn-status.js +16 -19
- package/dist/ui/use-card-request.js +44 -0
- package/dist/ui/widgets/ask-card.js +521 -0
- package/dist/ui/widgets/bar-chart.js +11 -5
- package/dist/ui/widgets/coaching-picker.js +50 -55
- package/dist/ui/widgets/confirm-card.js +193 -0
- package/dist/ui/widgets/dep-graph.js +3 -2
- package/dist/ui/widgets/diff-viewer.js +6 -5
- package/dist/ui/widgets/files-card.js +97 -0
- package/dist/ui/widgets/question-card.js +2 -1
- package/dist/ui/widgets/reclaim-raw-stdin.js +24 -0
- package/dist/ui/widgets/shortcuts-sheet.js +25 -0
- package/dist/ui/widgets/stack-frames.js +2 -1
- package/dist/upgrade-check.js +39 -8
- package/package.json +12 -2
- package/test-harness/fe-draft/app.ts +188 -0
- package/test-harness/fe-draft/fe-draft.e2e.test.ts +461 -0
- package/test-harness/fe-draft/manifest-enhancer.unit.test.ts +51 -0
- package/test-harness/fe-draft/routes.ts +224 -0
- package/test-harness/fe-draft/routes.unit.test.ts +45 -0
- package/test-harness/fe-draft/server.ts +225 -0
- package/test-harness/fe-draft/variants.ts +196 -0
- package/vitest.smoke-e2e.config.ts +20 -0
|
@@ -45,10 +45,22 @@ import * as path from 'node:path';
|
|
|
45
45
|
import { registerTool } from '../index.js';
|
|
46
46
|
import { resolveSafePath, PathOutsideRootError } from '../_filesystem-shared.js';
|
|
47
47
|
import { loadSafelist, buildSafeEnv, resolveExecutable, validateArgv, rejectPathSeparator, DEFAULT_SAFELIST, KILL_GRACE_MS, } from '../_command-shared.js';
|
|
48
|
+
import { killProcessTree } from '../fiori/preview/registry.js';
|
|
49
|
+
import { spillToolResult } from '../result-spill.js';
|
|
48
50
|
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
51
|
+
/** After the kill's grace: how long to wait for the pipes before returning anyway. */
|
|
52
|
+
const ORPHAN_WAIT_MS = 3_000;
|
|
49
53
|
const MAX_TIMEOUT_MS = 300_000;
|
|
50
54
|
const MAX_OUTPUT_BYTES = 1_000_000; // per stream
|
|
55
|
+
/**
|
|
56
|
+
* Task 17 (spec D12) — output above 8 000 chars is saved to the session spill
|
|
57
|
+
* file; the result carries the first 60 + last 20 lines (the exit code stays
|
|
58
|
+
* visible) and a pointer. Sized once, here, when the result is created.
|
|
59
|
+
*/
|
|
51
60
|
export async function shellExecHandler(args, ctx) {
|
|
61
|
+
return spillToolResult(await runShellExec(args, ctx), ctx, 'shell', { label: 'command output' });
|
|
62
|
+
}
|
|
63
|
+
async function runShellExec(args, ctx) {
|
|
52
64
|
if (!args.command || typeof args.command !== 'string') {
|
|
53
65
|
return { content: 'error: command is required (non-empty string)', is_error: true };
|
|
54
66
|
}
|
|
@@ -171,18 +183,38 @@ export async function shellExecHandler(args, ctx) {
|
|
|
171
183
|
stderrTrunc = true;
|
|
172
184
|
}
|
|
173
185
|
});
|
|
186
|
+
const timedOutResult = () => {
|
|
187
|
+
const tail = (stdoutBuf ? `stdout (partial):\n${stdoutBuf}\n${stdoutTrunc ? '[truncated]\n' : ''}` : '') +
|
|
188
|
+
(stderrBuf ? `stderr (partial):\n${stderrBuf}\n${stderrTrunc ? '[truncated]\n' : ''}` : '');
|
|
189
|
+
return { content: `error: command timed out after ${timeoutMs} ms — child killed.\n${tail}`, is_error: true };
|
|
190
|
+
};
|
|
174
191
|
const timer = setTimeout(() => {
|
|
175
192
|
timedOut = true;
|
|
176
|
-
|
|
177
|
-
|
|
193
|
+
if (process.platform === 'win32') {
|
|
194
|
+
// Dry run 2026-09-25: `npm start` = npm.cmd → cmd.exe → node. Killing the
|
|
195
|
+
// direct child left the server alive holding our pipes; 'close' never
|
|
196
|
+
// fired and a 45 s call returned after 21 minutes. Kill the whole tree.
|
|
197
|
+
void killProcessTree(child.pid, child);
|
|
178
198
|
}
|
|
179
|
-
|
|
180
|
-
setTimeout(() => {
|
|
199
|
+
else {
|
|
181
200
|
try {
|
|
182
|
-
child.kill('
|
|
201
|
+
child.kill('SIGTERM');
|
|
183
202
|
}
|
|
184
203
|
catch { /* ignore */ }
|
|
185
|
-
|
|
204
|
+
setTimeout(() => {
|
|
205
|
+
try {
|
|
206
|
+
child.kill('SIGKILL');
|
|
207
|
+
}
|
|
208
|
+
catch { /* ignore */ }
|
|
209
|
+
}, KILL_GRACE_MS).unref();
|
|
210
|
+
}
|
|
211
|
+
// Never wait on an orphan: if the pipes are still open after the kill
|
|
212
|
+
// had its grace, stop reading and return the timeout result anyway.
|
|
213
|
+
setTimeout(() => {
|
|
214
|
+
child.stdout?.destroy();
|
|
215
|
+
child.stderr?.destroy();
|
|
216
|
+
settle(timedOutResult());
|
|
217
|
+
}, KILL_GRACE_MS + ORPHAN_WAIT_MS).unref();
|
|
186
218
|
}, timeoutMs);
|
|
187
219
|
timer.unref();
|
|
188
220
|
child.on('error', (err) => {
|
|
@@ -192,12 +224,7 @@ export async function shellExecHandler(args, ctx) {
|
|
|
192
224
|
child.on('close', (code, signal) => {
|
|
193
225
|
clearTimeout(timer);
|
|
194
226
|
if (timedOut) {
|
|
195
|
-
|
|
196
|
-
(stderrBuf ? `stderr (partial):\n${stderrBuf}\n${stderrTrunc ? '[truncated]\n' : ''}` : '');
|
|
197
|
-
settle({
|
|
198
|
-
content: `error: command timed out after ${timeoutMs} ms — child killed.\n${tail}`,
|
|
199
|
-
is_error: true,
|
|
200
|
-
});
|
|
227
|
+
settle(timedOutResult());
|
|
201
228
|
return;
|
|
202
229
|
}
|
|
203
230
|
const stdoutSection = `stdout:\n${stdoutBuf}${stdoutTrunc ? '\n[truncated — exceeded 1 MB cap]' : ''}`;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Task 18 (ruling F21) — parallel readers share one AdtClient, and concurrent
|
|
3
|
+
* ADT requests through it are unverified. Readers run their MODEL work in
|
|
4
|
+
* parallel, but their SAP calls go through one lock: one ADT request at a
|
|
5
|
+
* time for the whole reader group.
|
|
6
|
+
*/
|
|
7
|
+
/** A FIFO mutex. A call that fails releases the lock like one that succeeds. */
|
|
8
|
+
export function createAdtLock() {
|
|
9
|
+
let tail = Promise.resolve();
|
|
10
|
+
return {
|
|
11
|
+
run(fn) {
|
|
12
|
+
const result = tail.then(() => fn());
|
|
13
|
+
tail = result.then(() => undefined, () => undefined);
|
|
14
|
+
return result;
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* A view of `adt` whose method calls run through `lock`. Methods run on the
|
|
20
|
+
* real client (`this` is the client, so its internal calls are not re-queued);
|
|
21
|
+
* non-function properties pass through. Every AdtClient method a reader tool
|
|
22
|
+
* calls is async, so returning a promise changes nothing for the caller.
|
|
23
|
+
*/
|
|
24
|
+
export function serializeAdt(adt, lock) {
|
|
25
|
+
return new Proxy(adt, {
|
|
26
|
+
get(target, prop, _receiver) {
|
|
27
|
+
const value = Reflect.get(target, prop, target);
|
|
28
|
+
if (typeof value !== 'function')
|
|
29
|
+
return value;
|
|
30
|
+
return (...args) => lock.run(() => value.apply(target, args));
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
}
|
|
@@ -122,6 +122,8 @@ export async function agentRunHandler(args, ctx) {
|
|
|
122
122
|
cwd: ctx.cwd,
|
|
123
123
|
provider: ctx.provider,
|
|
124
124
|
skillSource: ctx.skillSource,
|
|
125
|
+
// Task 18 fix M4 — an agent_run child never starts reader subagents.
|
|
126
|
+
subagent: true,
|
|
125
127
|
// previewHook + pendingDispatch + currentTransport + todoEmitter
|
|
126
128
|
// intentionally omitted.
|
|
127
129
|
};
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* read_agent — Task 18 (model control, 2026-09-27, spec D18).
|
|
3
|
+
*
|
|
4
|
+
* Heavy reading (many sources, a where-used walk, ATC-finding triage, a
|
|
5
|
+
* package inventory) runs in a lean child conversation and only a short
|
|
6
|
+
* report returns to the parent's history. Unlike agent_run (flag-gated, a
|
|
7
|
+
* full skill prefix per child), a reader carries:
|
|
8
|
+
* - a ~300-token system prompt owned by the proxy (skill header `_reader`,
|
|
9
|
+
* ruling F8; BYOK / AI-hub use the pinned copy in reader-prompt.ts),
|
|
10
|
+
* - a fixed small tool list (READER_TOOLS: read-only, no tool search),
|
|
11
|
+
* - the session model at effort `low`, role `reader` (never enforced, F7),
|
|
12
|
+
* - a capped report: overflow is saved to a file and the tail is cut.
|
|
13
|
+
*
|
|
14
|
+
* Read-only and depth 1 are enforced by `ctx.readerMode` at dispatch (F22).
|
|
15
|
+
* The loop runs consecutive read_agent calls concurrently, each with its own
|
|
16
|
+
* ctx clone whose `adt` is serialized through one lock (F21). The child's
|
|
17
|
+
* spend is added to the parent's per-turn `readerSpend` (F24); the child also
|
|
18
|
+
* writes its own reader-*-cost.jsonl.
|
|
19
|
+
*/
|
|
20
|
+
import { EventEmitter } from 'node:events';
|
|
21
|
+
import { listTools, registerTool, toolsForContext } from '../index.js';
|
|
22
|
+
import { runTurn } from '../../agent/loop.js';
|
|
23
|
+
import { collectTurnAssistantText } from '../../agent/turn-assistant-text.js';
|
|
24
|
+
import { newSession } from '../../session/schema.js';
|
|
25
|
+
import { computeSessionCost } from '../../cost/session-cost.js';
|
|
26
|
+
import { sessionIdOf, spillIfLarge } from '../result-spill.js';
|
|
27
|
+
import { INTERRUPTED_LINE, READER_SKILL, READER_SYSTEM_PROMPT, READER_TOOLS, hasReaderReadTools } from './reader-prompt.js';
|
|
28
|
+
export { INTERRUPTED_LINE, READER_SKILL, READER_SYSTEM_PROMPT, READER_TOOLS, hasReaderReadTools };
|
|
29
|
+
export const BRIEF_MAX_CHARS = 4_000;
|
|
30
|
+
export const DEFAULT_REPORT_MAX_CHARS = 3_000;
|
|
31
|
+
export const HARD_REPORT_MAX_CHARS = 6_000;
|
|
32
|
+
/**
|
|
33
|
+
* Final review tools-I4 — told to every reader. The proxy owns the reader's
|
|
34
|
+
* system prompt (pinned byte-equal), so the CLI says this in the child's
|
|
35
|
+
* user message.
|
|
36
|
+
*/
|
|
37
|
+
export const PARENT_OUTPUT_LINE = 'A saved-output path in this brief (a scan or ATC result the main session already ran) can be read with ' +
|
|
38
|
+
'tool_output_read. Read it instead of repeating the scan: never re-run ATC or a scan whose output you were given.';
|
|
39
|
+
/** The result when the account's proxy refuses the `_reader` skill. */
|
|
40
|
+
export const NOT_ENABLED_LINE = 'readers are not enabled for this account — read the sources directly';
|
|
41
|
+
function reportCap(raw) {
|
|
42
|
+
if (typeof raw !== 'number' || !Number.isFinite(raw) || raw < 1)
|
|
43
|
+
return DEFAULT_REPORT_MAX_CHARS;
|
|
44
|
+
return Math.min(Math.floor(raw), HARD_REPORT_MAX_CHARS);
|
|
45
|
+
}
|
|
46
|
+
function isNotEntitled(err) {
|
|
47
|
+
const e = err;
|
|
48
|
+
if (e?.error?.error === 'skill_not_in_entitlements')
|
|
49
|
+
return true;
|
|
50
|
+
return typeof e?.message === 'string' && e.message.includes('skill_not_in_entitlements');
|
|
51
|
+
}
|
|
52
|
+
export async function readAgentHandler(args, ctx) {
|
|
53
|
+
const brief = args?.brief;
|
|
54
|
+
if (typeof brief !== 'string' || brief.trim().length === 0) {
|
|
55
|
+
return { content: "error: 'brief' is required: say what to read and what to report.", is_error: true };
|
|
56
|
+
}
|
|
57
|
+
if (brief.length > BRIEF_MAX_CHARS) {
|
|
58
|
+
return { content: `error: 'brief' is ${brief.length} chars; keep it under ${BRIEF_MAX_CHARS}.`, is_error: true };
|
|
59
|
+
}
|
|
60
|
+
// Depth 1 (dispatch refuses this too; belt and braces for direct calls).
|
|
61
|
+
if (ctx.readerMode) {
|
|
62
|
+
return { content: 'error: a reader cannot start another reader.', is_error: true };
|
|
63
|
+
}
|
|
64
|
+
// Fix M4 — an agent_run child does its own reading.
|
|
65
|
+
if (ctx.subagent) {
|
|
66
|
+
return { content: 'error: a subagent cannot start a reader — read the sources directly.', is_error: true };
|
|
67
|
+
}
|
|
68
|
+
// Fix M5 — nothing a reader could read here (standalone, file tools off).
|
|
69
|
+
if (!hasReaderReadTools(new Set(toolsForContext(listTools(), ctx).map((t) => t.name)))) {
|
|
70
|
+
return { content: 'error: no read tools are available to a reader here — read the sources directly.', is_error: true };
|
|
71
|
+
}
|
|
72
|
+
if (ctx.signal?.aborted)
|
|
73
|
+
return { content: INTERRUPTED_LINE, is_error: true };
|
|
74
|
+
if (!ctx.provider) {
|
|
75
|
+
return { content: 'error: read_agent requires ctx.provider — not wired into this ToolContext.', is_error: true };
|
|
76
|
+
}
|
|
77
|
+
const cap = reportCap(args.report_max_chars);
|
|
78
|
+
// The session model, never a cheaper one (owner decision 11); effort low.
|
|
79
|
+
const model = ctx.session.model;
|
|
80
|
+
const childSession = newSession(`reader-${Date.now()}-${Math.floor(Math.random() * 1e9).toString(16)}`, ctx.session.sap_system ?? null, READER_SKILL, model);
|
|
81
|
+
// Only what a read needs. No previewHook (no writes), no chunkEmitter /
|
|
82
|
+
// todoEmitter (the child never paints the parent UI), no pendingDispatch /
|
|
83
|
+
// currentTransport (the child never changes parent REPL state).
|
|
84
|
+
const parentSessionId = sessionIdOf(ctx);
|
|
85
|
+
const childCtx = {
|
|
86
|
+
adt: ctx.adt,
|
|
87
|
+
sapAlias: ctx.sapAlias,
|
|
88
|
+
session: childSession,
|
|
89
|
+
cwd: ctx.cwd,
|
|
90
|
+
provider: ctx.provider,
|
|
91
|
+
skillSource: ctx.skillSource,
|
|
92
|
+
readerMode: true,
|
|
93
|
+
// Final review tools-I4 — the reader may open the parent's saved outputs.
|
|
94
|
+
...(parentSessionId !== undefined ? { spillParentSessionId: parentSessionId } : {}),
|
|
95
|
+
};
|
|
96
|
+
const userMessage = `${brief}\n\n${PARENT_OUTPUT_LINE}\n\nReport limit: ${cap} characters.`;
|
|
97
|
+
let failure = null;
|
|
98
|
+
try {
|
|
99
|
+
await runTurn({
|
|
100
|
+
provider: ctx.provider,
|
|
101
|
+
userMessage,
|
|
102
|
+
skill: READER_SKILL,
|
|
103
|
+
ctx: childCtx,
|
|
104
|
+
// Null sink: the child's text lands in childSession.messages only.
|
|
105
|
+
chunkEmitter: new EventEmitter(),
|
|
106
|
+
modelRole: 'reader',
|
|
107
|
+
toolsOverride: READER_TOOLS,
|
|
108
|
+
effortOverride: 'low',
|
|
109
|
+
modelOverride: model,
|
|
110
|
+
suppressSaveHook: true,
|
|
111
|
+
// Fix I1 — Esc / Ctrl+C on the parent turn stops the reader's stream.
|
|
112
|
+
signal: ctx.signal,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
catch (err) {
|
|
116
|
+
failure = err;
|
|
117
|
+
}
|
|
118
|
+
finally {
|
|
119
|
+
// Spend counts whether or not the reader finished.
|
|
120
|
+
if (ctx.readerSpend) {
|
|
121
|
+
ctx.readerSpend.cost += computeSessionCost(childSession);
|
|
122
|
+
ctx.readerSpend.calls += 1;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (failure !== null) {
|
|
126
|
+
if (ctx.signal?.aborted)
|
|
127
|
+
return { content: INTERRUPTED_LINE, is_error: true };
|
|
128
|
+
if (isNotEntitled(failure))
|
|
129
|
+
return { content: NOT_ENABLED_LINE, is_error: true };
|
|
130
|
+
const msg = failure instanceof Error ? failure.message : String(failure);
|
|
131
|
+
return { content: `error: the reader failed — ${msg.slice(0, 300)}`, is_error: true };
|
|
132
|
+
}
|
|
133
|
+
const text = collectTurnAssistantText(childSession.messages, 0).trim();
|
|
134
|
+
if (text.length === 0) {
|
|
135
|
+
return {
|
|
136
|
+
content: 'error: the reader returned no report — read the sources directly.',
|
|
137
|
+
is_error: true,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
if (text.length <= cap)
|
|
141
|
+
return { content: text };
|
|
142
|
+
// Over the cap: the full report goes to the PARENT's spill area under the
|
|
143
|
+
// parent's tool_use id, where tool_output_read can open it.
|
|
144
|
+
const head = text.slice(0, cap);
|
|
145
|
+
const sessionId = sessionIdOf(ctx);
|
|
146
|
+
const spilled = sessionId !== undefined
|
|
147
|
+
? spillIfLarge({ text, sessionId, toolUseId: ctx.toolUseId, kind: 'report', threshold: cap })
|
|
148
|
+
: null;
|
|
149
|
+
const where = spilled?.spilled ? ` — full report saved to ${spilled.path}` : '';
|
|
150
|
+
return { content: `${head}\n[report cut at ${cap} chars${where}]` };
|
|
151
|
+
}
|
|
152
|
+
registerTool({
|
|
153
|
+
name: 'read_agent',
|
|
154
|
+
description: 'Hand heavy reading to a lean read-only reader; only its short report comes back. Use it for more ' +
|
|
155
|
+
'than two source reads, a where-used scan, ATC-finding triage or a package inventory when the turn ' +
|
|
156
|
+
'will keep working afterwards. The reader has read-only SAP and file tools; it cannot write, ask the ' +
|
|
157
|
+
'user or start another reader. Give a precise brief: what to read and what to report. To hand over ' +
|
|
158
|
+
'a result you already have (ATC findings), put its saved-output path in the brief: the reader reads it ' +
|
|
159
|
+
'and never re-runs the scan. Send ' +
|
|
160
|
+
'independent readers in one response; they run at the same time.',
|
|
161
|
+
isMutating: false,
|
|
162
|
+
category: 'subagent',
|
|
163
|
+
input_schema: {
|
|
164
|
+
type: 'object',
|
|
165
|
+
properties: {
|
|
166
|
+
brief: {
|
|
167
|
+
type: 'string',
|
|
168
|
+
description: 'What to read (objects, includes, packages) and what to report. Max 4000 characters.',
|
|
169
|
+
},
|
|
170
|
+
report_max_chars: {
|
|
171
|
+
type: 'number',
|
|
172
|
+
description: 'Report length limit. Default 3000, max 6000. Longer reports are cut and saved to a file.',
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
required: ['brief'],
|
|
176
|
+
},
|
|
177
|
+
handler: (args, ctx) => readAgentHandler(args, ctx),
|
|
178
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Task 18 (model control, 2026-09-27, spec D18, ruling F8) — the reader
|
|
3
|
+
* subagent's system prompt and tool list.
|
|
4
|
+
*
|
|
5
|
+
* The PROXY owns READER_SYSTEM_PROMPT (cspeach-proxy/src/skills/
|
|
6
|
+
* reader-prompt.ts): for the skill header `_reader` it sends that constant
|
|
7
|
+
* and ignores the client's `system`. This is the CLI's pinned copy, used only
|
|
8
|
+
* where the CLI talks to a model directly (BYOK, AI-hub, local).
|
|
9
|
+
* src/tools/__tests__/reader-prompt-pin.test.ts asserts the two template
|
|
10
|
+
* literals stay byte-equal — edit both files together.
|
|
11
|
+
*
|
|
12
|
+
* No imports on purpose: tool-dispatch.ts reads READER_TOOLS, and importing
|
|
13
|
+
* read_agent.ts there would close a cycle through agent/loop.ts.
|
|
14
|
+
*/
|
|
15
|
+
/** The skill header a reader's model calls carry. */
|
|
16
|
+
export const READER_SKILL = '_reader';
|
|
17
|
+
export const READER_SYSTEM_PROMPT = `You are a read-only investigator inside CSPeach. Read exactly what the brief asks for with the tools you have, then answer the brief. Report facts with object names, includes and line numbers. Say what you could not find. No recommendations unless the brief asks. Stay under the report limit.`;
|
|
18
|
+
/**
|
|
19
|
+
* The reader's fixed, small, static tool list (no deferred entries, no tool
|
|
20
|
+
* search): the ten read-only tools of spec D18 plus tool_output_read (Task
|
|
21
|
+
* 17), so a reader can open a result that was saved to a file. Every entry is
|
|
22
|
+
* `isMutating: false`. Flag-gated entries (file_read, grep, glob) reach the
|
|
23
|
+
* reader only when the session has them on, and standalone hides the SAP ones.
|
|
24
|
+
* tool-dispatch.ts refuses every other tool while `ctx.readerMode` is set.
|
|
25
|
+
*/
|
|
26
|
+
export const READER_TOOLS = Object.freeze([
|
|
27
|
+
'sap_get_source',
|
|
28
|
+
'sap_object_structure',
|
|
29
|
+
'sap_search_object',
|
|
30
|
+
'sap_sql_query',
|
|
31
|
+
'sap_usage_references',
|
|
32
|
+
'sap_class_includes',
|
|
33
|
+
'sap_atc_run',
|
|
34
|
+
'file_read',
|
|
35
|
+
'grep',
|
|
36
|
+
'glob',
|
|
37
|
+
'tool_output_read',
|
|
38
|
+
]);
|
|
39
|
+
/** Task 18 fix I1 — the result of a reader stopped by Esc / Ctrl+C (or never started). */
|
|
40
|
+
export const INTERRUPTED_LINE = 'interrupted: the reader was stopped before it finished';
|
|
41
|
+
/**
|
|
42
|
+
* Task 18 fix M5 — true when a reader would have at least one real read tool
|
|
43
|
+
* among `visibleNames` (tool_output_read alone reads nothing new: standalone
|
|
44
|
+
* with the file tools off). The loop hides read_agent otherwise.
|
|
45
|
+
*/
|
|
46
|
+
export function hasReaderReadTools(visibleNames) {
|
|
47
|
+
return READER_TOOLS.some((n) => n !== 'tool_output_read' && visibleNames.has(n));
|
|
48
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Round 7 item 8e — which objects' LAST syntax check found errors (this CLI
|
|
3
|
+
* process). Written by every syntax check the CLI runs (the Rule 10 write
|
|
4
|
+
* gate and the sap_syntax_check tool); read by the approval risk floor (such
|
|
5
|
+
* an object never auto-approves) and by sap_activate (an automatically minted
|
|
6
|
+
* approval never activates it). A clean check clears the object.
|
|
7
|
+
*
|
|
8
|
+
* Review r7 M-3 — keyed by object TYPE and name: a PROG and a CLAS with the
|
|
9
|
+
* same name never share the flag. `PROG/P` and `PROG` are the same type.
|
|
10
|
+
*
|
|
11
|
+
* Re-review 2 item 2 — a name index of flagged objects. When either side's
|
|
12
|
+
* type is not a known ADT code (a recorded "ABAP CLASS", a queried "some
|
|
13
|
+
* thing"), the object is matched by name alone: over-match, never miss.
|
|
14
|
+
*/
|
|
15
|
+
import { normaliseAdtType } from '../approvals/adt-type.js';
|
|
16
|
+
const UNKNOWN = '?';
|
|
17
|
+
/** name → the type codes (or UNKNOWN) flagged under that name. */
|
|
18
|
+
const byName = new Map();
|
|
19
|
+
const nameOf = (name) => name.trim().toUpperCase();
|
|
20
|
+
const codeOf = (type) => normaliseAdtType(type) ?? UNKNOWN;
|
|
21
|
+
export function recordSyntaxCheck(type, name, hasErrors) {
|
|
22
|
+
const n = nameOf(name);
|
|
23
|
+
const c = codeOf(type);
|
|
24
|
+
const set = byName.get(n) ?? new Set();
|
|
25
|
+
if (hasErrors)
|
|
26
|
+
set.add(c);
|
|
27
|
+
else {
|
|
28
|
+
set.delete(c);
|
|
29
|
+
// Final review A M-3 — a clean check under a KNOWN code also clears an
|
|
30
|
+
// entry recorded under an unknown type word for the same name.
|
|
31
|
+
if (c !== UNKNOWN)
|
|
32
|
+
set.delete(UNKNOWN);
|
|
33
|
+
}
|
|
34
|
+
if (set.size > 0)
|
|
35
|
+
byName.set(n, set);
|
|
36
|
+
else
|
|
37
|
+
byName.delete(n);
|
|
38
|
+
}
|
|
39
|
+
export function hasRecordedSyntaxErrors(type, name) {
|
|
40
|
+
const set = byName.get(nameOf(name));
|
|
41
|
+
if (!set || set.size === 0)
|
|
42
|
+
return false;
|
|
43
|
+
const c = codeOf(type);
|
|
44
|
+
return c === UNKNOWN || set.has(c) || set.has(UNKNOWN);
|
|
45
|
+
}
|
|
46
|
+
/** Final review A M-1 — any flagged entry with this name, whatever its type. */
|
|
47
|
+
export function hasRecordedSyntaxErrorsByName(name) {
|
|
48
|
+
return (byName.get(nameOf(name))?.size ?? 0) > 0;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Final review A M-2 — a successful activation verified against the
|
|
52
|
+
* inactive-objects list proves the current source compiles: clear the name.
|
|
53
|
+
*/
|
|
54
|
+
export function clearSyntaxErrorsAfterActivation(name) {
|
|
55
|
+
byName.delete(nameOf(name));
|
|
56
|
+
}
|
|
57
|
+
/** `TYPE:NAME` keys (TYPE is an ADT code, or `?` when the type word was unknown). */
|
|
58
|
+
export function objectsWithSyntaxErrors() {
|
|
59
|
+
const out = [];
|
|
60
|
+
for (const [n, set] of byName)
|
|
61
|
+
for (const c of set)
|
|
62
|
+
out.push(`${c}:${n}`);
|
|
63
|
+
return out;
|
|
64
|
+
}
|
|
65
|
+
export function resetSyntaxStateForTest() {
|
|
66
|
+
byName.clear();
|
|
67
|
+
}
|
package/dist/tools/todo.js
CHANGED
|
@@ -7,14 +7,14 @@
|
|
|
7
7
|
*
|
|
8
8
|
* Semantics: FULL-LIST REPLACE on every call — never a delta. 1..20 items.
|
|
9
9
|
*
|
|
10
|
-
* Transcript render (CC-style
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
10
|
+
* Transcript render (piece 2, spec §4 — was CC-style 2026-07): WHERE the
|
|
11
|
+
* checklist is drawn depends on the render mode (renderer/checklist-format.ts
|
|
12
|
+
* checklistRenderMode). Ink draws a LIVE checklist (ui/checklist.tsx) that
|
|
13
|
+
* redraws in place, with one close line at turn end; classic interactive
|
|
14
|
+
* prints the block once at turn end (repl/plan-turn-end.ts); only headless /
|
|
15
|
+
* one-shot emit the per-set scrollback block (renderer/todo-block.ts) via
|
|
16
|
+
* ctx.chunkEmitter, so CI logs keep the progression. The raw ⏺/⎿ todo_set
|
|
17
|
+
* rows are widget-suppressed (renderer/tool-widget.ts) in every mode.
|
|
18
18
|
*
|
|
19
19
|
* Session-write route (chosen with evidence, 2026-07-05): the tool mutates
|
|
20
20
|
* `ctx.session.todos` directly. ToolContext.session IS the live
|
|
@@ -45,6 +45,8 @@
|
|
|
45
45
|
*/
|
|
46
46
|
import { registerTool } from './index.js';
|
|
47
47
|
import { formatTodoChecklistBlock } from '../renderer/todo-block.js';
|
|
48
|
+
import { checklistRenderMode } from '../renderer/checklist-format.js';
|
|
49
|
+
import { markTodoSetThisTurn } from '../ui/todo-emitter.js';
|
|
48
50
|
/** Hard cap on list length — past this the list stops being a glanceable plan. */
|
|
49
51
|
export const TODO_MAX_ITEMS = 20;
|
|
50
52
|
const VALID_STATUSES = new Set(['pending', 'in_progress', 'completed']);
|
|
@@ -85,16 +87,19 @@ export async function todoSetHandler(args, ctx) {
|
|
|
85
87
|
// copy so a consumer mutating the payload can never corrupt the
|
|
86
88
|
// persisted session state.
|
|
87
89
|
ctx.todoEmitter?.emit('update', todos.map((t) => ({ ...t })));
|
|
88
|
-
//
|
|
89
|
-
//
|
|
90
|
-
//
|
|
91
|
-
//
|
|
92
|
-
//
|
|
93
|
-
//
|
|
94
|
-
// (
|
|
95
|
-
//
|
|
96
|
-
|
|
97
|
-
|
|
90
|
+
// Piece 2 (spec §4) — where the checklist is drawn depends on the mode.
|
|
91
|
+
// Ink: the live checklist (ui/checklist.tsx) redraws in place — no
|
|
92
|
+
// scrollback reprint per update; one close line lands at turn end
|
|
93
|
+
// (repl/plan-turn-end.ts). Classic interactive: the block prints once, at
|
|
94
|
+
// turn end. Headless / one-shot: the block still prints on EVERY update so
|
|
95
|
+
// CI logs keep the progression. The raw ⏺/⎿ todo_set rows stay
|
|
96
|
+
// widget-suppressed (renderer/tool-widget.ts). Subagent ctxs omit both
|
|
97
|
+
// emitters, so a child todo_set never reaches the parent's transcript.
|
|
98
|
+
if (checklistRenderMode() === 'per-update') {
|
|
99
|
+
ctx.chunkEmitter?.emit('chunk', formatTodoChecklistBlock(todos));
|
|
100
|
+
}
|
|
101
|
+
if (ctx.todoEmitter)
|
|
102
|
+
markTodoSetThisTurn();
|
|
98
103
|
const result = {
|
|
99
104
|
count: todos.length,
|
|
100
105
|
// LAST-wins, matching the app strip and Ctrl+T panel (both `.at(-1)`).
|
|
@@ -119,7 +124,9 @@ registerTool({
|
|
|
119
124
|
'in_progress at a time. The user sees this list — keep items short and outcome-shaped. ' +
|
|
120
125
|
'Update at EVERY step transition: when you finish a step, immediately call todo_set ' +
|
|
121
126
|
'marking it completed and the next step in_progress. Never batch several finished steps ' +
|
|
122
|
-
'into one later call — the user watches the ▶ indicator live.'
|
|
127
|
+
'into one later call — the user watches the ▶ indicator live. ' +
|
|
128
|
+
// Task 16 (cost profile lever 7): a lone todo_set round re-reads the whole context.
|
|
129
|
+
'Call todo_set in the same response as your next action, never as the only tool call in a response.',
|
|
123
130
|
isMutating: false,
|
|
124
131
|
category: 'session',
|
|
125
132
|
input_schema: {
|