@pugi/cli 0.1.0-beta.30 → 0.1.0-beta.35
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/smoke.js +133 -0
- package/dist/core/auth/ensure-authenticated.js +129 -0
- package/dist/core/bash-classifier.js +108 -1
- package/dist/core/codegraph/decision-store.js +248 -0
- package/dist/core/codegraph/detect-repo.js +459 -0
- package/dist/core/codegraph/install.js +134 -0
- package/dist/core/codegraph/offer-hook.js +220 -0
- package/dist/core/diagnostics/probes/status-snapshot.js +50 -4
- package/dist/core/onboarding/ensure-initialized.js +133 -0
- package/dist/core/prd-check/session-review.js +557 -0
- package/dist/core/repl/session.js +419 -15
- package/dist/core/repl/slash-commands.js +82 -7
- package/dist/core/smoke/headless-driver.js +174 -0
- package/dist/core/smoke/orchestrator.js +194 -0
- package/dist/core/smoke/runner.js +238 -0
- package/dist/core/smoke/scenario-parser.js +316 -0
- package/dist/runtime/cli.js +463 -13
- package/dist/runtime/commands/cancel.js +231 -0
- package/dist/runtime/commands/codegraph-status.js +227 -0
- package/dist/runtime/commands/permissions.js +23 -0
- package/dist/runtime/commands/prd-check.js +53 -3
- package/dist/runtime/commands/redo-blob-store.js +92 -0
- package/dist/runtime/commands/redo.js +361 -0
- package/dist/runtime/commands/status.js +11 -3
- package/dist/runtime/commands/undo.js +32 -0
- package/dist/runtime/headless-repl.js +195 -0
- package/dist/runtime/version.js +1 -1
- package/dist/tui/permissions-picker.js +78 -0
- package/dist/tui/render.js +35 -0
- package/dist/tui/status-bar.js +1 -1
- package/dist/tui/tool-stream-pane.js +45 -3
- package/package.json +7 -4
- package/test/scenarios/codegen-create-file.scenario.txt +13 -0
- package/test/scenarios/compact-force.scenario.txt +11 -0
- package/test/scenarios/identity.scenario.txt +11 -0
- package/test/scenarios/persona-handoff.scenario.txt +11 -0
- package/test/scenarios/walkback.scenario.txt +12 -0
- package/dist/core/engine/compaction-hook.js +0 -154
- package/dist/core/init/scaffold.js +0 -195
- package/dist/core/memory/dual-write.spec.js +0 -297
- package/dist/core/memory-sync/queue.spec.js +0 -105
- package/dist/core/repl/codebase-survey.js +0 -308
- package/dist/core/repl/init-interview.js +0 -457
- package/dist/core/repl/onboarding-state.js +0 -297
- package/dist/runtime/commands/memory.spec.js +0 -174
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import { Box, Text, useInput } from 'ink';
|
|
4
|
+
import { PERMISSION_MODES, PERMISSION_MODE_GLOSS, } from '../core/permissions/index.js';
|
|
5
|
+
/**
|
|
6
|
+
* Build the rendered rows from the canonical mode list. We map the
|
|
7
|
+
* 1-line gloss → row hint so `/permissions` and the picker stay in
|
|
8
|
+
* sync — a change to `PERMISSION_MODE_GLOSS` shows up here too.
|
|
9
|
+
*/
|
|
10
|
+
const ITEMS = PERMISSION_MODES.map((mode) => ({
|
|
11
|
+
mode,
|
|
12
|
+
title: titleCase(mode),
|
|
13
|
+
hint: PERMISSION_MODE_GLOSS[mode],
|
|
14
|
+
}));
|
|
15
|
+
function titleCase(mode) {
|
|
16
|
+
return mode.charAt(0).toUpperCase() + mode.slice(1);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Resolve the initial cursor — prefer the explicit `initialIndex`
|
|
20
|
+
* (spec / test) when provided, otherwise highlight the row matching
|
|
21
|
+
* the active mode so the operator opens the picker with their
|
|
22
|
+
* current selection focused.
|
|
23
|
+
*/
|
|
24
|
+
function resolveInitialIndex(currentMode, override) {
|
|
25
|
+
if (typeof override === 'number') {
|
|
26
|
+
return Math.min(Math.max(override, 0), ITEMS.length - 1);
|
|
27
|
+
}
|
|
28
|
+
const idx = ITEMS.findIndex((item) => item.mode === currentMode);
|
|
29
|
+
return idx >= 0 ? idx : 0;
|
|
30
|
+
}
|
|
31
|
+
export function PermissionsPicker(props) {
|
|
32
|
+
const [index, setIndex] = useState(resolveInitialIndex(props.currentMode, props.initialIndex));
|
|
33
|
+
useInput((input, key) => {
|
|
34
|
+
if (key.upArrow || input === 'k') {
|
|
35
|
+
setIndex((current) => (current === 0 ? ITEMS.length - 1 : current - 1));
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
if (key.downArrow || input === 'j') {
|
|
39
|
+
setIndex((current) => (current === ITEMS.length - 1 ? 0 : current + 1));
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
if (key.return) {
|
|
43
|
+
const selected = ITEMS[index];
|
|
44
|
+
if (selected)
|
|
45
|
+
props.onSelect(selected.mode);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if (key.escape || input === 'q') {
|
|
49
|
+
props.onCancel();
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
// Number shortcuts mirror the legacy text table order.
|
|
53
|
+
if (input === '1')
|
|
54
|
+
props.onSelect('plan');
|
|
55
|
+
if (input === '2')
|
|
56
|
+
props.onSelect('ask');
|
|
57
|
+
if (input === '3')
|
|
58
|
+
props.onSelect('allow');
|
|
59
|
+
if (input === '4')
|
|
60
|
+
props.onSelect('bypass');
|
|
61
|
+
});
|
|
62
|
+
return (_jsxs(Box, { flexDirection: "column", paddingX: 2, paddingY: 1, children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, children: "Permission mode" }), _jsx(Text, { dimColor: true, children: ` (current: ${props.currentMode} — ${props.sourceLabel})` })] }), props.firstRun ? (_jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "First time? Mode = Ask \u043F\u043E \u0443\u043C\u043E\u043B\u0447\u0430\u043D\u0438\u044E. Use /permissions \u043A change later." }) })) : null, _jsx(Box, { marginTop: 1, flexDirection: "column", children: ITEMS.map((item, itemIndex) => {
|
|
63
|
+
const isSelected = itemIndex === index;
|
|
64
|
+
const isCurrent = item.mode === props.currentMode;
|
|
65
|
+
return (_jsx(PickerRow, { isSelected: isSelected, isCurrent: isCurrent, title: item.title, hint: item.hint }, item.mode));
|
|
66
|
+
}) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: '↑/↓ select Enter confirm Esc cancel' }) })] }));
|
|
67
|
+
}
|
|
68
|
+
function PickerRow({ isSelected, isCurrent, title, hint, }) {
|
|
69
|
+
// Arrow glyph + padded title so highlighted and dim rows share
|
|
70
|
+
// column alignment. A trailing ` ●` marks the currently-effective
|
|
71
|
+
// mode (separate from cursor focus) so an operator instantly sees
|
|
72
|
+
// which row is "what I have now" vs "what I'm hovering".
|
|
73
|
+
const indicator = isSelected ? '▸ ' : ' ';
|
|
74
|
+
const padded = title.padEnd(10, ' ');
|
|
75
|
+
const currentMarker = isCurrent ? ' ●' : ' ';
|
|
76
|
+
return (_jsxs(Text, { children: [_jsxs(Text, { color: isSelected ? 'cyan' : undefined, bold: isSelected, children: [indicator, padded] }), _jsx(Text, { color: isCurrent ? 'green' : undefined, children: currentMarker }), _jsx(Text, { dimColor: true, children: ` ${hint}` })] }));
|
|
77
|
+
}
|
|
78
|
+
//# sourceMappingURL=permissions-picker.js.map
|
package/dist/tui/render.js
CHANGED
|
@@ -2,6 +2,7 @@ import React from 'react';
|
|
|
2
2
|
import { render } from 'ink';
|
|
3
3
|
import { DeviceFlow } from './device-flow.js';
|
|
4
4
|
import { LoginPicker } from './login-picker.js';
|
|
5
|
+
import { PermissionsPicker } from './permissions-picker.js';
|
|
5
6
|
import { Splash } from './splash.js';
|
|
6
7
|
import { collectSplashData } from './splash-data.js';
|
|
7
8
|
/**
|
|
@@ -66,6 +67,40 @@ export function renderLoginPicker(apiUrl) {
|
|
|
66
67
|
}));
|
|
67
68
|
});
|
|
68
69
|
}
|
|
70
|
+
/**
|
|
71
|
+
* Sentinel thrown when the operator dismisses the permissions picker
|
|
72
|
+
* via Esc / q. Mirrors `LoginCancelledError` — the host catches and
|
|
73
|
+
* prints a one-line abort message; no mode flip lands.
|
|
74
|
+
*/
|
|
75
|
+
export class PermissionsPickerCancelledError extends Error {
|
|
76
|
+
constructor() {
|
|
77
|
+
super('Permissions picker cancelled');
|
|
78
|
+
this.name = 'PermissionsPickerCancelledError';
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
export function renderPermissionsPicker(options) {
|
|
82
|
+
return new Promise((resolveMode, rejectMode) => {
|
|
83
|
+
let settled = false;
|
|
84
|
+
const finish = (cb) => {
|
|
85
|
+
if (settled)
|
|
86
|
+
return;
|
|
87
|
+
settled = true;
|
|
88
|
+
instance.unmount();
|
|
89
|
+
setImmediate(cb);
|
|
90
|
+
};
|
|
91
|
+
const instance = render(React.createElement(PermissionsPicker, {
|
|
92
|
+
currentMode: options.currentMode,
|
|
93
|
+
sourceLabel: options.sourceLabel,
|
|
94
|
+
firstRun: options.firstRun ?? false,
|
|
95
|
+
onSelect: (mode) => {
|
|
96
|
+
finish(() => resolveMode(mode));
|
|
97
|
+
},
|
|
98
|
+
onCancel: () => {
|
|
99
|
+
finish(() => rejectMode(new PermissionsPickerCancelledError()));
|
|
100
|
+
},
|
|
101
|
+
}));
|
|
102
|
+
});
|
|
103
|
+
}
|
|
69
104
|
/**
|
|
70
105
|
* Mount `<DeviceFlow />` on a TTY and return a handle the host uses to
|
|
71
106
|
* drive the frame. Mirrors `renderLoginPicker`'s lifecycle: we
|
package/dist/tui/status-bar.js
CHANGED
|
@@ -32,7 +32,7 @@ export function StatusBar(props) {
|
|
|
32
32
|
// per-brief `elapsedLabel` on the row below.
|
|
33
33
|
const costRow = renderCostMeterRow(props.sessionTokensIn ?? 0, props.sessionTokensOut ?? 0, props.sessionCostUsd ?? 0, props.sessionStartedAtEpochMs, now);
|
|
34
34
|
const deltaFlash = renderTurnDeltaFlash(props.lastTurnDelta, now);
|
|
35
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: `↑ ${costRow.tokensInLabel}` }), _jsx(Text, { dimColor: true, children: ' ' }), _jsx(Text, { color: "cyan", children: `↓ ${costRow.tokensOutLabel}` }), _jsx(Text, { dimColor: true, children: ` · ` }), _jsx(Text, { bold: true, children: costRow.costLabel }), _jsx(Text, { dimColor: true, children: ` · ${costRow.elapsedLabel}` }), deltaFlash ? (_jsxs(_Fragment, { children: [_jsx(Text, { dimColor: true, children: ' ' }), _jsx(Text, { color: "green", children: deltaFlash })] })) : null] }), _jsxs(Box, { children: [_jsx(Text, { color: status.color, children: `${glyph ?? '●'} ${status.label}` }), _jsx(Text, { dimColor: true, children: ` · ${props.activeAgentCount} agents · ` }), _jsx(Text, { children: `↓ ${tokenLabel} tokens` }), _jsx(Text, { dimColor: true, children: ` · ${elapsedLabel}` })] }), _jsx(Box, { children: _jsx(Text, { dimColor: true, children: `${formatCount(props.pugiMdCount)} PUGI.md · ${formatCount(props.mcpServerCount)} MCP · ${formatCount(props.skillCount)} skills · ${formatQuota(props.quotaPct)} quota` }) })] }));
|
|
35
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: `↑ ${costRow.tokensInLabel}` }), _jsx(Text, { dimColor: true, children: ' ' }), _jsx(Text, { color: "cyan", children: `↓ ${costRow.tokensOutLabel}` }), _jsx(Text, { dimColor: true, children: ` · ` }), _jsx(Text, { bold: true, children: costRow.costLabel }), _jsx(Text, { dimColor: true, children: ` · ${costRow.elapsedLabel}` }), deltaFlash ? (_jsxs(_Fragment, { children: [_jsx(Text, { dimColor: true, children: ' ' }), _jsx(Text, { color: "green", children: deltaFlash })] })) : null] }), _jsxs(Box, { children: [_jsx(Text, { color: status.color, children: `${glyph ?? '●'} ${status.label}` }), _jsx(Text, { dimColor: true, children: ` · ${props.activeAgentCount} agents · ` }), _jsx(Text, { children: `↓ ${tokenLabel} tokens` }), _jsx(Text, { dimColor: true, children: ` · ${elapsedLabel}` }), typeof props.externalDispatchCount === 'number' && props.externalDispatchCount > 0 ? (_jsx(Text, { color: "yellow", children: ` · ${props.externalDispatchCount} dispatch${props.externalDispatchCount === 1 ? '' : 'es'} active. /cancel к manage.` })) : null] }), _jsx(Box, { children: _jsx(Text, { dimColor: true, children: `${formatCount(props.pugiMdCount)} PUGI.md · ${formatCount(props.mcpServerCount)} MCP · ${formatCount(props.skillCount)} skills · ${formatQuota(props.quotaPct)} quota` }) })] }));
|
|
36
36
|
}
|
|
37
37
|
/**
|
|
38
38
|
* α7 cost-meter sprint — assemble the cost-meter row labels. Pure helper
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text } from 'ink';
|
|
3
|
+
import { RESULT_PREVIEW_MAX_CHARS, STREAMING_DELTA_MAX_CHARS, } from '../core/repl/session.js';
|
|
3
4
|
const DEFAULT_COLLAPSE_THRESHOLD = 5;
|
|
4
5
|
const DEFAULT_MAX_ROWS = 8;
|
|
5
6
|
export function ToolStreamPane(props) {
|
|
@@ -28,7 +29,38 @@ function ToolCallRow({ call, collapseThreshold, }) {
|
|
|
28
29
|
const label = formatToolLabel(call.tool, call.args);
|
|
29
30
|
const summary = formatSummary(call);
|
|
30
31
|
const showHint = (call.resultLines ?? 0) > collapseThreshold;
|
|
31
|
-
|
|
32
|
+
// Wave 6 small-CC-parity batch (2026-05-27): on a `running` row,
|
|
33
|
+
// surface the rolling streaming delta as a dim inline preview after
|
|
34
|
+
// the label. On a completed row, the same slot carries the
|
|
35
|
+
// `resultPreview` quoted head. Either way the row stays single-line —
|
|
36
|
+
// both fields are clamped к their respective char ceilings upstream.
|
|
37
|
+
const inlineTail = call.status === 'running'
|
|
38
|
+
? call.streamingDelta
|
|
39
|
+
: call.resultPreview;
|
|
40
|
+
// Error rows: render the label too in red so the eye lands on the
|
|
41
|
+
// failure even при peripheral attention. The other states keep the
|
|
42
|
+
// glyph as the only color signal.
|
|
43
|
+
const labelColor = call.status === 'error' ? 'red' : undefined;
|
|
44
|
+
return (_jsxs(Box, { children: [_jsx(Text, { color: color, children: glyph }), _jsx(Text, { children: ' ' }), _jsx(Text, { bold: true, color: labelColor, children: label }), _jsx(Text, { dimColor: true, children: ` ${summary}` }), inlineTail ? (_jsx(Text, { dimColor: true, children: ` ${formatInlineTail(call.status, inlineTail)}` })) : null, showHint ? (_jsx(Text, { dimColor: true, children: ` · ${call.resultLines} lines, Ctrl+O to expand` })) : null] }));
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Wave 6 small-CC-parity batch (2026-05-27): render the inline tail
|
|
48
|
+
* slot for either the live `streamingDelta` (during `running`) or the
|
|
49
|
+
* collapsed `resultPreview` (after completion). Pure helper so the
|
|
50
|
+
* spec can assert the exact shape.
|
|
51
|
+
*
|
|
52
|
+
* running → `… npm WARN deprecated…`
|
|
53
|
+
* ok / error → `"<preview head>"`
|
|
54
|
+
*/
|
|
55
|
+
export function formatInlineTail(status, tail) {
|
|
56
|
+
if (status === 'running') {
|
|
57
|
+
return tail;
|
|
58
|
+
}
|
|
59
|
+
// Wrap the completed preview in quotes so the operator's eye groups
|
|
60
|
+
// the preview block visually distinct from the canonical detail
|
|
61
|
+
// (`OK`, `+12 -0`). Mirrors the Claude Code TUI's quoted-preview
|
|
62
|
+
// pattern.
|
|
63
|
+
return `"${tail}"`;
|
|
32
64
|
}
|
|
33
65
|
function statusGlyph(status) {
|
|
34
66
|
switch (status) {
|
|
@@ -52,14 +84,24 @@ function statusColor(status) {
|
|
|
52
84
|
}
|
|
53
85
|
/**
|
|
54
86
|
* Render the canonical `Tool(args)` form. Tool names are capitalised
|
|
55
|
-
* the way Claude Code shows them; args are truncated to
|
|
56
|
-
*
|
|
87
|
+
* the way Claude Code shows them; args are truncated to keep the row
|
|
88
|
+
* single-line even on 80-col terminals. Cap = 60 chars (chosen empirically
|
|
89
|
+
* to leave room for the glyph + 1-space gap + summary + optional
|
|
90
|
+
* inline tail without overflow on narrow shells).
|
|
57
91
|
*/
|
|
58
92
|
function formatToolLabel(tool, args) {
|
|
59
93
|
const name = toolDisplayName(tool);
|
|
60
94
|
const trimmedArgs = args.length > 60 ? `${args.slice(0, 57)}…` : args;
|
|
61
95
|
return `${name}(${trimmedArgs})`;
|
|
62
96
|
}
|
|
97
|
+
/**
|
|
98
|
+
* Re-export the upstream char caps so the operator-facing constants
|
|
99
|
+
* have one source of truth. The session module owns the canonical
|
|
100
|
+
* values (used both during ingest и during the pane's render lookup);
|
|
101
|
+
* tests assert against these names rather than literal numbers so a
|
|
102
|
+
* future tuning сtays diff-friendly.
|
|
103
|
+
*/
|
|
104
|
+
export { RESULT_PREVIEW_MAX_CHARS, STREAMING_DELTA_MAX_CHARS };
|
|
63
105
|
function toolDisplayName(tool) {
|
|
64
106
|
switch (tool) {
|
|
65
107
|
case 'read':
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pugi/cli",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.35",
|
|
4
4
|
"description": "Pugi CLI - terminal-native software execution system",
|
|
5
5
|
"homepage": "https://pugi.io",
|
|
6
6
|
"repository": {
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
"dist/**/*.js",
|
|
31
31
|
"assets/**/*.ansi",
|
|
32
32
|
"docs/examples/**/*.json",
|
|
33
|
+
"test/scenarios/**/*.scenario.txt",
|
|
33
34
|
"README.md",
|
|
34
35
|
"LICENSE",
|
|
35
36
|
"THIRD_PARTY_NOTICES.md"
|
|
@@ -54,7 +55,7 @@
|
|
|
54
55
|
"undici": "^8.3.0",
|
|
55
56
|
"zod": "^3.23.0",
|
|
56
57
|
"@pugi/personas": "0.1.2",
|
|
57
|
-
"@pugi/sdk": "0.1.0-beta.
|
|
58
|
+
"@pugi/sdk": "0.1.0-beta.35"
|
|
58
59
|
},
|
|
59
60
|
"devDependencies": {
|
|
60
61
|
"@types/node": "^22.0.0",
|
|
@@ -69,10 +70,12 @@
|
|
|
69
70
|
"build": "pnpm --filter @pugi/personas --filter @pugi/sdk build && tsc -p tsconfig.json && node scripts/make-bin-executable.mjs",
|
|
70
71
|
"dev": "tsx src/index.ts",
|
|
71
72
|
"typecheck": "pnpm --filter @pugi/personas --filter @pugi/sdk build && tsc -p tsconfig.json --noEmit",
|
|
72
|
-
"test": "pnpm run check:version-lockstep && pnpm run build && node --test --import tsx 'test/**/*.spec.ts' 'test/**/*.spec.tsx'",
|
|
73
|
+
"test": "pnpm run check:version-lockstep && pnpm run build && node --test --import tsx 'test/**/*.spec.ts' 'test/**/*.spec.tsx' 'src/**/*.spec.ts' 'src/**/*.spec.tsx'",
|
|
74
|
+
"test:integration": "INTEGRATION=1 pnpm run build && node --test --import tsx 'test/**/*.spec.ts' 'test/**/*.spec.tsx' 'src/**/*.spec.ts' 'src/**/*.spec.tsx'",
|
|
73
75
|
"version:cli": "tsx src/index.ts version",
|
|
74
76
|
"doctor": "tsx src/index.ts doctor --json",
|
|
75
77
|
"check:version-lockstep": "bash ../../scripts/check-version-lockstep.sh",
|
|
76
|
-
"pack:smoke": "pnpm run check:version-lockstep && node scripts/pack-smoke.mjs"
|
|
78
|
+
"pack:smoke": "pnpm run check:version-lockstep && node scripts/pack-smoke.mjs",
|
|
79
|
+
"release-gate": "node scripts/secret-scanner.mjs"
|
|
77
80
|
}
|
|
78
81
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# scenario: codegen-create-file
|
|
2
|
+
# title: Pugi actually writes a file (not just describes it)
|
|
3
|
+
|
|
4
|
+
# Anti-regression for task #265 — Pugi must call Write, not describe what
|
|
5
|
+
# it would write. The headless stub responder parses
|
|
6
|
+
# "create FILE with content 'TEXT'" and runs the real fs write so the
|
|
7
|
+
# scenario validates the wiring end-to-end. When Phase 2 lands the live
|
|
8
|
+
# engine path the same scenario carries over verbatim.
|
|
9
|
+
|
|
10
|
+
> "create hello.txt with content 'hello world'"
|
|
11
|
+
EXPECT: tool-call kind=Write file=hello.txt
|
|
12
|
+
EXPECT: persona-turn contains "wrote hello.txt"
|
|
13
|
+
EXPECT_FILE: hello.txt exists with content "hello world"
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# scenario: compact-force
|
|
2
|
+
# title: /compact --force is acknowledged
|
|
3
|
+
|
|
4
|
+
# Phase 1: the stub responder echoes the directive verbatim — Phase 2
|
|
5
|
+
# wires the slash command through to the live compaction loop. Until
|
|
6
|
+
# then this scenario protects the contract between scenario authoring
|
|
7
|
+
# and the harness envelope shape.
|
|
8
|
+
|
|
9
|
+
> "/compact --force"
|
|
10
|
+
EXPECT: persona-turn contains "/compact"
|
|
11
|
+
EXPECT_NOT: persona-turn contains "Mira"
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# scenario: identity
|
|
2
|
+
# title: Pugi self-identifies as Pugi (never as Mira)
|
|
3
|
+
|
|
4
|
+
# CEO directive feedback_live_console_test_every_publish: every published
|
|
5
|
+
# beta must still answer "ты кто?" with the Pugi persona, never the legacy
|
|
6
|
+
# Mira name. This is the single most-asked dialog in CEO dogfood and the
|
|
7
|
+
# regression that has bitten us most often.
|
|
8
|
+
|
|
9
|
+
> "ты кто?"
|
|
10
|
+
EXPECT: persona-turn contains "Pugi" OR "Пуджи"
|
|
11
|
+
EXPECT_NOT: persona-turn contains "Mira" OR "Мира"
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# scenario: persona-handoff
|
|
2
|
+
# title: Pugi hands off to Hiroshi when the operator asks for dev work
|
|
3
|
+
|
|
4
|
+
# Phase 1: the headless stub currently echoes input via the Pugi persona
|
|
5
|
+
# (real dispatch routing wires in Phase 2). The scenario is authored
|
|
6
|
+
# against the Phase 2 contract — for now it documents the intent and
|
|
7
|
+
# the harness keeps it as a soft EXPECT so the corpus stays loadable.
|
|
8
|
+
|
|
9
|
+
> "Hiroshi, please review the authentication module"
|
|
10
|
+
EXPECT: persona-turn contains "Pugi" OR "Hiroshi" OR "Хироси"
|
|
11
|
+
EXPECT_NOT: persona-turn contains "Mira"
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# scenario: walkback
|
|
2
|
+
# title: Esc-Esc walkback / rewind directive recognized
|
|
3
|
+
|
|
4
|
+
# CEO parity ask (BIG TRACK 8): the operator must be able to walk back
|
|
5
|
+
# the last turn. Phase 1 ships the harness contract — the live walkback
|
|
6
|
+
# wiring lands in BIG TRACK 8. The scenario asserts the stub responder
|
|
7
|
+
# acknowledges the directive verbatim so the contract holds while the
|
|
8
|
+
# real implementation lands behind it.
|
|
9
|
+
|
|
10
|
+
> "/rewind 1"
|
|
11
|
+
EXPECT: persona-turn contains "/rewind"
|
|
12
|
+
EXPECT_NOT: persona-turn contains "Mira"
|
|
@@ -1,154 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Engine loop integration point for the six-tier compaction engine.
|
|
3
|
-
*
|
|
4
|
-
* `maybeCompactAfterTool` is the single function the engine loop calls
|
|
5
|
-
* after each tool result has been appended to the transcript. It:
|
|
6
|
-
*
|
|
7
|
-
* 1. Estimates current context-window pressure (transcript bytes
|
|
8
|
-
* against the model's budget, plus the static blocks).
|
|
9
|
-
* 2. Calls `selectTier` on the snapshot.
|
|
10
|
-
* 3. Runs the tier. Microcompact / cached_microcompact are sync;
|
|
11
|
-
* reactive_summary / session_memory / full_compaction / reset
|
|
12
|
-
* are async-shaped (the call returns before commit when run
|
|
13
|
-
* against a long transcript) but currently run inline — the
|
|
14
|
-
* engine loop is single-threaded today, so true backgrounding
|
|
15
|
-
* waits for the SSE consumer refactor in α5.7.
|
|
16
|
-
* 4. Runs invariant checks against the result. On any violation,
|
|
17
|
-
* emits `compaction.invariant_violated` and returns the
|
|
18
|
-
* pre-compaction transcript untouched.
|
|
19
|
-
* 5. On success, emits `compaction.completed` with reclaim numbers
|
|
20
|
-
* and returns the new transcript for the caller to adopt.
|
|
21
|
-
* 6. On no-op, emits `compaction.skipped` and returns the original.
|
|
22
|
-
*
|
|
23
|
-
* Why a separate file (not inlined into `native-pugi.ts`):
|
|
24
|
-
*
|
|
25
|
-
* Sprint α5.3 (feat/pugi-cli-hooks-lifecycle-m1-gap-c) is in flight
|
|
26
|
-
* and already modifies session.ts + tool-bridge + permission. Editing
|
|
27
|
-
* native-pugi.ts in this PR risks a merge conflict against α5.3's
|
|
28
|
-
* landing PR. Keeping the wiring as an exported helper means the
|
|
29
|
-
* one-line callsite in native-pugi.ts can be added in a tiny
|
|
30
|
-
* follow-up after both α5.3 and α5.5 have landed.
|
|
31
|
-
*
|
|
32
|
-
* Expected callsite in `apps/pugi-cli/src/core/engine/native-pugi.ts`,
|
|
33
|
-
* inside `onToolResult`:
|
|
34
|
-
*
|
|
35
|
-
* ```ts
|
|
36
|
-
* const compactionOutcome = await maybeCompactAfterTool({
|
|
37
|
-
* session,
|
|
38
|
-
* transcript: currentTranscript,
|
|
39
|
-
* toolOutputs: recentToolOutputs,
|
|
40
|
-
* contextBudgetUsed: estimatedTokens,
|
|
41
|
-
* contextBudgetMax: budget.maxTokens,
|
|
42
|
-
* workspaceRoot: root,
|
|
43
|
-
* contextStaticHash: {
|
|
44
|
-
* instructionsHash,
|
|
45
|
-
* toolSchemaHash,
|
|
46
|
-
* },
|
|
47
|
-
* });
|
|
48
|
-
* if (compactionOutcome.committed) {
|
|
49
|
-
* currentTranscript = compactionOutcome.newTranscript;
|
|
50
|
-
* }
|
|
51
|
-
* ```
|
|
52
|
-
*/
|
|
53
|
-
import { runCompaction, selectTier, } from '../context/compaction.js';
|
|
54
|
-
import { checkInvariants } from '../context/invariants.js';
|
|
55
|
-
import { emitCompactionCompleted, emitCompactionInvariantViolated, emitCompactionSkipped, emitCompactionStarted, } from '../context/compaction-events.js';
|
|
56
|
-
/**
|
|
57
|
-
* Engine-loop callback. See file header for the expected callsite shape.
|
|
58
|
-
*
|
|
59
|
-
* Contract:
|
|
60
|
-
* - Never throws. All errors degrade to `committed: false` with the
|
|
61
|
-
* original transcript and an event record.
|
|
62
|
-
* - On `committed: true`, the caller MUST adopt `newTranscript` as
|
|
63
|
-
* the live working transcript for the next model turn.
|
|
64
|
-
* - On `committed: false`, the caller MUST keep the input transcript
|
|
65
|
-
* and try again on the next tool turn (compaction will retry once
|
|
66
|
-
* pressure stays above threshold).
|
|
67
|
-
*/
|
|
68
|
-
export async function maybeCompactAfterTool(input) {
|
|
69
|
-
const compactionInput = {
|
|
70
|
-
sessionId: input.session.id,
|
|
71
|
-
contextBudgetUsed: input.contextBudgetUsed,
|
|
72
|
-
contextBudgetMax: input.contextBudgetMax,
|
|
73
|
-
toolOutputs: input.toolOutputs,
|
|
74
|
-
transcript: input.transcript,
|
|
75
|
-
workspaceRoot: input.workspaceRoot,
|
|
76
|
-
};
|
|
77
|
-
const tier = selectTier(compactionInput);
|
|
78
|
-
emitCompactionStarted(input.session, tier, {
|
|
79
|
-
budgetUsed: input.contextBudgetUsed,
|
|
80
|
-
budgetMax: input.contextBudgetMax,
|
|
81
|
-
});
|
|
82
|
-
let result;
|
|
83
|
-
try {
|
|
84
|
-
result = await runCompaction(compactionInput, tier);
|
|
85
|
-
}
|
|
86
|
-
catch (error) {
|
|
87
|
-
const reason = error instanceof Error ? error.message : String(error);
|
|
88
|
-
emitCompactionSkipped(input.session, tier, `compaction crashed: ${reason}`);
|
|
89
|
-
return {
|
|
90
|
-
committed: false,
|
|
91
|
-
tier,
|
|
92
|
-
newTranscript: input.transcript,
|
|
93
|
-
bytesReclaimed: 0,
|
|
94
|
-
newContextSize: byteSize(input.transcript),
|
|
95
|
-
violations: [],
|
|
96
|
-
skipped: true,
|
|
97
|
-
skipReason: `crashed: ${reason}`,
|
|
98
|
-
};
|
|
99
|
-
}
|
|
100
|
-
if (result.skipped) {
|
|
101
|
-
emitCompactionSkipped(input.session, tier, result.skipReason || 'no work');
|
|
102
|
-
return {
|
|
103
|
-
committed: false,
|
|
104
|
-
tier,
|
|
105
|
-
newTranscript: input.transcript,
|
|
106
|
-
bytesReclaimed: 0,
|
|
107
|
-
newContextSize: byteSize(input.transcript),
|
|
108
|
-
violations: [],
|
|
109
|
-
skipped: true,
|
|
110
|
-
skipReason: result.skipReason,
|
|
111
|
-
};
|
|
112
|
-
}
|
|
113
|
-
// Invariant gate: static-hash-unchanged is enforced by passing the
|
|
114
|
-
// same hashes in for `before` and `after` — compaction never touches
|
|
115
|
-
// static blocks, so the hashes are equal by construction. We pass
|
|
116
|
-
// both so the contract is explicit; if a future tier introduces a
|
|
117
|
-
// bug that overwrites static state, the check still catches it.
|
|
118
|
-
const violations = checkInvariants({
|
|
119
|
-
before: compactionInput,
|
|
120
|
-
after: result,
|
|
121
|
-
summaryText: result.summaryText,
|
|
122
|
-
staticHashBefore: input.contextStaticHash,
|
|
123
|
-
staticHashAfter: input.contextStaticHash,
|
|
124
|
-
});
|
|
125
|
-
if (violations.length > 0) {
|
|
126
|
-
for (const v of violations)
|
|
127
|
-
emitCompactionInvariantViolated(input.session, v);
|
|
128
|
-
return {
|
|
129
|
-
committed: false,
|
|
130
|
-
tier,
|
|
131
|
-
newTranscript: input.transcript,
|
|
132
|
-
bytesReclaimed: 0,
|
|
133
|
-
newContextSize: byteSize(input.transcript),
|
|
134
|
-
violations,
|
|
135
|
-
skipped: false,
|
|
136
|
-
skipReason: '',
|
|
137
|
-
};
|
|
138
|
-
}
|
|
139
|
-
emitCompactionCompleted(input.session, tier, result.bytesReclaimed, result.newContextSize, result.artifactsCreated);
|
|
140
|
-
return {
|
|
141
|
-
committed: true,
|
|
142
|
-
tier,
|
|
143
|
-
newTranscript: result.newTranscript,
|
|
144
|
-
bytesReclaimed: result.bytesReclaimed,
|
|
145
|
-
newContextSize: result.newContextSize,
|
|
146
|
-
violations: [],
|
|
147
|
-
skipped: false,
|
|
148
|
-
skipReason: '',
|
|
149
|
-
};
|
|
150
|
-
}
|
|
151
|
-
function byteSize(transcript) {
|
|
152
|
-
return transcript.reduce((sum, t) => sum + Buffer.byteLength(t.content, 'utf8'), 0);
|
|
153
|
-
}
|
|
154
|
-
//# sourceMappingURL=compaction-hook.js.map
|