mixdog 0.9.15 → 0.9.17
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/package.json +2 -1
- package/scripts/atomic-lock-tryonce-test.mjs +66 -0
- package/scripts/explore-bench.mjs +5 -4
- package/scripts/ingest-pure-conversation-smoke.mjs +27 -0
- package/scripts/output-style-smoke.mjs +3 -3
- package/scripts/provider-toolcall-test.mjs +79 -2
- package/scripts/termio-input-smoke.mjs +199 -0
- package/scripts/tool-efficiency-diag.mjs +88 -0
- package/scripts/tool-smoke.mjs +40 -24
- package/scripts/tui-frame-harness-shim.mjs +32 -0
- package/scripts/tui-frame-harness.mjs +306 -0
- package/src/agents/heavy-worker/AGENT.md +6 -7
- package/src/agents/worker/AGENT.md +6 -7
- package/src/lib/keychain-cjs.cjs +28 -11
- package/src/lib/rules-builder.cjs +6 -10
- package/src/mixdog-session-runtime.mjs +102 -20
- package/src/output-styles/simple.md +22 -24
- package/src/rules/agent/00-core.md +12 -11
- package/src/rules/agent/30-explorer.md +22 -21
- package/src/rules/lead/01-general.md +8 -8
- package/src/rules/lead/lead-brief.md +11 -12
- package/src/rules/shared/01-tool.md +25 -14
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +4 -3
- package/src/runtime/agent/orchestrator/config.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +33 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +31 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +49 -0
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +35 -1
- package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +29 -8
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +13 -5
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/model-list-sanitize.mjs +11 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +18 -0
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +30 -2
- package/src/runtime/agent/orchestrator/session/cache/scoped-cache.mjs +19 -0
- package/src/runtime/agent/orchestrator/session/compact/constants.mjs +2 -2
- package/src/runtime/agent/orchestrator/session/compact/engine.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/compact/summary.mjs +37 -15
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +62 -0
- package/src/runtime/agent/orchestrator/session/lifecycle-scan.mjs +177 -0
- package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +12 -19
- package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +10 -0
- package/src/runtime/agent/orchestrator/session/loop.mjs +30 -1
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +8 -0
- package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +13 -18
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +37 -19
- package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +4 -1
- package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +142 -3
- package/src/runtime/agent/orchestrator/session/manager.mjs +59 -2
- package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +118 -22
- package/src/runtime/agent/orchestrator/session/store.mjs +38 -25
- package/src/runtime/agent/orchestrator/stall-policy.mjs +22 -12
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +4 -2
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +7 -5
- package/src/runtime/agent/orchestrator/tools/builtin/path-diagnostics.mjs +38 -1
- package/src/runtime/agent/orchestrator/tools/builtin/path-utils.mjs +24 -0
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +17 -16
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +14 -1
- package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +25 -1
- package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +73 -3
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +23 -10
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +55 -0
- package/src/runtime/agent/orchestrator/tools/builtin/snapshot-store.mjs +10 -2
- package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +8 -2
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +21 -13
- package/src/runtime/agent/orchestrator/tools/patch/paths.mjs +13 -2
- package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +44 -6
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +5 -0
- package/src/runtime/channels/backends/telegram.mjs +29 -9
- package/src/runtime/channels/lib/event-queue.mjs +6 -2
- package/src/runtime/channels/lib/memory-client.mjs +66 -1
- package/src/runtime/channels/lib/runtime-paths.mjs +8 -19
- package/src/runtime/channels/lib/webhook/deliveries.mjs +22 -1
- package/src/runtime/memory/index.mjs +132 -8
- package/src/runtime/memory/lib/cycle-scheduler.mjs +24 -5
- package/src/runtime/memory/lib/embedding-warmup.mjs +3 -0
- package/src/runtime/memory/lib/ko-morph.mjs +1 -0
- package/src/runtime/memory/lib/memory-cycle1.mjs +45 -3
- package/src/runtime/memory/lib/memory-cycle2-gate.mjs +7 -3
- package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +54 -10
- package/src/runtime/memory/lib/memory-cycle2.mjs +21 -8
- package/src/runtime/memory/lib/memory-embed.mjs +48 -0
- package/src/runtime/memory/lib/memory-recall-store.mjs +57 -13
- package/src/runtime/memory/lib/memory.mjs +88 -1
- package/src/runtime/memory/lib/session-ingest.mjs +34 -3
- package/src/runtime/memory/lib/trace-store.mjs +52 -3
- package/src/runtime/memory/lib/transcript-ingest.mjs +27 -4
- package/src/runtime/shared/atomic-file.mjs +110 -0
- package/src/runtime/shared/child-guardian.mjs +4 -2
- package/src/runtime/shared/open-url.mjs +2 -1
- package/src/runtime/shared/singleton-owner.mjs +26 -0
- package/src/runtime/shared/tool-execution-contract.mjs +25 -0
- package/src/runtime/shared/transcript-writer.mjs +49 -5
- package/src/session-runtime/config-helpers.mjs +7 -2
- package/src/session-runtime/cwd-plugins.mjs +6 -1
- package/src/session-runtime/effort.mjs +6 -1
- package/src/session-runtime/plugin-mcp.mjs +116 -12
- package/src/session-runtime/provider-models.mjs +47 -8
- package/src/session-runtime/settings-api.mjs +7 -1
- package/src/standalone/channel-worker.mjs +25 -3
- package/src/standalone/explore-tool.mjs +5 -5
- package/src/standalone/hook-bus/config.mjs +38 -2
- package/src/standalone/hook-bus/handlers.mjs +103 -11
- package/src/standalone/hook-bus.mjs +20 -6
- package/src/standalone/memory-runtime-proxy.mjs +40 -5
- package/src/tui/App.jsx +214 -30
- package/src/tui/app/core-memory-picker.mjs +6 -6
- package/src/tui/app/model-options.mjs +10 -4
- package/src/tui/app/settings-picker.mjs +5 -30
- package/src/tui/app/slash-commands.mjs +0 -1
- package/src/tui/app/slash-dispatch.mjs +0 -16
- package/src/tui/app/text-layout.mjs +57 -0
- package/src/tui/app/transcript-window.mjs +162 -2
- package/src/tui/app/use-mouse-input.mjs +60 -47
- package/src/tui/app/use-transcript-window.mjs +96 -17
- package/src/tui/components/PromptInput.jsx +18 -1
- package/src/tui/components/StatusLine.jsx +1 -1
- package/src/tui/components/TextEntryPanel.jsx +97 -33
- package/src/tui/dist/index.mjs +856 -288
- package/src/tui/engine/tool-card-results.mjs +22 -5
- package/src/tui/engine/tui-steering-persist.mjs +66 -35
- package/src/tui/engine.mjs +41 -9
- package/src/tui/index.jsx +101 -7
- package/src/ui/statusline-segments.mjs +54 -36
- package/src/ui/statusline.mjs +141 -95
- package/src/workflows/default/WORKFLOW.md +27 -31
- package/vendor/ink/build/components/App.js +62 -17
- package/vendor/ink/build/ink.js +78 -3
- package/vendor/ink/build/input-parser.d.ts +9 -4
- package/vendor/ink/build/input-parser.js +45 -176
- package/vendor/ink/build/log-update.js +47 -2
- package/vendor/ink/build/termio-keypress.js +240 -0
- package/vendor/ink/build/termio-tokenize.js +253 -0
package/scripts/tool-smoke.mjs
CHANGED
|
@@ -42,6 +42,7 @@ import { mergeSessionRowsIntoGlobal } from '../src/runtime/memory/lib/memory-ses
|
|
|
42
42
|
import { TOOL_DEFS as SEARCH_TOOL_DEFS } from '../src/runtime/search/tool-defs.mjs';
|
|
43
43
|
import { TOOL_DEFS as CHANNEL_TOOL_DEFS } from '../src/runtime/channels/tool-defs.mjs';
|
|
44
44
|
import { AGENT_OWNER } from '../src/runtime/agent/orchestrator/agent-owner.mjs';
|
|
45
|
+
import { recursiveWrapperToolNameForPublicAgent } from '../src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs';
|
|
45
46
|
import { composeSystemPrompt } from '../src/runtime/agent/orchestrator/context/collect.mjs';
|
|
46
47
|
import { setInternalToolsProvider } from '../src/runtime/agent/orchestrator/internal-tools.mjs';
|
|
47
48
|
import { prepareAgentSession } from '../src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs';
|
|
@@ -386,7 +387,7 @@ assertOk('glob explicit src', explicitSrcGlobOut, /src[\\/].*engine\.mjs/i);
|
|
|
386
387
|
|
|
387
388
|
const globPathOnlyOut = await executeBuiltinTool('glob', {
|
|
388
389
|
path: 'scripts',
|
|
389
|
-
head_limit:
|
|
390
|
+
head_limit: 200,
|
|
390
391
|
}, root);
|
|
391
392
|
assertOk('glob path-only default *', globPathOnlyOut, /tool-smoke\.mjs/i);
|
|
392
393
|
|
|
@@ -398,8 +399,8 @@ const grepNoPatternGlobOut = await executeBuiltinTool('grep', {
|
|
|
398
399
|
assertOk('grep without pattern routes to glob', grepNoPatternGlobOut, /tool-smoke\.mjs/i);
|
|
399
400
|
|
|
400
401
|
const grepManyPatterns = [
|
|
401
|
-
...Array.from({ length: 20 }, (_, i) => `__tool_smoke_miss_${i}__`),
|
|
402
402
|
'tool-smoke',
|
|
403
|
+
...Array.from({ length: 20 }, (_, i) => `__tool_smoke_miss_${i}__`),
|
|
403
404
|
];
|
|
404
405
|
const grepManyPatternsOut = await executeBuiltinTool('grep', {
|
|
405
406
|
pattern: grepManyPatterns,
|
|
@@ -407,12 +408,12 @@ const grepManyPatternsOut = await executeBuiltinTool('grep', {
|
|
|
407
408
|
glob: '*.mjs',
|
|
408
409
|
head_limit: 5,
|
|
409
410
|
}, root);
|
|
410
|
-
if (/exceeds the
|
|
411
|
-
throw new Error(`grep should
|
|
411
|
+
if (/exceeds the \d+-pattern cap/i.test(String(grepManyPatternsOut))) {
|
|
412
|
+
throw new Error(`grep should truncate oversized pattern[] instead of error:\n${grepManyPatternsOut.slice(0, 400)}`);
|
|
412
413
|
}
|
|
413
|
-
assertOk('grep >
|
|
414
|
-
if (!/\[
|
|
415
|
-
throw new Error(`grep >
|
|
414
|
+
assertOk('grep >10 pattern cap keeps first patterns', grepManyPatternsOut, /tool-smoke\.mjs/i);
|
|
415
|
+
if (!/\[capped at 10 of 21 patterns\]/.test(String(grepManyPatternsOut))) {
|
|
416
|
+
throw new Error(`grep >10 patterns should emit cap note:\n${grepManyPatternsOut.slice(0, 400)}`);
|
|
416
417
|
}
|
|
417
418
|
|
|
418
419
|
function grepCountTotalMatches(body) {
|
|
@@ -431,9 +432,8 @@ if (singleCountTotal == null || singleCountTotal < 1) {
|
|
|
431
432
|
}
|
|
432
433
|
|
|
433
434
|
const grepCountOverlapPatterns = [
|
|
434
|
-
...Array.from({ length: 19 }, (_, i) => `__count_overlap_a_${i}__`),
|
|
435
435
|
'assertOk',
|
|
436
|
-
...Array.from({ length:
|
|
436
|
+
...Array.from({ length: 8 }, (_, i) => `__count_overlap_a_${i}__`),
|
|
437
437
|
'assertOk',
|
|
438
438
|
];
|
|
439
439
|
const grepCountOverlapOut = await executeBuiltinTool('grep', {
|
|
@@ -444,36 +444,37 @@ const grepCountOverlapOut = await executeBuiltinTool('grep', {
|
|
|
444
444
|
const overlapCountTotal = grepCountTotalMatches(grepCountOverlapOut);
|
|
445
445
|
if (overlapCountTotal !== singleCountTotal) {
|
|
446
446
|
throw new Error(
|
|
447
|
-
`
|
|
447
|
+
`multi-pattern count must not double-count overlapping lines (single=${singleCountTotal} overlap=${overlapCountTotal}):\n${grepCountOverlapOut.slice(0, 600)}`,
|
|
448
448
|
);
|
|
449
449
|
}
|
|
450
450
|
|
|
451
451
|
const grepChunkContextPatterns = [
|
|
452
|
-
...Array.from({ length: 20 }, (_, i) => `__ctx_chunk_miss_${i}__`),
|
|
453
452
|
'grepCountTotalMatches',
|
|
453
|
+
...Array.from({ length: 20 }, (_, i) => `__ctx_chunk_miss_${i}__`),
|
|
454
454
|
];
|
|
455
455
|
const grepChunkContextOut = await executeBuiltinTool('grep', {
|
|
456
456
|
pattern: grepChunkContextPatterns,
|
|
457
|
-
path: 'scripts
|
|
457
|
+
path: 'scripts',
|
|
458
|
+
glob: 'tool-smoke.mjs',
|
|
458
459
|
'-C': 1,
|
|
459
460
|
head_limit: 30,
|
|
460
461
|
}, root);
|
|
461
|
-
if (!/\[
|
|
462
|
-
throw new Error(`
|
|
462
|
+
if (!/\[capped at 10 of 21 patterns\]/.test(String(grepChunkContextOut))) {
|
|
463
|
+
throw new Error(`oversized -C pattern[] should emit cap note:\n${grepChunkContextOut.slice(0, 500)}`);
|
|
463
464
|
}
|
|
464
465
|
if (!/tool-smoke\.mjs:\d+:/.test(String(grepChunkContextOut))) {
|
|
465
|
-
throw new Error(`
|
|
466
|
+
throw new Error(`capped -C must emit path-prefixed match lines:\n${grepChunkContextOut.slice(0, 800)}`);
|
|
466
467
|
}
|
|
467
468
|
if (!/tool-smoke\.mjs-\d+-/.test(String(grepChunkContextOut))) {
|
|
468
|
-
throw new Error(`
|
|
469
|
+
throw new Error(`capped -C must keep path-prefixed context lines:\n${grepChunkContextOut.slice(0, 800)}`);
|
|
469
470
|
}
|
|
470
471
|
const ctxBodyLines = String(grepChunkContextOut).split('\n').filter((l) => l && !/^\[/.test(l) && !/^\(no matches\)/.test(l));
|
|
471
472
|
const orphanLineOnlyContext = ctxBodyLines.some((l) => /^\d+-/.test(l));
|
|
472
473
|
if (orphanLineOnlyContext) {
|
|
473
|
-
throw new Error(`
|
|
474
|
+
throw new Error(`capped -C must not leave line-only context orphans:\n${grepChunkContextOut.slice(0, 800)}`);
|
|
474
475
|
}
|
|
475
476
|
if (!/function grepCountTotalMatches/.test(String(grepChunkContextOut))) {
|
|
476
|
-
throw new Error(`
|
|
477
|
+
throw new Error(`capped -C should include match span:\n${grepChunkContextOut.slice(0, 800)}`);
|
|
477
478
|
}
|
|
478
479
|
|
|
479
480
|
const findOut = await executeBuiltinTool('find', {
|
|
@@ -1165,12 +1166,19 @@ setInternalToolsProvider({
|
|
|
1165
1166
|
if (!/Read-only retrieval role/i.test(visible) || /# environment/i.test(visible) || /git operations deferred to Lead/i.test(visible)) {
|
|
1166
1167
|
throw new Error(`explorer hidden retrieval context should stay slim: ${visible.slice(0, 1200)}`);
|
|
1167
1168
|
}
|
|
1168
|
-
if (!/# Role: explorer/i.test(systemVisible) || /# Role: explorer/i.test(userReminderVisible) || !/
|
|
1169
|
+
if (!/# Role: explorer/i.test(systemVisible) || /# Role: explorer/i.test(userReminderVisible) || !/Coordinate locator/i.test(systemVisible)) {
|
|
1169
1170
|
throw new Error(`explorer role md must ride BP2 system, not BP3 user reminder: system=${systemVisible.slice(0, 600)} user=${userReminderVisible.slice(0, 600)}`);
|
|
1170
1171
|
}
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1172
|
+
// System layers (BP1 tool policy + BP2 role md) are shared/frozen and sized
|
|
1173
|
+
// elsewhere; the slimness cap guards only the per-session injected layers
|
|
1174
|
+
// (BP3 user reminder etc.), so measure non-system messages only.
|
|
1175
|
+
const injectedVisible = (explorerSession.messages || [])
|
|
1176
|
+
.filter((m) => m?.role !== 'system')
|
|
1177
|
+
.map((m) => String(m.content || ''))
|
|
1178
|
+
.join('\n');
|
|
1179
|
+
const injectedBytes = Buffer.byteLength(injectedVisible, 'utf8');
|
|
1180
|
+
if (injectedBytes > 1800) {
|
|
1181
|
+
throw new Error(`explorer hidden retrieval context too large: ${injectedBytes} bytes (injected layers only)`);
|
|
1174
1182
|
}
|
|
1175
1183
|
} finally {
|
|
1176
1184
|
closeSession(explorerSession.id, 'tool-smoke');
|
|
@@ -1298,8 +1306,16 @@ setInternalToolsProvider({
|
|
|
1298
1306
|
if (!fullTools.includes('explore')) {
|
|
1299
1307
|
throw new Error(`full agent schema must expose explore: full=${fullTools.join(', ')}`);
|
|
1300
1308
|
}
|
|
1301
|
-
|
|
1302
|
-
|
|
1309
|
+
// Unified-shard policy (v0.9.13): the explore wrapper stays IN the schema
|
|
1310
|
+
// for every read role — including the explore agent itself — so the
|
|
1311
|
+
// read-only bundle is bit-identical across roles (one cache group).
|
|
1312
|
+
// Recursion is broken at call time in pre-dispatch-deny.mjs via
|
|
1313
|
+
// recursiveWrapperToolNameForPublicAgent, not by schema stripping.
|
|
1314
|
+
if (JSON.stringify(publicExploreTools) !== JSON.stringify(expectedReadTools)) {
|
|
1315
|
+
throw new Error(`public explore role must ship the shared read-only bundle (incl. explore): expected=${expectedReadTools.join(', ')} actual=${publicExploreTools.join(', ')}`);
|
|
1316
|
+
}
|
|
1317
|
+
if (recursiveWrapperToolNameForPublicAgent('explore') !== 'explore') {
|
|
1318
|
+
throw new Error('call-time anti-recursion must map public explore agent to its own wrapper tool');
|
|
1303
1319
|
}
|
|
1304
1320
|
} finally {
|
|
1305
1321
|
closeSession(readAgentSession.id, 'tool-smoke');
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Mirror of vendor/ink/build/ink.js shouldClearTerminalForFrame, with the
|
|
2
|
+
// Windows branch parameterized so the harness can probe both platforms without
|
|
3
|
+
// relying on process.platform. Kept byte-faithful to the source predicate so
|
|
4
|
+
// the harness reflects real clear decisions.
|
|
5
|
+
export function shouldClearTerminalForFrameProbe({
|
|
6
|
+
isTty, viewportRows, previousViewportRows, previousOutputHeight,
|
|
7
|
+
nextOutputHeight, isUnmounting, isWindows,
|
|
8
|
+
}) {
|
|
9
|
+
if (!isTty) return false;
|
|
10
|
+
const priorViewportRows = previousViewportRows ?? viewportRows;
|
|
11
|
+
const hadPreviousFrame = previousOutputHeight > 0;
|
|
12
|
+
const wasFullscreen = previousOutputHeight >= priorViewportRows;
|
|
13
|
+
const wasOverflowing = previousOutputHeight > priorViewportRows;
|
|
14
|
+
const isOverflowing = nextOutputHeight > viewportRows;
|
|
15
|
+
const isFullscreen = nextOutputHeight >= viewportRows;
|
|
16
|
+
const isLeavingFullscreen = wasFullscreen && nextOutputHeight < viewportRows;
|
|
17
|
+
const isShrinkingAtViewport = hadPreviousFrame &&
|
|
18
|
+
nextOutputHeight < previousOutputHeight &&
|
|
19
|
+
(wasFullscreen || isFullscreen || wasOverflowing || isOverflowing);
|
|
20
|
+
const shouldClearOnUnmount = isUnmounting && wasFullscreen;
|
|
21
|
+
const viewportResized = previousViewportRows != null && previousViewportRows !== viewportRows;
|
|
22
|
+
if (isWindows && (wasFullscreen || isFullscreen) &&
|
|
23
|
+
(viewportResized || isShrinkingAtViewport || isLeavingFullscreen)) {
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
return (
|
|
27
|
+
wasOverflowing ||
|
|
28
|
+
(isOverflowing && hadPreviousFrame) ||
|
|
29
|
+
isLeavingFullscreen ||
|
|
30
|
+
isShrinkingAtViewport ||
|
|
31
|
+
shouldClearOnUnmount);
|
|
32
|
+
}
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* scripts/tui-frame-harness.mjs — renderer-level frame-grid evidence harness.
|
|
4
|
+
*
|
|
5
|
+
* Drives vendor/ink log-update createIncremental(fakeStream) through the SAME
|
|
6
|
+
* wrapper logic ink.js renderInteractiveFrame() applies (fullscreen detect +
|
|
7
|
+
* trailing-newline normalization + shouldClearTerminalForFrame), replays the
|
|
8
|
+
* emitted ANSI into a minimal VT grid interpreter, and asserts the absolute row
|
|
9
|
+
* of a tracked marker line (the "prompt" / "statusline") across each frame.
|
|
10
|
+
*
|
|
11
|
+
* A deviant frame = the marker's absolute row changes for a single frame then
|
|
12
|
+
* snaps back. That is the one-row-low dip reported under Windows Terminal.
|
|
13
|
+
*
|
|
14
|
+
* Run: node scripts/tui-frame-harness.mjs
|
|
15
|
+
*/
|
|
16
|
+
// [harness] log-update reads process.platform/WT_SESSION AT IMPORT to pick its
|
|
17
|
+
// Windows-safe absolute-cursor branch. Force WT_SESSION on BEFORE importing it
|
|
18
|
+
// so POSIX/CI runs exercise the same branch WT users hit. Must precede the
|
|
19
|
+
// dynamic import below.
|
|
20
|
+
process.env.WT_SESSION = process.env.WT_SESSION || '1';
|
|
21
|
+
const { default: logUpdate } = await import('../vendor/ink/build/log-update.js');
|
|
22
|
+
const { shouldClearTerminalForFrameProbe } = await import('./tui-frame-harness-shim.mjs');
|
|
23
|
+
const { default: ansiEscapes } = await import('ansi-escapes');
|
|
24
|
+
|
|
25
|
+
// Fail loudly if the Windows-safe branch is NOT engaged: without it the harness
|
|
26
|
+
// silently tests the wrong path and reports a false pass. Probe by driving a
|
|
27
|
+
// fullscreen→one-short pair through a throwaway log and asserting the branch's
|
|
28
|
+
// absolute cursorTo(0,y) addressing (not a relative cursorUp walk) is emitted.
|
|
29
|
+
function assertWindowsBranchEngaged() {
|
|
30
|
+
const chunks = [];
|
|
31
|
+
const fs = { write: (s) => { chunks.push(s); return true; }, isTTY: true, columns: 20, rows: 4 };
|
|
32
|
+
const log = logUpdate.create(fs, { incremental: true });
|
|
33
|
+
log.sync('a\nb\nc\nd'); // 4 lines, fullscreen, no trailing nl
|
|
34
|
+
chunks.length = 0;
|
|
35
|
+
log('a\nX\nc\nd'); // change one middle row, still fullscreen
|
|
36
|
+
const emitted = chunks.join('');
|
|
37
|
+
// Windows-safe branch uses absolute cursorTo(0, i) => CSI <row>;1H. The POSIX
|
|
38
|
+
// relative branch uses cursorUp / cursorNextLine (CSI A / E) with no ;1H rows.
|
|
39
|
+
const hasAbsolute = /\x1b\[\d+;1H/.test(emitted);
|
|
40
|
+
if (!hasAbsolute) {
|
|
41
|
+
console.error('tui-frame-harness: FAIL — log-update Windows-safe branch NOT engaged '
|
|
42
|
+
+ '(WT_SESSION/platform did not force it at import). Emitted:', JSON.stringify(emitted));
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
assertWindowsBranchEngaged();
|
|
47
|
+
|
|
48
|
+
// ---- Minimal VT grid interpreter -----------------------------------------
|
|
49
|
+
// Supports the escapes log-update / ink emit: cursorTo(x[,y]), cursorUp/Down/
|
|
50
|
+
// NextLine, eraseLine, eraseEndLine, eraseLines(n), clearTerminal, plain text,
|
|
51
|
+
// newline, SGR (ignored for geometry), hide/show cursor (ignored).
|
|
52
|
+
class VT {
|
|
53
|
+
constructor(rows, cols) {
|
|
54
|
+
this.rows = rows;
|
|
55
|
+
this.cols = cols;
|
|
56
|
+
this.grid = Array.from({ length: rows }, () => '');
|
|
57
|
+
this.cx = 0;
|
|
58
|
+
this.cy = 0;
|
|
59
|
+
}
|
|
60
|
+
_clampRow() { if (this.cy < 0) this.cy = 0; if (this.cy >= this.rows) this.cy = this.rows - 1; }
|
|
61
|
+
write(data) {
|
|
62
|
+
let i = 0;
|
|
63
|
+
while (i < data.length) {
|
|
64
|
+
const ch = data[i];
|
|
65
|
+
if (ch === '\u001B') {
|
|
66
|
+
// CSI
|
|
67
|
+
if (data[i + 1] === '[') {
|
|
68
|
+
let j = i + 2;
|
|
69
|
+
let params = '';
|
|
70
|
+
while (j < data.length && /[0-9;]/.test(data[j])) { params += data[j]; j++; }
|
|
71
|
+
const cmd = data[j];
|
|
72
|
+
const nums = params.split(';').map((p) => (p === '' ? undefined : Number(p)));
|
|
73
|
+
this._csi(cmd, nums, params);
|
|
74
|
+
i = j + 1;
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
// other escapes (e.g. \u001B[?25l already handled via '[' path); skip 2
|
|
78
|
+
i += 1;
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (ch === '\n') { this.cy += 1; this.cx = 0; this._clampRow(); i++; continue; }
|
|
82
|
+
if (ch === '\r') { this.cx = 0; i++; continue; }
|
|
83
|
+
// printable
|
|
84
|
+
this._clampRow();
|
|
85
|
+
const row = this.grid[this.cy];
|
|
86
|
+
const padded = row.length < this.cx ? row + ' '.repeat(this.cx - row.length) : row;
|
|
87
|
+
this.grid[this.cy] = padded.slice(0, this.cx) + ch + padded.slice(this.cx + 1);
|
|
88
|
+
this.cx += 1;
|
|
89
|
+
i++;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
_csi(cmd, nums, params) {
|
|
93
|
+
const n = nums[0];
|
|
94
|
+
switch (cmd) {
|
|
95
|
+
case 'H': case 'f': { // cursor position (1-based row;col)
|
|
96
|
+
this.cy = (nums[0] ?? 1) - 1; this.cx = (nums[1] ?? 1) - 1; this._clampRow(); break;
|
|
97
|
+
}
|
|
98
|
+
case 'A': this.cy -= (n ?? 1); this._clampRow(); break; // up
|
|
99
|
+
case 'B': this.cy += (n ?? 1); this._clampRow(); break; // down
|
|
100
|
+
case 'C': this.cx += (n ?? 1); break; // forward
|
|
101
|
+
case 'D': this.cx -= (n ?? 1); if (this.cx < 0) this.cx = 0; break; // back
|
|
102
|
+
case 'E': this.cy += (n ?? 1); this.cx = 0; this._clampRow(); break; // next line
|
|
103
|
+
case 'F': this.cy -= (n ?? 1); this.cx = 0; this._clampRow(); break; // prev line
|
|
104
|
+
case 'G': this.cx = (n ?? 1) - 1; break; // column (cursorTo(x) → \u001B[(x+1)G)
|
|
105
|
+
case 'J': { // erase display; 2 = whole screen (clearTerminal uses 2J + H)
|
|
106
|
+
if (n === 2 || n === 3) { this.grid = Array.from({ length: this.rows }, () => ''); }
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
case 'K': { // erase line (eraseEndLine=0/none; eraseLine full = 2K)
|
|
110
|
+
if (n === undefined || n === 0) { this.grid[this.cy] = this.grid[this.cy].slice(0, this.cx); }
|
|
111
|
+
else if (n === 1) { this.grid[this.cy] = ' '.repeat(this.cx) + this.grid[this.cy].slice(this.cx); }
|
|
112
|
+
else if (n === 2) { this.grid[this.cy] = ''; }
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
// SGR (m), hide/show cursor (h/l), etc. — no geometry effect
|
|
116
|
+
default: break;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
markerRow(marker) {
|
|
120
|
+
for (let r = 0; r < this.rows; r++) {
|
|
121
|
+
if (this.grid[r].includes(marker)) return r;
|
|
122
|
+
}
|
|
123
|
+
return -1;
|
|
124
|
+
}
|
|
125
|
+
dump() {
|
|
126
|
+
return this.grid.map((r, i) => `${String(i).padStart(2)}|${r}`).join('\n');
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ---- Wrapper mirroring ink.js renderInteractiveFrame ----------------------
|
|
131
|
+
function makeDriver({ rows, cols, isWindows }) {
|
|
132
|
+
const chunks = [];
|
|
133
|
+
const fakeStream = { write: (s) => { chunks.push(s); return true; }, isTTY: true, columns: cols, rows };
|
|
134
|
+
// Force the incremental renderer's Windows branch by faking env/platform is
|
|
135
|
+
// out of scope here; log-update reads process.platform/WT_SESSION at import.
|
|
136
|
+
const log = logUpdate.create(fakeStream, { incremental: true });
|
|
137
|
+
const vt = new VT(rows, cols);
|
|
138
|
+
let lastOutput = '';
|
|
139
|
+
let lastOutputHeight = 0;
|
|
140
|
+
let lastViewportRows = rows;
|
|
141
|
+
let lastOneShortPadded = false;
|
|
142
|
+
const commit = (output) => {
|
|
143
|
+
chunks.length = 0;
|
|
144
|
+
// [FAITHFUL] outputHeight = output.get().height = Output.height = the
|
|
145
|
+
// Yoga-computed ROOT height (renderer.js L45). The App pins the outer
|
|
146
|
+
// column to height=resizeState.rows, so a steady frame reports height=rows.
|
|
147
|
+
// BUT when the App's row accounting is off by one for a single commit
|
|
148
|
+
// (a reclaimed panel/hint row not yet refilled), the root lays out at
|
|
149
|
+
// rows-1 and outputHeight==rows-1. Model the height as the caller states
|
|
150
|
+
// it via a marker: the frame string's real line count is authoritative
|
|
151
|
+
// here because we construct each frame to physically carry `heightRows`
|
|
152
|
+
// lines. So derive it from the string.
|
|
153
|
+
const lineCount = output.split('\n').length;
|
|
154
|
+
let outputHeight = output === '' ? 0 : lineCount;
|
|
155
|
+
// Mirror ink.js (post-fix): an exactly-one-row-short frame following a
|
|
156
|
+
// fullscreen frame is padded with a leading blank line so the bottom
|
|
157
|
+
// cluster stays at its steady rows and the fullscreen path stays engaged.
|
|
158
|
+
const wasFullscreenFrame = lastOutputHeight >= lastViewportRows && lastOutputHeight > 0;
|
|
159
|
+
// Mirror ink.js guard exactly: Windows-like only + one-commit transient.
|
|
160
|
+
const isExactlyOneRowShort = isWindows && outputHeight === rows - 1
|
|
161
|
+
&& wasFullscreenFrame && !lastOneShortPadded;
|
|
162
|
+
if (isExactlyOneRowShort) {
|
|
163
|
+
output = '\n' + output;
|
|
164
|
+
outputHeight = rows;
|
|
165
|
+
lastOneShortPadded = true;
|
|
166
|
+
} else {
|
|
167
|
+
lastOneShortPadded = false;
|
|
168
|
+
}
|
|
169
|
+
const isFullscreen = outputHeight >= rows;
|
|
170
|
+
let outputToRender = isFullscreen ? output : output + '\n';
|
|
171
|
+
if (isFullscreen && outputToRender.endsWith('\n')) outputToRender += '\u001B[0m';
|
|
172
|
+
const clearDecision = shouldClearTerminalForFrameProbe({
|
|
173
|
+
isTty: true, viewportRows: rows, previousViewportRows: lastViewportRows,
|
|
174
|
+
previousOutputHeight: lastOutputHeight, nextOutputHeight: outputHeight,
|
|
175
|
+
isUnmounting: false, isWindows,
|
|
176
|
+
});
|
|
177
|
+
if (clearDecision) {
|
|
178
|
+
fakeStream.write('\u001B[0m' + ansiEscapes.clearTerminal + outputToRender);
|
|
179
|
+
log.sync(outputToRender);
|
|
180
|
+
} else {
|
|
181
|
+
log(outputToRender);
|
|
182
|
+
}
|
|
183
|
+
for (const c of chunks) vt.write(c);
|
|
184
|
+
lastOutput = output;
|
|
185
|
+
lastOutputHeight = outputHeight;
|
|
186
|
+
lastViewportRows = rows;
|
|
187
|
+
const trailingNL = outputToRender.endsWith('\n');
|
|
188
|
+
return { outputHeight, trailingNL, clearDecision, isFullscreen };
|
|
189
|
+
};
|
|
190
|
+
return { vt, commit };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// ---- Scenario builders ----------------------------------------------------
|
|
194
|
+
// Build an App-like frame with the prompt+statusline ALWAYS pinned to the two
|
|
195
|
+
// bottom-most non-blank rows (matching App.jsx: the bottom bar never moves).
|
|
196
|
+
// The App keeps total painted rows == viewport by ceding transcript rows to a
|
|
197
|
+
// panel; so the prompt row is invariant whether or not the palette is open.
|
|
198
|
+
// The only thing that changes between commits is: does the serialized output
|
|
199
|
+
// end on a blank row (→ trailing-newline flip, fs classification flip)?
|
|
200
|
+
// `bottomBlank` models a frame whose LAST painted row is blank padding.
|
|
201
|
+
// `shortByOne` = the root Yoga height is rows-1 for this commit (a reclaimed
|
|
202
|
+
// row that the App's accounting has NOT yet refilled). This is the documented
|
|
203
|
+
// deviant: outputHeight = rows-1 < viewportRows → ink.js takes output+'\n'
|
|
204
|
+
// (NON-fullscreen branch) → log-update relative cursorUp walk → one-row-low
|
|
205
|
+
// dip under Windows Terminal. A steady frame (shortByOne=false) fills the
|
|
206
|
+
// viewport, stays on the absolute cursorTo path, and is stable.
|
|
207
|
+
function frame({ rows, cols, palette, shortByOne, heightRows }) {
|
|
208
|
+
// [FAITHFUL] App.jsx pins the bottom cluster with the outer full-height
|
|
209
|
+
// column (height=resizeState.rows) + flexShrink={0} on the bottom bar. So the
|
|
210
|
+
// Yoga root height is `rows` when accounting is correct. When a reclaimed row
|
|
211
|
+
// is momentarily unaccounted, the laid-out tree is `rows-1` tall for one
|
|
212
|
+
// commit — that is `shortByOne`. In that frame the WHOLE column (including
|
|
213
|
+
// the pinned bottom cluster) sits one physical row higher.
|
|
214
|
+
// heightRows (explicit) overrides shortByOne — lets a caller drive an
|
|
215
|
+
// arbitrary frame height (e.g. rows-3) to exercise the real leave-fullscreen
|
|
216
|
+
// shrink chain, which the boolean shortByOne cannot express.
|
|
217
|
+
const height = heightRows != null ? heightRows : (shortByOne ? rows - 1 : rows);
|
|
218
|
+
const statusRow = height - 1;
|
|
219
|
+
const promptRow = statusRow - 1;
|
|
220
|
+
const lines = [];
|
|
221
|
+
for (let r = 0; r < height; r++) {
|
|
222
|
+
if (r === statusRow) { lines.push('STATUSLINE'); continue; }
|
|
223
|
+
if (r === promptRow) { lines.push('PROMPT>'); continue; }
|
|
224
|
+
if (palette && r === promptRow - 1) { lines.push('SLASHPALETTE'); continue; }
|
|
225
|
+
lines.push(`t${r}`);
|
|
226
|
+
}
|
|
227
|
+
// Serialized string carries exactly `height` lines (no trailing newline).
|
|
228
|
+
return lines.join('\n');
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function run() {
|
|
232
|
+
const rows = 40, cols = 120;
|
|
233
|
+
const isWindows = true;
|
|
234
|
+
console.log(`# renderer harness rows=${rows} cols=${cols} isWindows(assumed)=${isWindows}`);
|
|
235
|
+
console.log(`# NOTE: log-update Windows branch active iff process.platform===win32||WT_SESSION at import`);
|
|
236
|
+
console.log(`# (set WT_SESSION=1 to force it on non-Windows)\n`);
|
|
237
|
+
|
|
238
|
+
// Each frame: { palette, shortByOne }. shortByOne=true is the ONE deviant
|
|
239
|
+
// commit where the App's row accounting leaves the laid-out tree one row
|
|
240
|
+
// short of the viewport (reclaimed panel/hint row not yet refilled).
|
|
241
|
+
const scenarios = [
|
|
242
|
+
{ name: 'palette close, always viewport-filling (correct accounting)', seq: [
|
|
243
|
+
{ palette: true, shortByOne: false },
|
|
244
|
+
{ palette: false, shortByOne: false },
|
|
245
|
+
{ palette: false, shortByOne: false },
|
|
246
|
+
]},
|
|
247
|
+
{ name: 'palette close, close commit ONE ROW SHORT (deviant accounting)', seq: [
|
|
248
|
+
{ palette: true, shortByOne: false },
|
|
249
|
+
{ palette: false, shortByOne: true }, // reclaimed row unaccounted
|
|
250
|
+
{ palette: false, shortByOne: false },
|
|
251
|
+
]},
|
|
252
|
+
{ name: 'prompt newline remove, transitional ONE ROW SHORT', seq: [
|
|
253
|
+
{ palette: false, shortByOne: false },
|
|
254
|
+
{ palette: false, shortByOne: true },
|
|
255
|
+
{ palette: false, shortByOne: false },
|
|
256
|
+
]},
|
|
257
|
+
// Steady one-short: repeated rows-1 frames. The pad must fire ONCE then
|
|
258
|
+
// stop (lastOneShortPadded), so f2/f3 are NOT re-padded and settle at the
|
|
259
|
+
// rows-1 layout (prompt rows-3 / status rows-2) with correct clear
|
|
260
|
+
// decisions — no infinite downward shift.
|
|
261
|
+
{ name: 'steady ONE ROW SHORT (repeated rows-1) — pad once, then stop', seq: [
|
|
262
|
+
{ palette: false, shortByOne: false },
|
|
263
|
+
{ palette: false, shortByOne: true },
|
|
264
|
+
{ palette: false, shortByOne: true },
|
|
265
|
+
{ palette: false, shortByOne: true },
|
|
266
|
+
]},
|
|
267
|
+
// Real leave-fullscreen shrink chain rows→rows-1→rows-3: after the padded
|
|
268
|
+
// rows-1 frame, a further shrink to rows-3 must reach the shrink/clear path
|
|
269
|
+
// (not be masked by a stale pad) and settle cleanly.
|
|
270
|
+
{ name: 'real shrink chain fullscreen→rows-1→rows-3', seq: [
|
|
271
|
+
{ palette: false, heightRows: 40 },
|
|
272
|
+
{ palette: false, heightRows: 39 },
|
|
273
|
+
{ palette: false, heightRows: 37 },
|
|
274
|
+
{ palette: false, heightRows: 37 },
|
|
275
|
+
]},
|
|
276
|
+
];
|
|
277
|
+
|
|
278
|
+
let anyDeviant = false;
|
|
279
|
+
for (const sc of scenarios) {
|
|
280
|
+
const { vt, commit } = makeDriver({ rows, cols, isWindows });
|
|
281
|
+
const promptRows = [];
|
|
282
|
+
console.log(`## ${sc.name}`);
|
|
283
|
+
sc.seq.forEach((f, idx) => {
|
|
284
|
+
const out = frame({ rows, cols, palette: f.palette, shortByOne: f.shortByOne, heightRows: f.heightRows });
|
|
285
|
+
const info = commit(out);
|
|
286
|
+
const pRow = vt.markerRow('PROMPT>');
|
|
287
|
+
const sRow = vt.markerRow('STATUSLINE');
|
|
288
|
+
promptRows.push(pRow);
|
|
289
|
+
console.log(` f${idx} short1=${f.shortByOne?1:0} h=${info.outputHeight} fs=${info.isFullscreen?1:0} trailNL=${info.trailingNL?1:0} clear=${info.clearDecision?1:0} promptRow=${pRow} statusRow=${sRow}`);
|
|
290
|
+
});
|
|
291
|
+
// Deviant = the prompt row BOUNCES (differs from the settled row for a
|
|
292
|
+
// transient frame then returns). A monotone shift to a new steady row
|
|
293
|
+
// (steady-one-short, real shrink) is NOT a bounce — check the LAST row is
|
|
294
|
+
// reached and held, and no interior frame differs from BOTH neighbors.
|
|
295
|
+
const settled = promptRows[promptRows.length - 1];
|
|
296
|
+
const bounce = promptRows.some((r, i) =>
|
|
297
|
+
i > 0 && i < promptRows.length - 1 &&
|
|
298
|
+
r >= 0 && r !== promptRows[i - 1] && r !== promptRows[i + 1]);
|
|
299
|
+
if (bounce) { anyDeviant = true; console.log(` >> DEVIANT(bounce): prompt rows ${JSON.stringify(promptRows)} vs settled ${settled}`); }
|
|
300
|
+
console.log('');
|
|
301
|
+
}
|
|
302
|
+
if (!anyDeviant) console.log('# no deviant frame reproduced at renderer level for these scenarios');
|
|
303
|
+
process.exitCode = 0;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
run();
|
|
@@ -9,13 +9,12 @@ Bounded slices; smallest coherent change, not rewrite. Stop: unclear scope,
|
|
|
9
9
|
growing blast radius, or Lead-only verification.
|
|
10
10
|
|
|
11
11
|
EDIT-FIRST DISCIPLINE. Survey the slice with ONE batched read/grep round, then
|
|
12
|
-
patch the first bounded piece —
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
deep verification is Lead's.
|
|
12
|
+
patch the first bounded piece — edit incrementally, don't read exhaustively.
|
|
13
|
+
NEVER "one more confirming read": a plausible anchor means the next call is
|
|
14
|
+
`apply_patch`. Repeated read-only turns without an edit = stalling; on a
|
|
15
|
+
runtime reminder, patch the piece you understand or report blocked — a valid
|
|
16
|
+
completion. Self-check comes AFTER edits; deep verification is Lead's.
|
|
18
17
|
|
|
19
18
|
Minimal checks + how-to-verify. Hand off outcome as fragments anchored to
|
|
20
|
-
`file:line`; no narration
|
|
19
|
+
`file:line`; no narration or bloat.
|
|
21
20
|
|
|
@@ -8,13 +8,12 @@ Scoped implementation agent.
|
|
|
8
8
|
Smallest scoped change; no drive-by cleanup. Stop when done/blocked.
|
|
9
9
|
|
|
10
10
|
EDIT-FIRST DISCIPLINE. Brief anchors (`file:line`) are pre-verified — trust and
|
|
11
|
-
patch. No anchor: locate with AT MOST 1-2 reads, then edit
|
|
12
|
-
confirming read"
|
|
13
|
-
`apply_patch`. Repeated read-only turns without an edit = stalling;
|
|
14
|
-
reminder
|
|
15
|
-
|
|
11
|
+
patch. No anchor: locate with AT MOST 1-2 reads, then edit — NEVER "one more
|
|
12
|
+
confirming read"; if you know the file and the change, the next call is
|
|
13
|
+
`apply_patch`. Repeated read-only turns without an edit = stalling; on a
|
|
14
|
+
runtime reminder, patch now or return blocked with what's missing — a blocked
|
|
15
|
+
report is a valid completion. Threshold is "plausible", not "proven";
|
|
16
16
|
self-check comes AFTER the edit, and Lead/Reviewer own final verification.
|
|
17
17
|
|
|
18
|
-
Hand off outcome as fragments anchored to `file:line`; no narration
|
|
19
|
-
bloat.
|
|
18
|
+
Hand off outcome as fragments anchored to `file:line`; no narration or bloat.
|
|
20
19
|
|
package/src/lib/keychain-cjs.cjs
CHANGED
|
@@ -182,23 +182,40 @@ function loadKeytar() {
|
|
|
182
182
|
function keytarSync(method, ...args) {
|
|
183
183
|
loadKeytar(); // throws if not installed — before spawning child
|
|
184
184
|
const { spawnSync } = require('child_process');
|
|
185
|
-
// Pass service/account/value via env to avoid shell injection
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
185
|
+
// Pass service/account/value via stdin (not env) to avoid shell injection
|
|
186
|
+
// and to keep secret values out of /proc/<pid>/environ on Linux.
|
|
187
|
+
// Build a minimal child env instead of spreading process.env: the parent
|
|
188
|
+
// may hold *_API_KEY secrets in its own environment, and those would
|
|
189
|
+
// otherwise be visible via /proc/<pid>/environ for the lifetime of this
|
|
190
|
+
// child. Only pass what PATH resolution / locale / the libsecret D-Bus
|
|
191
|
+
// session bridge actually need.
|
|
192
|
+
const passthroughKeys = [
|
|
193
|
+
'PATH', 'HOME', 'USER', 'LOGNAME',
|
|
194
|
+
'LANG', 'LC_ALL',
|
|
195
|
+
'TMPDIR', 'TMP', 'TEMP',
|
|
196
|
+
'DISPLAY', 'DBUS_SESSION_BUS_ADDRESS', 'XDG_RUNTIME_DIR', 'XDG_CURRENT_DESKTOP', 'XDG_DATA_DIRS',
|
|
197
|
+
];
|
|
198
|
+
const env = { _KEYTAR_METHOD: method };
|
|
199
|
+
for (const key of passthroughKeys) {
|
|
200
|
+
if (process.env[key] !== undefined) env[key] = process.env[key];
|
|
201
|
+
}
|
|
191
202
|
const script = [
|
|
192
203
|
'const kt = require("keytar");',
|
|
193
204
|
'const method = process.env._KEYTAR_METHOD;',
|
|
194
|
-
'
|
|
195
|
-
'
|
|
196
|
-
'
|
|
197
|
-
'
|
|
205
|
+
'let input = "";',
|
|
206
|
+
'process.stdin.setEncoding("utf8");',
|
|
207
|
+
'process.stdin.on("data", (chunk) => { input += chunk; });',
|
|
208
|
+
'process.stdin.on("end", () => {',
|
|
209
|
+
' const args = JSON.parse(input);',
|
|
210
|
+
' kt[method](...args)',
|
|
211
|
+
' .then(v => { process.stdout.write(JSON.stringify({ ok: true, value: v })); })',
|
|
212
|
+
' .catch(e => { process.stdout.write(JSON.stringify({ ok: false, error: e.message })); });',
|
|
213
|
+
'});',
|
|
198
214
|
].join(' ');
|
|
199
215
|
const r = spawnSync(process.execPath, ['-e', script], {
|
|
200
216
|
env,
|
|
201
|
-
|
|
217
|
+
input: JSON.stringify(args),
|
|
218
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
202
219
|
timeout: 5000,
|
|
203
220
|
encoding: 'utf8',
|
|
204
221
|
windowsHide: true,
|
|
@@ -129,10 +129,10 @@ function buildProfilePreferencesContent(dataDir) {
|
|
|
129
129
|
const profile = normalizeProfileConfig(readAgentConfig(dataDir).profile);
|
|
130
130
|
const lines = [];
|
|
131
131
|
if (profile.title) {
|
|
132
|
-
lines.push(`- Use "${profile.title}" when directly addressing the user (greetings, answers, questions)
|
|
132
|
+
lines.push(`- Use "${profile.title}" when directly addressing the user (greetings, answers, questions); do not repeat it in routine progress updates or pre-tool preambles.`);
|
|
133
133
|
}
|
|
134
134
|
const shell = process.platform === 'win32' ? 'powershell' : 'bash';
|
|
135
|
-
lines.push(`- Shell environment: ${shell}.
|
|
135
|
+
lines.push(`- Shell environment: ${shell}. Write shell commands and scripts in ${shell} syntax unless the user specifies otherwise; keep commands, paths, symbols, and exact errors verbatim.`);
|
|
136
136
|
return lines.length ? `# Profile Preferences\n\n${lines.join('\n')}` : '';
|
|
137
137
|
}
|
|
138
138
|
|
|
@@ -144,7 +144,7 @@ function buildLanguageSection(dataDir) {
|
|
|
144
144
|
? ` from system locale ${language.locale}`
|
|
145
145
|
: '';
|
|
146
146
|
const lines = [
|
|
147
|
-
`- Default user-facing response language${source}: ${language.prompt}.
|
|
147
|
+
`- Default user-facing response language${source}: ${language.prompt}. EVERY user-facing message — prose, pre-tool preambles (even single-line), progress updates, questions, final reports, notices — MUST be written in ${language.prompt} and no other language; this overrides any tone implied by the output style. Switch only when the user writes in another language or explicitly asks you to.`,
|
|
148
148
|
`- Code identifiers, paths, commands, symbols, API names, and exact errors should remain in their original form.`,
|
|
149
149
|
];
|
|
150
150
|
return `# Language\n\n${lines.join('\n')}`;
|
|
@@ -382,13 +382,9 @@ function buildAgentRoleContent({ PLUGIN_ROOT, profile = 'full' }) {
|
|
|
382
382
|
function buildAgentRetrievalInjectionContent({ PLUGIN_ROOT }) {
|
|
383
383
|
const AGENT_DIR = path.join(PLUGIN_ROOT, 'rules', 'agent');
|
|
384
384
|
const core = readOptional(path.join(AGENT_DIR, '00-core.md'));
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
'- Batch independent read-only lookups in the same tool turn.',
|
|
389
|
-
'- Use code_graph for symbols/dependencies, grep for exact text, find/glob/list for files, and read only known paths/windows.',
|
|
390
|
-
'',
|
|
391
|
-
];
|
|
385
|
+
// Full shared tool policy (01-tool.md) now ships via BP1 for retrieval
|
|
386
|
+
// roles too; no compact duplicate here.
|
|
387
|
+
const parts = [];
|
|
392
388
|
if (core) parts.push(core.trim());
|
|
393
389
|
parts.push('', '- Read-only retrieval role: do not edit files, run shell, or use git.');
|
|
394
390
|
return parts.join('\n');
|