@pugi/cli 0.1.0-beta.31 → 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/repl/session.js +370 -9
- package/dist/core/repl/slash-commands.js +68 -5
- 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 +453 -11
- 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/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
|
@@ -47,6 +47,22 @@ import { DispatchFSM } from './dispatch-fsm.js';
|
|
|
47
47
|
import { computeCostUsd, formatCostUsd, formatTokens } from './model-pricing.js';
|
|
48
48
|
const MAX_TRANSCRIPT_ROWS = 500;
|
|
49
49
|
const MAX_TOOL_CALLS = 200;
|
|
50
|
+
/**
|
|
51
|
+
* Wave 6 small-CC-parity batch (2026-05-27): width cap for the inline
|
|
52
|
+
* `streamingDelta` tail rendered next to the args while the call is
|
|
53
|
+
* `running`. Keeps the tool-stream row single-line on an 80-col
|
|
54
|
+
* terminal even when Bash output blasts through stdout. Exported so the
|
|
55
|
+
* spec can pin the truncation behaviour.
|
|
56
|
+
*/
|
|
57
|
+
export const STREAMING_DELTA_MAX_CHARS = 80;
|
|
58
|
+
/**
|
|
59
|
+
* Wave 6 small-CC-parity batch (2026-05-27): character cap for the
|
|
60
|
+
* collapsed `resultPreview` on a completed row. The pane shows
|
|
61
|
+
* `✓ Read(file) OK (2ms) "first 50 chars…"` so the operator sees what
|
|
62
|
+
* the tool produced without expanding. Per CEO spec (50 chars).
|
|
63
|
+
* Exported so the spec + the pane share one source of truth.
|
|
64
|
+
*/
|
|
65
|
+
export const RESULT_PREVIEW_MAX_CHARS = 50;
|
|
50
66
|
const MAX_RECONNECT_ATTEMPTS = 10;
|
|
51
67
|
const RECONNECT_BASE_MS = 250;
|
|
52
68
|
const RECONNECT_MAX_MS = 5_000;
|
|
@@ -397,6 +413,13 @@ export class ReplSession {
|
|
|
397
413
|
// a failed flush leaves the queue intact for the next start.
|
|
398
414
|
// Never blocks bootstrap.
|
|
399
415
|
void this.flushFeedbackQueueOnBootstrap().catch(() => undefined);
|
|
416
|
+
// Wave 6 BT 9 Phase 2 (2026-05-27): codegraph cold-start hook.
|
|
417
|
+
// Surfaces ONE of two nudges:
|
|
418
|
+
// - stale-index reminder ("Codegraph index is N days old…")
|
|
419
|
+
// - 30-day post-decline reminder ("Detected medium TS repo…")
|
|
420
|
+
// Skips silently in every other case. Best-effort — a failed
|
|
421
|
+
// detection NEVER blocks bootstrap (the helper itself catches).
|
|
422
|
+
void this.runCodegraphColdStart().catch(() => undefined);
|
|
400
423
|
}
|
|
401
424
|
catch (error) {
|
|
402
425
|
this.appendSystemLine(`Could not open Pugi session: ${this.errorMessage(error)}`);
|
|
@@ -455,6 +478,47 @@ export class ReplSession {
|
|
|
455
478
|
apiKey: this.options.apiKey,
|
|
456
479
|
});
|
|
457
480
|
}
|
|
481
|
+
/**
|
|
482
|
+
* Wave 6 BT 9 Phase 2 (2026-05-27): codegraph cold-start nudge.
|
|
483
|
+
*
|
|
484
|
+
* Surfaces ONE of two nudges on REPL boot when the gate trips:
|
|
485
|
+
* - 30-day post-decline reminder ("Detected medium TS repo…")
|
|
486
|
+
* - stale-index reminder ("Codegraph index is N days old…")
|
|
487
|
+
*
|
|
488
|
+
* The evaluator is pure; we stamp `lastReindexCheckAt` here so the
|
|
489
|
+
* stale-index nudge throttles к once-per-day. The init-flow first-
|
|
490
|
+
* run prompt is handled separately by `pugi init` to avoid double-
|
|
491
|
+
* prompting в the common "init + then code" boot sequence.
|
|
492
|
+
*
|
|
493
|
+
* Best-effort: any error inside the codegraph module is swallowed —
|
|
494
|
+
* a cold-start nudge that breaks the REPL would be worse than no
|
|
495
|
+
* nudge at all.
|
|
496
|
+
*/
|
|
497
|
+
async runCodegraphColdStart() {
|
|
498
|
+
try {
|
|
499
|
+
const workspaceRoot = this.options.workspace?.workspaceCwd ?? process.cwd();
|
|
500
|
+
const { evaluateColdStart } = await import('../codegraph/offer-hook.js');
|
|
501
|
+
const verdict = evaluateColdStart({ workspaceRoot });
|
|
502
|
+
if (verdict.kind === 'silent')
|
|
503
|
+
return;
|
|
504
|
+
if (verdict.kind === 'stale-index') {
|
|
505
|
+
this.appendSystemLine(verdict.message);
|
|
506
|
+
const { markReindexChecked } = await import('../codegraph/decision-store.js');
|
|
507
|
+
markReindexChecked(workspaceRoot);
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
// 'remind' — surface the offer copy as a system line. Operator
|
|
511
|
+
// accepts via `/codegraph-status --install` OR explicitly via
|
|
512
|
+
// `pugi mcp install codegraph codegraph serve --mcp`.
|
|
513
|
+
this.appendSystemLine('');
|
|
514
|
+
this.appendSystemLine(verdict.message);
|
|
515
|
+
this.appendSystemLine(' Accept: run `pugi mcp install codegraph codegraph serve --mcp && pugi mcp trust codegraph`');
|
|
516
|
+
this.appendSystemLine(' Skip: /codegraph-status to inspect the decision; the prompt re-appears in 30 days');
|
|
517
|
+
}
|
|
518
|
+
catch {
|
|
519
|
+
// Codegraph nudge is decoration — failure must NEVER surface.
|
|
520
|
+
}
|
|
521
|
+
}
|
|
458
522
|
/**
|
|
459
523
|
* Tear down the SSE stream and stop the reconnect timer. The session
|
|
460
524
|
* id stays valid server-side; `pugi resume <id>` reopens later.
|
|
@@ -714,7 +778,43 @@ export class ReplSession {
|
|
|
714
778
|
return verdict;
|
|
715
779
|
}
|
|
716
780
|
case 'jobs': {
|
|
717
|
-
|
|
781
|
+
// Wave 6 cleanup (2026-05-27): `/jobs --watch` mounts the
|
|
782
|
+
// live Ink TUI from inside the REPL. The dispatcher does NOT
|
|
783
|
+
// mount the watcher itself (that would unmount the REPL's
|
|
784
|
+
// own Ink tree) — instead it surfaces the shell command so
|
|
785
|
+
// the operator runs the watcher in a fresh terminal. Bare
|
|
786
|
+
// `/jobs` continues to render the one-shot snapshot.
|
|
787
|
+
if (verdict.watch) {
|
|
788
|
+
this.appendSystemLine('Run `pugi jobs --watch` from a fresh shell — the live TUI cannot share the REPL Ink tree.');
|
|
789
|
+
}
|
|
790
|
+
else {
|
|
791
|
+
await this.dispatchJobs();
|
|
792
|
+
}
|
|
793
|
+
return verdict;
|
|
794
|
+
}
|
|
795
|
+
case 'cancel': {
|
|
796
|
+
// Wave 6 small-CC-parity batch (2026-05-27): forward the parsed
|
|
797
|
+
// mode + dispatchId to `runCancelCommand`. The dispatcher uses
|
|
798
|
+
// a dynamic import so the cancel module's filesystem helpers
|
|
799
|
+
// stay out of the REPL keystroke hot path; same separation as
|
|
800
|
+
// `/redo`, `/prd-check`, `/chain`. The runner writes its
|
|
801
|
+
// output lines through `appendSystemLine` so the verdict
|
|
802
|
+
// lands on the system pane alongside other slash results.
|
|
803
|
+
try {
|
|
804
|
+
const { runCancelCommand } = await import('../../runtime/commands/cancel.js');
|
|
805
|
+
const cancelMode = verdict.mode === 'list'
|
|
806
|
+
? { kind: 'list' }
|
|
807
|
+
: verdict.mode === 'all'
|
|
808
|
+
? { kind: 'all' }
|
|
809
|
+
: { kind: 'one', dispatchId: verdict.dispatchId };
|
|
810
|
+
await runCancelCommand(cancelMode, {
|
|
811
|
+
write: (line) => this.appendSystemLine(line),
|
|
812
|
+
});
|
|
813
|
+
}
|
|
814
|
+
catch (err) {
|
|
815
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
816
|
+
this.appendSystemLine(`/cancel failed: ${message}`);
|
|
817
|
+
}
|
|
718
818
|
return verdict;
|
|
719
819
|
}
|
|
720
820
|
case 'diff': {
|
|
@@ -1131,6 +1231,38 @@ export class ReplSession {
|
|
|
1131
1231
|
}
|
|
1132
1232
|
return verdict;
|
|
1133
1233
|
}
|
|
1234
|
+
case 'codegraph-status': {
|
|
1235
|
+
// Wave 6 BT 9 Phase 2 (2026-05-27): forward to the runner. The
|
|
1236
|
+
// bare form renders the four-row status table; flags handle
|
|
1237
|
+
// install / reindex / offer. Dynamic import keeps the
|
|
1238
|
+
// codegraph module out of the REPL hot path until first use.
|
|
1239
|
+
try {
|
|
1240
|
+
const { runCodegraphStatusCommand } = await import('../../runtime/commands/codegraph-status.js');
|
|
1241
|
+
const lines = [];
|
|
1242
|
+
const workspaceRoot = this.options.workspace?.workspaceCwd ?? process.cwd();
|
|
1243
|
+
await runCodegraphStatusCommand(verdict.args, {
|
|
1244
|
+
workspaceRoot,
|
|
1245
|
+
writeOutput: (_payload, text) => {
|
|
1246
|
+
for (const raw of text.split('\n')) {
|
|
1247
|
+
const trimmed = raw.replace(/\s+$/u, '');
|
|
1248
|
+
lines.push(trimmed);
|
|
1249
|
+
}
|
|
1250
|
+
},
|
|
1251
|
+
});
|
|
1252
|
+
if (lines.length === 0) {
|
|
1253
|
+
this.appendSystemLine('/codegraph-status: no output.');
|
|
1254
|
+
}
|
|
1255
|
+
else {
|
|
1256
|
+
for (const line of lines)
|
|
1257
|
+
this.appendSystemLine(line);
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
catch (error) {
|
|
1261
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1262
|
+
this.appendSystemLine(`/codegraph-status failed: ${message}`);
|
|
1263
|
+
}
|
|
1264
|
+
return verdict;
|
|
1265
|
+
}
|
|
1134
1266
|
case 'permissions': {
|
|
1135
1267
|
// Leak L6: handle the `/permissions [mode] [--persist]` flow.
|
|
1136
1268
|
// The session module forwards to the runtime helper so the
|
|
@@ -1517,6 +1649,39 @@ export class ReplSession {
|
|
|
1517
1649
|
}
|
|
1518
1650
|
return verdict;
|
|
1519
1651
|
}
|
|
1652
|
+
case 'redo': {
|
|
1653
|
+
// Wave 6 cleanup (2026-05-27): counterpart к /undo. The runtime
|
|
1654
|
+
// command `runRedoCommand` consumes one entry from the LIFO
|
|
1655
|
+
// undo stack (most recent unconsumed `tool=undo` result), reads
|
|
1656
|
+
// the captured AFTER content from `.pugi/undo-blobs/`, and
|
|
1657
|
+
// re-applies the mutations under the same mtime+hash external-
|
|
1658
|
+
// modification gate the undo runner uses. Same dynamic-import
|
|
1659
|
+
// posture as /undo so the redo + blob-store + git plumbing
|
|
1660
|
+
// stays out of the REPL hot path.
|
|
1661
|
+
try {
|
|
1662
|
+
const [{ runRedoCommand }, { openSession }] = await Promise.all([
|
|
1663
|
+
import('../../runtime/commands/redo.js'),
|
|
1664
|
+
import('../session.js'),
|
|
1665
|
+
]);
|
|
1666
|
+
const workspaceRoot = process.cwd();
|
|
1667
|
+
const session = openSession(workspaceRoot);
|
|
1668
|
+
this.appendSystemLine('Reapplying last undo...');
|
|
1669
|
+
await runRedoCommand([], {
|
|
1670
|
+
workspaceRoot,
|
|
1671
|
+
session,
|
|
1672
|
+
writeOutput: (_payload, text) => {
|
|
1673
|
+
const trimmed = text.replace(/\n+$/u, '');
|
|
1674
|
+
if (trimmed.length > 0)
|
|
1675
|
+
this.appendSystemLine(trimmed);
|
|
1676
|
+
},
|
|
1677
|
+
});
|
|
1678
|
+
}
|
|
1679
|
+
catch (error) {
|
|
1680
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1681
|
+
this.appendSystemLine(`/redo failed: ${message}`);
|
|
1682
|
+
}
|
|
1683
|
+
return verdict;
|
|
1684
|
+
}
|
|
1520
1685
|
case 'stub': {
|
|
1521
1686
|
this.appendSystemLine(verdict.message);
|
|
1522
1687
|
return verdict;
|
|
@@ -1863,22 +2028,87 @@ export class ReplSession {
|
|
|
1863
2028
|
try {
|
|
1864
2029
|
const registry = getJobRegistry();
|
|
1865
2030
|
const entries = await registry.list();
|
|
1866
|
-
|
|
2031
|
+
// Wave 6 cleanup (2026-05-27): also scan `.pugi/agent-progress/*.json`
|
|
2032
|
+
// so long-running external agents (the JSON pattern from
|
|
2033
|
+
// `feedback_agent_progress_tracking_pattern.md`) show up next к
|
|
2034
|
+
// background-bash entries. The two surfaces are orthogonal — bash
|
|
2035
|
+
// jobs come from the in-process registry, agent-progress comes from
|
|
2036
|
+
// sidecar JSON written by any agent (Pugi-spawned or external) — so
|
|
2037
|
+
// we render both, sorted with running first.
|
|
2038
|
+
const agentProgressRows = await this.collectAgentProgressRows();
|
|
2039
|
+
if (entries.length === 0 && agentProgressRows.length === 0) {
|
|
1867
2040
|
this.appendSystemLine('No background jobs tracked.');
|
|
1868
2041
|
return;
|
|
1869
2042
|
}
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
const
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
2043
|
+
if (entries.length > 0) {
|
|
2044
|
+
this.appendSystemLine(`Background jobs (${entries.length}):`);
|
|
2045
|
+
for (const entry of entries) {
|
|
2046
|
+
const id = entry.id.replace(/^pj-/, '').slice(0, 8);
|
|
2047
|
+
const status = entry.status;
|
|
2048
|
+
const cmd = entry.command.length > 48 ? `${entry.command.slice(0, 47)}…` : entry.command;
|
|
2049
|
+
this.appendSystemLine(` ${id} ${status.padEnd(10)} ${cmd}`);
|
|
2050
|
+
}
|
|
2051
|
+
}
|
|
2052
|
+
if (agentProgressRows.length > 0) {
|
|
2053
|
+
this.appendSystemLine(`Agent progress (${agentProgressRows.length}):`);
|
|
2054
|
+
for (const row of agentProgressRows) {
|
|
2055
|
+
this.appendSystemLine(` ${row}`);
|
|
2056
|
+
}
|
|
2057
|
+
this.appendSystemLine('Tip: run `pugi jobs --watch` for the live Ink TUI.');
|
|
1876
2058
|
}
|
|
1877
2059
|
}
|
|
1878
2060
|
catch (error) {
|
|
1879
2061
|
this.appendSystemLine(`/jobs failed: ${this.errorMessage(error)}`);
|
|
1880
2062
|
}
|
|
1881
2063
|
}
|
|
2064
|
+
/**
|
|
2065
|
+
* Wave 6 cleanup (2026-05-27): scan `.pugi/agent-progress/*.json`
|
|
2066
|
+
* for in-flight long-running agent tasks and emit a one-line per
|
|
2067
|
+
* agent for the `/jobs` snapshot. Sorting matches the live TUI's
|
|
2068
|
+
* `sortProgressEntries` (running first, then by lastUpdate desc).
|
|
2069
|
+
*
|
|
2070
|
+
* Best-effort: a missing dir, malformed JSON, or bad permissions
|
|
2071
|
+
* yields an empty list and a swallowed error — the in-process
|
|
2072
|
+
* registry view is the older well-tested surface and must never be
|
|
2073
|
+
* gated behind a sidecar dir's health.
|
|
2074
|
+
*/
|
|
2075
|
+
async collectAgentProgressRows() {
|
|
2076
|
+
try {
|
|
2077
|
+
const [{ resolveProgressDir }, { readProgressFile, sortProgressEntries }, fs, path] = await Promise.all([
|
|
2078
|
+
import('../agent-progress/writer.js'),
|
|
2079
|
+
import('../../commands/jobs-watch.js'),
|
|
2080
|
+
import('node:fs'),
|
|
2081
|
+
import('node:path'),
|
|
2082
|
+
]);
|
|
2083
|
+
const dir = resolveProgressDir();
|
|
2084
|
+
if (!fs.existsSync(dir))
|
|
2085
|
+
return [];
|
|
2086
|
+
const files = fs
|
|
2087
|
+
.readdirSync(dir)
|
|
2088
|
+
.filter((f) => f.endsWith('.json'))
|
|
2089
|
+
.map((f) => path.join(dir, f));
|
|
2090
|
+
const progress = files
|
|
2091
|
+
.map((p) => readProgressFile(p))
|
|
2092
|
+
.filter((p) => p !== undefined);
|
|
2093
|
+
const sorted = sortProgressEntries(progress);
|
|
2094
|
+
return sorted.map((p) => {
|
|
2095
|
+
const id = p.agentId.length > 24 ? `${p.agentId.slice(0, 23)}…` : p.agentId;
|
|
2096
|
+
const pct = `${String(Math.round(p.percentComplete)).padStart(3, ' ')}%`;
|
|
2097
|
+
const elapsedSec = Math.max(0, Math.floor(p.elapsedMs / 1000));
|
|
2098
|
+
const elapsed = elapsedSec >= 60
|
|
2099
|
+
? `${Math.floor(elapsedSec / 60)}m${String(elapsedSec % 60).padStart(2, '0')}s`
|
|
2100
|
+
: `${elapsedSec}s`;
|
|
2101
|
+
const status = p.status.padEnd(9, ' ');
|
|
2102
|
+
const step = p.stepDescription.length > 36
|
|
2103
|
+
? `${p.stepDescription.slice(0, 35)}…`
|
|
2104
|
+
: p.stepDescription;
|
|
2105
|
+
return `${id.padEnd(24, ' ')} ${status} ${pct} ${elapsed.padStart(6, ' ')} ${step}`;
|
|
2106
|
+
});
|
|
2107
|
+
}
|
|
2108
|
+
catch {
|
|
2109
|
+
return [];
|
|
2110
|
+
}
|
|
2111
|
+
}
|
|
1882
2112
|
dispatchDiff() {
|
|
1883
2113
|
try {
|
|
1884
2114
|
const artifactsRoot = resolvePath(process.cwd(), '.pugi', 'artifacts');
|
|
@@ -2015,14 +2245,25 @@ export class ReplSession {
|
|
|
2015
2245
|
['review', used.review, caps.review],
|
|
2016
2246
|
['engine', used.engine, caps.engine],
|
|
2017
2247
|
];
|
|
2248
|
+
// Wave 6 cleanup (2026-05-27): color-code each counter row by
|
|
2249
|
+
// utilisation. The thresholds match Claude Code's tier-meter
|
|
2250
|
+
// convention so operators trained on that surface read the same
|
|
2251
|
+
// signal here. ANSI codes wrap the WHOLE row (not just the
|
|
2252
|
+
// percent) so the line wraps as one visual unit; the cost-quota
|
|
2253
|
+
// spec regex still matches because anchors are inside the
|
|
2254
|
+
// wrapped substring.
|
|
2018
2255
|
for (const [name, value, cap] of counters) {
|
|
2019
2256
|
const v = typeof value === 'number' ? value : 0;
|
|
2020
2257
|
if (cap === null || cap === undefined) {
|
|
2258
|
+
// Unlimited counters never trip the gauge — leave them
|
|
2259
|
+
// uncolored so the eye does not register an alarm signal
|
|
2260
|
+
// where there is no cap к exhaust.
|
|
2021
2261
|
this.appendSystemLine(` ${name.padEnd(7, ' ')} ${v.toLocaleString()} / unlimited`);
|
|
2022
2262
|
}
|
|
2023
2263
|
else {
|
|
2024
2264
|
const pct = cap > 0 ? Math.round((v / cap) * 100) : 0;
|
|
2025
|
-
|
|
2265
|
+
const row = ` ${name.padEnd(7, ' ')} ${v.toLocaleString()} / ${cap.toLocaleString()} (${pct}%)`;
|
|
2266
|
+
this.appendSystemLine(colorizeQuotaRow(row, pct));
|
|
2026
2267
|
}
|
|
2027
2268
|
}
|
|
2028
2269
|
}
|
|
@@ -2079,6 +2320,17 @@ export class ReplSession {
|
|
|
2079
2320
|
liveTokensUsed: liveTokens >= 0 ? liveTokens : 0,
|
|
2080
2321
|
lastCommand,
|
|
2081
2322
|
lastCommandAtEpochMs,
|
|
2323
|
+
// Repl-mode context: the session knows both the live
|
|
2324
|
+
// transport URL and the operator's workspace label, so we
|
|
2325
|
+
// forward them as authoritative inputs к the snapshot.
|
|
2326
|
+
// The status snapshot used к infer these from the
|
|
2327
|
+
// credentials file, which was wrong in two cases:
|
|
2328
|
+
// (a) the operator was inside a REPL talking к Anvil dev
|
|
2329
|
+
// (port 4100) but credentials still pointed к
|
|
2330
|
+
// api.pugi.io — the `Backend` row mis-reported;
|
|
2331
|
+
// (b) `workspaceLabel` was никогда rendered at all.
|
|
2332
|
+
liveApiUrl: this.options.apiUrl,
|
|
2333
|
+
workspaceLabel: this.options.workspaceLabel,
|
|
2082
2334
|
writeOutput: (_payload, text) => {
|
|
2083
2335
|
for (const line of text.split('\n')) {
|
|
2084
2336
|
const trimmed = line.replace(/\s+$/u, '');
|
|
@@ -3098,6 +3350,73 @@ export class ReplSession {
|
|
|
3098
3350
|
const agent = this.state.agents.find((a) => a.taskId === taskId);
|
|
3099
3351
|
return agent?.personaSlug ?? 'unknown';
|
|
3100
3352
|
}
|
|
3353
|
+
/**
|
|
3354
|
+
* Wave 6 small-CC-parity batch (2026-05-27): public ingest path for
|
|
3355
|
+
* a backend-driven `tool.call.delta` event. Appends the delta tail
|
|
3356
|
+
* onto the row's `streamingDelta` (capped at
|
|
3357
|
+
* `STREAMING_DELTA_MAX_CHARS` so the row stays single-line) when the
|
|
3358
|
+
* id matches a `running` row. No-op when the id is unknown OR when
|
|
3359
|
+
* the row already transitioned to a terminal status — late deltas
|
|
3360
|
+
* from a completed call must not overwrite the final detail.
|
|
3361
|
+
*
|
|
3362
|
+
* The renderer in `tool-stream-pane.tsx` reads `streamingDelta` to
|
|
3363
|
+
* paint the inline preview after the canonical args. This method is
|
|
3364
|
+
* the seam the future admin-api SSE consumer hooks into; until then
|
|
3365
|
+
* the spec drives it directly so the delta-append branch is locked
|
|
3366
|
+
* down behaviourally.
|
|
3367
|
+
*/
|
|
3368
|
+
appendToolCallDelta(id, deltaChunk) {
|
|
3369
|
+
if (!id || !deltaChunk)
|
|
3370
|
+
return;
|
|
3371
|
+
const idx = this.state.toolCalls.findIndex((c) => c.id === id);
|
|
3372
|
+
if (idx < 0)
|
|
3373
|
+
return;
|
|
3374
|
+
const existing = this.state.toolCalls[idx];
|
|
3375
|
+
if (existing.status !== 'running')
|
|
3376
|
+
return;
|
|
3377
|
+
const current = existing.streamingDelta ?? '';
|
|
3378
|
+
let combined = current + deltaChunk;
|
|
3379
|
+
if (combined.length > STREAMING_DELTA_MAX_CHARS) {
|
|
3380
|
+
// Keep the TAIL — the operator wants the freshest bytes (the
|
|
3381
|
+
// line being written right now), not the stale head. The leading
|
|
3382
|
+
// ellipsis signals truncation.
|
|
3383
|
+
combined = `…${combined.slice(combined.length - STREAMING_DELTA_MAX_CHARS + 1)}`;
|
|
3384
|
+
}
|
|
3385
|
+
const next = this.state.toolCalls.slice();
|
|
3386
|
+
next[idx] = { ...existing, streamingDelta: combined };
|
|
3387
|
+
this.patch({ toolCalls: next });
|
|
3388
|
+
}
|
|
3389
|
+
/**
|
|
3390
|
+
* Wave 6 small-CC-parity batch (2026-05-27): public ingest path for
|
|
3391
|
+
* the terminal `tool.call.end` event. Flips the row to `ok` / `error`
|
|
3392
|
+
* with the resolved duration + optional result preview. Cleans up the
|
|
3393
|
+
* transient `streamingDelta` so the completed row renders cleanly
|
|
3394
|
+
* without the live tail. No-op when the id is unknown.
|
|
3395
|
+
*/
|
|
3396
|
+
endToolCall(input) {
|
|
3397
|
+
if (!input.id)
|
|
3398
|
+
return;
|
|
3399
|
+
const idx = this.state.toolCalls.findIndex((c) => c.id === input.id);
|
|
3400
|
+
if (idx < 0)
|
|
3401
|
+
return;
|
|
3402
|
+
const existing = this.state.toolCalls[idx];
|
|
3403
|
+
const endedAt = input.endedAtEpochMs ?? Date.now();
|
|
3404
|
+
const durationMs = Math.max(0, endedAt - existing.startedAtEpochMs);
|
|
3405
|
+
const preview = input.resultPreview
|
|
3406
|
+
? truncatePreview(input.resultPreview, RESULT_PREVIEW_MAX_CHARS)
|
|
3407
|
+
: undefined;
|
|
3408
|
+
const next = this.state.toolCalls.slice();
|
|
3409
|
+
next[idx] = {
|
|
3410
|
+
...existing,
|
|
3411
|
+
status: input.status,
|
|
3412
|
+
detail: input.detail ?? existing.detail,
|
|
3413
|
+
resultLines: input.resultLines ?? existing.resultLines,
|
|
3414
|
+
durationMs,
|
|
3415
|
+
resultPreview: preview,
|
|
3416
|
+
streamingDelta: undefined,
|
|
3417
|
+
};
|
|
3418
|
+
this.patch({ toolCalls: next });
|
|
3419
|
+
}
|
|
3101
3420
|
/**
|
|
3102
3421
|
* Fold a tool call entry into the rolling list. If the entry id
|
|
3103
3422
|
* already exists, replace it in-place (so a synthesised `running` →
|
|
@@ -3671,6 +3990,30 @@ function formatResetWindow(resetAtIso, nowEpochMs) {
|
|
|
3671
3990
|
const minutes = Math.max(1, Math.floor(deltaMs / (60 * 1000)));
|
|
3672
3991
|
return `in ${minutes}m`;
|
|
3673
3992
|
}
|
|
3993
|
+
/**
|
|
3994
|
+
* Wave 6 cleanup (2026-05-27): wrap a `/quota` counter row in ANSI
|
|
3995
|
+
* color codes by utilisation percent. Thresholds match Claude Code's
|
|
3996
|
+
* tier-meter convention so operators trained on that surface read the
|
|
3997
|
+
* same signal here:
|
|
3998
|
+
*
|
|
3999
|
+
* - 0..70% → green (32m) — comfortable headroom
|
|
4000
|
+
* - 70..90% → yellow (33m) — approaching cap, plan ahead
|
|
4001
|
+
* - 90..100% → red (31m) — burn rate alarm, throttle now
|
|
4002
|
+
*
|
|
4003
|
+
* The wrap is whole-row (not just the percent) so the eye registers
|
|
4004
|
+
* the level on the line, not just the trailing parenthesis. Tests
|
|
4005
|
+
* that match the inner row text via regex are unaffected because the
|
|
4006
|
+
* regex anchors live inside the wrapped substring; the ANSI codes
|
|
4007
|
+
* sit at the boundaries.
|
|
4008
|
+
*/
|
|
4009
|
+
export function colorizeQuotaRow(row, pct) {
|
|
4010
|
+
const RESET = '\x1b[0m';
|
|
4011
|
+
if (pct >= 90)
|
|
4012
|
+
return `\x1b[31m${row}${RESET}`;
|
|
4013
|
+
if (pct >= 70)
|
|
4014
|
+
return `\x1b[33m${row}${RESET}`;
|
|
4015
|
+
return `\x1b[32m${row}${RESET}`;
|
|
4016
|
+
}
|
|
3674
4017
|
/* ------------------------------------------------------------------ */
|
|
3675
4018
|
/* Tool call synthesiser - α6.12 */
|
|
3676
4019
|
/* ------------------------------------------------------------------ */
|
|
@@ -3722,6 +4065,24 @@ export function synthesiseToolCall(input) {
|
|
|
3722
4065
|
startedAtEpochMs: input.now,
|
|
3723
4066
|
};
|
|
3724
4067
|
}
|
|
4068
|
+
/**
|
|
4069
|
+
* Wave 6 small-CC-parity batch (2026-05-27): collapse a multi-line
|
|
4070
|
+
* result preview down to a single-line head capped at `max` chars. The
|
|
4071
|
+
* collapsed-result row on a completed tool call uses this so the
|
|
4072
|
+
* preview never expands the row vertically. Exported для the spec so
|
|
4073
|
+
* the truncation behaviour is locked down.
|
|
4074
|
+
*/
|
|
4075
|
+
export function truncatePreview(value, max) {
|
|
4076
|
+
if (!value)
|
|
4077
|
+
return '';
|
|
4078
|
+
// Strip CR/LF + tab so the preview stays single-line. Multiple
|
|
4079
|
+
// whitespace runs collapse to single space — operator wants signal,
|
|
4080
|
+
// not formatting noise.
|
|
4081
|
+
const single = value.replace(/[\r\n\t]+/g, ' ').replace(/\s{2,}/g, ' ').trim();
|
|
4082
|
+
if (single.length <= max)
|
|
4083
|
+
return single;
|
|
4084
|
+
return `${single.slice(0, Math.max(0, max - 1))}…`;
|
|
4085
|
+
}
|
|
3725
4086
|
function normaliseToolName(raw) {
|
|
3726
4087
|
const lower = raw.toLowerCase();
|
|
3727
4088
|
if (lower === 'webfetch' || lower === 'web_fetch')
|
|
@@ -63,7 +63,8 @@ export const SLASH_COMMAND_HELP = Object.freeze([
|
|
|
63
63
|
{ name: 'agents', args: '', gloss: 'List the on-watch agent roster', group: 'Workforce dispatch' },
|
|
64
64
|
{ name: 'delegate', args: '<slug> <brief>', gloss: 'Dispatch a brief to one Tier 1 specialist (α7.5)', group: 'Workforce dispatch' },
|
|
65
65
|
{ name: 'stop', args: '<persona>', gloss: 'Stop one agent by persona slug', group: 'Workforce dispatch' },
|
|
66
|
-
{ name: 'jobs', args: '', gloss: 'List background jobs', group: 'Workforce dispatch' },
|
|
66
|
+
{ name: 'jobs', args: '[--watch]', gloss: 'List background jobs + agent-progress; --watch mounts the live Ink TUI', group: 'Workforce dispatch' },
|
|
67
|
+
{ name: 'cancel', args: '[<id> | all]', gloss: 'Halt active dispatch by id (Wave 6)', group: 'Workforce dispatch' },
|
|
67
68
|
{ name: 'ask', args: '<question>', gloss: 'Surface a yes/no modal locally (α6.3 forcing question)', group: 'Workforce dispatch' },
|
|
68
69
|
// Session
|
|
69
70
|
{ name: 'clear', args: '', gloss: 'Clear conversation pane', group: 'Session' },
|
|
@@ -81,6 +82,7 @@ export const SLASH_COMMAND_HELP = Object.freeze([
|
|
|
81
82
|
{ name: 'status', args: '', gloss: 'Session snapshot — id · cwd · mode · tokens · dispatches · auth', group: 'Pugi tools' },
|
|
82
83
|
{ name: 'consensus', args: '[ref]', gloss: '3-model consensus review (codex · claude · deepseek)', group: 'Pugi tools' },
|
|
83
84
|
{ name: 'repo-map', args: '[refresh]', gloss: 'AST-light symbol summary of the workspace (leak L28)', group: 'Pugi tools' },
|
|
85
|
+
{ name: 'codegraph-status', args: '[--install|--reindex|--offer]', gloss: 'Codegraph MCP — install state, index age, symbol count, refresh CTA (Wave 6 BT 9 P2)', group: 'Pugi tools' },
|
|
84
86
|
// Settings
|
|
85
87
|
{ name: 'config', args: '', gloss: 'Show config', group: 'Settings', stub: true },
|
|
86
88
|
{ name: 'privacy', args: '', gloss: 'Show privacy mode + contract', group: 'Settings' },
|
|
@@ -94,6 +96,7 @@ export const SLASH_COMMAND_HELP = Object.freeze([
|
|
|
94
96
|
{ name: 'onboarding', args: '[--reset|--non-interactive]', gloss: 'First-run wizard — auth / mode / style / MCP / telemetry (leak L25)', group: 'Settings' },
|
|
95
97
|
{ name: 'vim', args: '[on|off|status]', gloss: 'Toggle vim-style modal editing in the input buffer (leak L26)', group: 'Settings' },
|
|
96
98
|
{ name: 'undo', args: '', gloss: 'Revert the last successful write / edit / multi_edit (Aider walk-back, Wave 6)', group: 'Settings' },
|
|
99
|
+
{ name: 'redo', args: '', gloss: 'Reapply the most recent /undo (LIFO stack, Wave 6 cleanup)', group: 'Settings' },
|
|
97
100
|
// Meta
|
|
98
101
|
{ name: 'help', args: '', gloss: 'Show this help overlay', group: 'Meta' },
|
|
99
102
|
{ name: 'version', args: '', gloss: 'Show CLI version', group: 'Meta' },
|
|
@@ -231,7 +234,48 @@ export function parseSlashCommand(input) {
|
|
|
231
234
|
return { kind: 'version' };
|
|
232
235
|
}
|
|
233
236
|
case 'jobs': {
|
|
234
|
-
|
|
237
|
+
// Wave 6 cleanup (2026-05-27): tokenise the tail so the slash
|
|
238
|
+
// can route `--watch` к the live Ink TUI (same renderer as
|
|
239
|
+
// `pugi jobs --watch`). Unknown tokens fall through silently —
|
|
240
|
+
// the slash surface is intentionally minimal vs. the shell
|
|
241
|
+
// command (which supports list/status/tail/kill subcommands
|
|
242
|
+
// through `runJobsCommand`).
|
|
243
|
+
const tokens = tail.length === 0 ? [] : tail.split(/\s+/).filter((s) => s.length > 0);
|
|
244
|
+
const watch = tokens.includes('--watch') || tokens.includes('-w') || tokens[0] === 'watch';
|
|
245
|
+
return { kind: 'jobs', watch };
|
|
246
|
+
}
|
|
247
|
+
case 'cancel':
|
|
248
|
+
case 'halt': {
|
|
249
|
+
// Wave 6 small-CC-parity batch (2026-05-27):
|
|
250
|
+
//
|
|
251
|
+
// /cancel -> list active dispatches
|
|
252
|
+
// /cancel all -> halt every running dispatch
|
|
253
|
+
// /cancel <id> -> halt one (id may be a prefix; runner
|
|
254
|
+
// does startsWith lookup)
|
|
255
|
+
//
|
|
256
|
+
// The `halt` alias matches operator muscle memory from systemd /
|
|
257
|
+
// brand voice (`stop` is already taken by /stop <persona>; cancel
|
|
258
|
+
// is dispatch-id-keyed, stop is persona-keyed). Unknown extra
|
|
259
|
+
// tokens are tolerated — the runner reads only the first.
|
|
260
|
+
const trimmedTail = tail.trim();
|
|
261
|
+
if (trimmedTail.length === 0) {
|
|
262
|
+
return { kind: 'cancel', mode: 'list', dispatchId: '' };
|
|
263
|
+
}
|
|
264
|
+
const firstToken = trimmedTail.split(/\s+/)[0].toLowerCase();
|
|
265
|
+
if (firstToken === 'all' || firstToken === '*') {
|
|
266
|
+
return { kind: 'cancel', mode: 'all', dispatchId: 'all' };
|
|
267
|
+
}
|
|
268
|
+
// Defensive: dispatch ids are filename-safe per the
|
|
269
|
+
// `validateAgentProgress` agentId regex (`[a-zA-Z0-9_-]+`).
|
|
270
|
+
// Reject anything outside that range with a usage tip so the
|
|
271
|
+
// operator sees the typo before the round-trip.
|
|
272
|
+
if (!/^[A-Za-z0-9_-]+$/.test(firstToken)) {
|
|
273
|
+
return {
|
|
274
|
+
kind: 'error',
|
|
275
|
+
message: `/cancel: invalid dispatch id '${firstToken}'. Use letters / digits / '-' / '_' only.`,
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
return { kind: 'cancel', mode: 'one', dispatchId: firstToken };
|
|
235
279
|
}
|
|
236
280
|
case 'ask': {
|
|
237
281
|
if (tail.length === 0) {
|
|
@@ -477,6 +521,17 @@ export function parseSlashCommand(input) {
|
|
|
477
521
|
const tokens = tail.length === 0 ? [] : tail.split(/\s+/).filter((s) => s.length > 0);
|
|
478
522
|
return { kind: 'chain', args: tokens };
|
|
479
523
|
}
|
|
524
|
+
case 'codegraph-status':
|
|
525
|
+
case 'codegraph': {
|
|
526
|
+
// Wave 6 BT 9 Phase 2 (2026-05-27): forward the tokenized argv
|
|
527
|
+
// to `runCodegraphStatusCommand`. Flags handled by the runner:
|
|
528
|
+
// --install — merge codegraph into .pugi/mcp.json (accept)
|
|
529
|
+
// --reindex — stamp lastIndexedAt + hint runtime to refresh
|
|
530
|
+
// --offer — surface the install prompt even after a decline
|
|
531
|
+
// `/codegraph` is the short alias; same handler.
|
|
532
|
+
const tokens = tail.length === 0 ? [] : tail.split(/\s+/).filter((s) => s.length > 0);
|
|
533
|
+
return { kind: 'codegraph-status', args: tokens };
|
|
534
|
+
}
|
|
480
535
|
case 'compact': {
|
|
481
536
|
// Leak L8 (2026-05-27): graduated from stub. The session module
|
|
482
537
|
// owns the summariser round-trip. Wave 6 BT 8: `--force` overrides
|
|
@@ -561,11 +616,19 @@ export function parseSlashCommand(input) {
|
|
|
561
616
|
// Wave 6 final (2026-05-27): graduated from stub. Tail args are
|
|
562
617
|
// ignored — `runUndoCommand` is parameterless (single-step revert
|
|
563
618
|
// of the most recent successful mutating tool result). Multiple
|
|
564
|
-
// undos = stack of single-step undos.
|
|
565
|
-
//
|
|
566
|
-
//
|
|
619
|
+
// undos = stack of single-step undos. `/redo` is the counterpart
|
|
620
|
+
// (Wave 6 cleanup) — operators ping-pong через undo/redo on the
|
|
621
|
+
// event-log stack without re-running the underlying tool.
|
|
567
622
|
return { kind: 'undo' };
|
|
568
623
|
}
|
|
624
|
+
case 'redo': {
|
|
625
|
+
// Wave 6 cleanup (2026-05-27): counterpart к /undo. Tail args
|
|
626
|
+
// are ignored — `runRedoCommand` is parameterless. Each /redo
|
|
627
|
+
// pops one entry from the LIFO undo stack (the runner tracks
|
|
628
|
+
// which undos have already been consumed by previous redos so
|
|
629
|
+
// double-/redo is a noop, not a double-write).
|
|
630
|
+
return { kind: 'redo' };
|
|
631
|
+
}
|
|
569
632
|
case 'memory':
|
|
570
633
|
case 'config':
|
|
571
634
|
case 'budget': {
|