@pugi/cli 0.1.0-beta.21 → 0.1.0-beta.23

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.
Files changed (63) hide show
  1. package/dist/core/auth/env-provider.js +238 -0
  2. package/dist/core/bare-mode/index.js +107 -0
  3. package/dist/core/diagnostics/probes/bare-mode.js +42 -0
  4. package/dist/core/diagnostics/probes/pugi-md.js +89 -0
  5. package/dist/core/engine/native-pugi.js +55 -11
  6. package/dist/core/engine/prompts.js +30 -2
  7. package/dist/core/engine/tool-bridge.js +32 -0
  8. package/dist/core/feedback/queue.js +177 -0
  9. package/dist/core/feedback/submitter.js +145 -0
  10. package/dist/core/onboarding/marker.js +111 -0
  11. package/dist/core/onboarding/telemetry-state.js +108 -0
  12. package/dist/core/output-style/presets.js +176 -0
  13. package/dist/core/output-style/state.js +185 -0
  14. package/dist/core/permissions/index.js +1 -1
  15. package/dist/core/permissions/state.js +55 -0
  16. package/dist/core/pugi-md/context-injector.js +76 -0
  17. package/dist/core/pugi-md/walk-up.js +207 -0
  18. package/dist/core/release-notes/parser.js +241 -0
  19. package/dist/core/release-notes/state.js +116 -0
  20. package/dist/core/repl/session.js +482 -12
  21. package/dist/core/repl/slash-commands.js +134 -1
  22. package/dist/core/repl/workspace-context.js +22 -0
  23. package/dist/core/share/formatter.js +271 -0
  24. package/dist/core/share/redactor.js +221 -0
  25. package/dist/core/share/uploader.js +267 -0
  26. package/dist/core/theme/context.js +91 -0
  27. package/dist/core/theme/presets.js +228 -0
  28. package/dist/core/theme/state.js +181 -0
  29. package/dist/core/todos/invariant.js +10 -0
  30. package/dist/core/todos/state.js +177 -0
  31. package/dist/core/vim/keymap.js +288 -0
  32. package/dist/core/vim/state.js +92 -0
  33. package/dist/runtime/cli.js +603 -15
  34. package/dist/runtime/commands/doctor.js +21 -0
  35. package/dist/runtime/commands/feedback.js +184 -0
  36. package/dist/runtime/commands/onboarding.js +275 -0
  37. package/dist/runtime/commands/plan.js +143 -0
  38. package/dist/runtime/commands/release-notes.js +229 -0
  39. package/dist/runtime/commands/share.js +316 -0
  40. package/dist/runtime/commands/stickers.js +82 -0
  41. package/dist/runtime/commands/style.js +194 -0
  42. package/dist/runtime/commands/theme.js +196 -0
  43. package/dist/runtime/commands/vim.js +140 -0
  44. package/dist/runtime/version.js +1 -1
  45. package/dist/tools/registry.js +8 -0
  46. package/dist/tools/todo-write.js +184 -0
  47. package/dist/tui/compact-banner.js +28 -1
  48. package/dist/tui/conversation-pane.js +13 -0
  49. package/dist/tui/doctor-table.js +32 -17
  50. package/dist/tui/feedback-prompt.js +156 -0
  51. package/dist/tui/onboarding-wizard.js +240 -0
  52. package/dist/tui/repl-render.js +26 -3
  53. package/dist/tui/repl.js +9 -1
  54. package/dist/tui/stickers-art.js +136 -0
  55. package/dist/tui/style-table.js +28 -0
  56. package/dist/tui/theme-table.js +29 -0
  57. package/dist/tui/vim-input.js +267 -0
  58. package/package.json +2 -2
  59. package/dist/core/engine/compaction-hook.js +0 -154
  60. package/dist/core/init/scaffold.js +0 -195
  61. package/dist/core/repl/codebase-survey.js +0 -308
  62. package/dist/core/repl/init-interview.js +0 -457
  63. package/dist/core/repl/onboarding-state.js +0 -297
@@ -0,0 +1,267 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * Leak L26 (2026-05-27) — Vim-mode-aware REPL input.
4
+ *
5
+ * Thin wrapper that sits BETWEEN the REPL session and the legacy
6
+ * `InputBox`. When vim mode is off it forwards every prop unchanged
7
+ * so existing operators see zero behavioural delta. When vim mode is
8
+ * on it intercepts keystrokes through `useInput` BEFORE Ink's normal
9
+ * dispatch and either:
10
+ *
11
+ * - in `insert` mode, lets the keystroke fall through to `InputBox`
12
+ * so the legacy buffer / cursor / history / palette code stays
13
+ * authoritative (we do NOT re-implement insert-mode editing);
14
+ * - in `normal` mode, routes the key through `core/vim/keymap.ts`
15
+ * and applies the result to a shadow buffer + cursor mirror that
16
+ * it then pushes back into `InputBox` via the existing `initial`
17
+ * prop on remount.
18
+ *
19
+ * Why a wrapper instead of inlining into `InputBox`?
20
+ *
21
+ * - Keeps the legacy input surface untouched for non-vim operators
22
+ * (the L26 ship risks zero regression to ~year-old code).
23
+ * - The wrapper renders a thin status row ABOVE the input frame so
24
+ * the active mode + pending sequence + cheat sheet are visible.
25
+ * - Tests can exercise the wrapper without dragging in the full
26
+ * ink + clipboard stack of `InputBox` (the spec drives the
27
+ * keymap directly; the wrapper is exercised via the runtime
28
+ * command tests + manual smoke from the REPL).
29
+ *
30
+ * The component intentionally only models the SINGLE-LINE REPL prompt
31
+ * buffer — that is the surface Claude Code's `/vim` covers, and that
32
+ * is what the leak research validated. Multi-line + visual-mode +
33
+ * counts are out of scope for this sprint.
34
+ *
35
+ * ─── Closure-staleness contract (post-L26 fix, 2026-05-27) ───
36
+ *
37
+ * Three-of-three reviewers flagged the original `setShadowLine`
38
+ * functional updater for reading `shadowCursor` and `pending` from
39
+ * the closure, which goes stale across consecutive keystrokes
40
+ * (React batches dispatch, so the second `d` of `dd` saw the
41
+ * `pending` value from the render that scheduled the first `d`,
42
+ * not the post-first-d value). Same problem for cursor advancement
43
+ * across chained `l`/`h`/`w`/`b` presses.
44
+ *
45
+ * Additionally, calling `props.onSubmit` inline inside a setState
46
+ * updater is double-fired by React strict mode (updaters MUST be
47
+ * pure — strict mode runs them twice to surface impurity).
48
+ *
49
+ * The fix moves shadow cursor + pending + mode into `useRef`
50
+ * (refs survive across renders, are read synchronously, and are
51
+ * not subject to closure capture), drives the keymap dispatch
52
+ * OUTSIDE any setState callback, and defers `props.onSubmit` /
53
+ * `props.onExit` invocations until AFTER the render via the
54
+ * `useEffect` queued by toggling a transition ref.
55
+ *
56
+ * The `line` state is the only React state we mutate per keystroke
57
+ * because the inner `InputBox` re-reads it via the `initial` prop
58
+ * on remount — refs alone cannot trigger that remount. Cursor + mode
59
+ * + pending are surfaced to the render path through the same
60
+ * `line`/`tick` rerender, so the status bar stays in sync without
61
+ * itself being captured by a stale closure.
62
+ */
63
+ import { useEffect, useRef, useState } from 'react';
64
+ import { Box, Text, useInput } from 'ink';
65
+ import { handleNormalKey, PENDING_NONE, describePending, } from '../core/vim/keymap.js';
66
+ import { InputBox } from './input-box.js';
67
+ /**
68
+ * Render the mode badge + pending sequence + cheat sheet above the
69
+ * input frame. Two lines:
70
+ *
71
+ * ─ NORMAL ─ d: Esc=normal · i=insert · :w=submit · :q=cancel
72
+ * ─ INSERT ─ (mode-specific tail dropped when there's nothing to show)
73
+ *
74
+ * Plain ASCII + dim accents to match the rest of the REPL's chrome.
75
+ */
76
+ function VimStatusBar(props) {
77
+ const modeLabel = props.mode === 'normal' ? '-- NORMAL --' : '-- INSERT --';
78
+ const pendingLabel = describePending(props.pending);
79
+ const hint = props.mode === 'normal'
80
+ ? 'h/j/k/l move · i/a insert · x del · dd line · :w submit · :q cancel'
81
+ : 'Esc → normal mode';
82
+ return (_jsxs(Box, { children: [_jsx(Text, { color: "#3da9fc", bold: true, children: modeLabel }), pendingLabel.length > 0 ? (_jsxs(_Fragment, { children: [_jsx(Text, { children: " " }), _jsx(Text, { color: "yellow", children: `${pendingLabel}` })] })) : null, _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: hint })] }));
83
+ }
84
+ export function VimInput(props) {
85
+ const { vimEnabled, initialMode, ...inputBoxProps } = props;
86
+ // When vim mode is off we DO NOT mount the `useInput` overlay — the
87
+ // legacy `InputBox` keeps full ownership of the keystrokes and
88
+ // behaves byte-for-byte the way it always did. This is the same
89
+ // pattern `output-style` followed (off → pass through; on → activate
90
+ // the modal surface).
91
+ if (!vimEnabled) {
92
+ return _jsx(InputBox, { ...inputBoxProps });
93
+ }
94
+ // ─── Render-driving state ─────────────────────────────────────
95
+ //
96
+ // `line` + `tick` are the only pieces of state the render path
97
+ // reads — the inner `InputBox` is remounted on every `tick` bump
98
+ // so the `initial` prop is honoured. Cursor / pending / mode are
99
+ // held in refs (see below) and mirrored into render state via the
100
+ // same setLine/setTick burst at the end of each keystroke so the
101
+ // status bar stays in lockstep without ever being captured by a
102
+ // stale closure inside a setState updater.
103
+ const [line, setLine] = useState(props.initial ?? '');
104
+ const [mode, setMode] = useState(initialMode ?? 'normal');
105
+ const [pending, setPending] = useState(PENDING_NONE);
106
+ const [remountTick, setRemountTick] = useState(0);
107
+ // ─── Closure-safe scratch refs ────────────────────────────────
108
+ //
109
+ // These refs are written synchronously inside the `useInput`
110
+ // handler and read on the NEXT keystroke. They survive across
111
+ // renders, are not captured by stale closures, and are NEVER
112
+ // mutated inside a setState updater — so React strict-mode's
113
+ // double-invocation cannot corrupt them.
114
+ //
115
+ // Why not `useReducer`? A reducer would also see fresh state on
116
+ // every dispatch, but the keymap result is a tagged union whose
117
+ // side effects (mode flip, submit, cancel, remount) are easier
118
+ // to read as imperative steps after the pure `handleNormalKey`
119
+ // call. Refs keep the dispatcher linear and the diff vs the
120
+ // pre-fix file minimal.
121
+ const lineRef = useRef(props.initial ?? '');
122
+ const cursorRef = useRef(props.initial?.length ?? 0);
123
+ const pendingRef = useRef(PENDING_NONE);
124
+ const modeRef = useRef(initialMode ?? 'normal');
125
+ // Deferred side-effect queue — submit / exit must NOT run inline
126
+ // inside a setState updater (strict mode double-invokes them).
127
+ // We stash the payload here and flush it from a `useEffect` after
128
+ // the render commits.
129
+ const pendingSubmitRef = useRef(null);
130
+ const pendingExitRef = useRef(false);
131
+ const [sideEffectTick, setSideEffectTick] = useState(0);
132
+ useEffect(() => {
133
+ // Flush any deferred host callback exactly once per scheduled
134
+ // burst. The refs are nulled BEFORE the call so a re-entrant
135
+ // host onSubmit handler that re-renders us cannot re-fire the
136
+ // same payload.
137
+ const submit = pendingSubmitRef.current;
138
+ if (submit !== null) {
139
+ pendingSubmitRef.current = null;
140
+ props.onSubmit(submit);
141
+ }
142
+ if (pendingExitRef.current) {
143
+ pendingExitRef.current = false;
144
+ props.onExit();
145
+ }
146
+ // Intentionally only depends on `sideEffectTick`: we want this
147
+ // effect to fire ONLY when the keystroke handler asked for it
148
+ // (via setSideEffectTick), not on every prop change.
149
+ // eslint-disable-next-line react-hooks/exhaustive-deps
150
+ }, [sideEffectTick]);
151
+ useInput((input, key) => {
152
+ // Esc always returns to normal mode (and clears any pending
153
+ // sequence). This binding wins over `InputBox`'s own Esc handling
154
+ // because we own the `useInput` hook earlier in the render tree.
155
+ if (key.escape) {
156
+ modeRef.current = 'normal';
157
+ pendingRef.current = PENDING_NONE;
158
+ setMode('normal');
159
+ setPending(PENDING_NONE);
160
+ return;
161
+ }
162
+ // In insert mode we relinquish dispatch entirely so the legacy
163
+ // input box owns typing / palette / history / clipboard / kill
164
+ // ring without interference. Returning `undefined` from a
165
+ // `useInput` callback is a no-op — InputBox's own `useInput` will
166
+ // receive the same event on the next frame.
167
+ if (modeRef.current === 'insert')
168
+ return;
169
+ // In normal mode we drive the buffer through the keymap.
170
+ //
171
+ // CRITICAL: all inputs to `handleNormalKey` are read from refs,
172
+ // not from the React state closure captured at render time.
173
+ // This is what makes consecutive keystrokes ("ll", "dd", ":w")
174
+ // observe the post-previous-keystroke state instead of the
175
+ // pre-batch render snapshot.
176
+ const ch = input ?? '';
177
+ const curLine = lineRef.current;
178
+ const curCursor = cursorRef.current;
179
+ const curPending = pendingRef.current;
180
+ const out = handleNormalKey({
181
+ line: curLine,
182
+ cursor: curCursor,
183
+ pending: curPending,
184
+ ch,
185
+ enter: key.return,
186
+ escape: false,
187
+ backspace: key.backspace || key.delete,
188
+ });
189
+ // Step 1: write the new pending state to BOTH the ref (read by
190
+ // the next keystroke synchronously) AND the React state (drives
191
+ // the status bar render).
192
+ pendingRef.current = out.pending;
193
+ setPending(out.pending);
194
+ // Step 2: apply the discriminated result. Each branch updates
195
+ // the refs first (synchronous, closure-safe) and then schedules
196
+ // any required React state update or deferred side effect.
197
+ switch (out.result.kind) {
198
+ case 'move': {
199
+ cursorRef.current = out.result.cursor;
200
+ // Cursor lives in the ref; the inner InputBox does not need
201
+ // a remount for a pure motion, so we skip the tick bump.
202
+ break;
203
+ }
204
+ case 'edit': {
205
+ cursorRef.current = out.result.cursor;
206
+ lineRef.current = out.result.line;
207
+ setLine(out.result.line);
208
+ setRemountTick((t) => t + 1);
209
+ break;
210
+ }
211
+ case 'mode': {
212
+ cursorRef.current = out.result.cursor;
213
+ modeRef.current = out.result.mode;
214
+ setMode(out.result.mode);
215
+ break;
216
+ }
217
+ case 'submit': {
218
+ // Forward to the host's onSubmit and clear the shadow buffer
219
+ // so the next prompt starts empty. We DO NOT re-route
220
+ // through InputBox's Enter handler — that path also writes
221
+ // history; submitting from `:w` should mirror that, so we
222
+ // call props.onSubmit indirectly via the deferred-effect
223
+ // queue. (The host's history append happens inside InputBox;
224
+ // for the modal path the host can observe via props.onSubmit
225
+ // and append manually if needed. The leak-parity surface for
226
+ // L26 documents `:w` as equivalent to Enter so this is fine.)
227
+ const payload = out.result.payload;
228
+ if (payload.trim().length > 0) {
229
+ // Queue the host callback; useEffect flushes it after the
230
+ // render commits. This avoids React strict-mode's
231
+ // double-invocation of setState updaters double-firing
232
+ // onSubmit (which would, e.g., submit the same prompt
233
+ // twice to the agent on every `:w`).
234
+ pendingSubmitRef.current = payload;
235
+ }
236
+ cursorRef.current = 0;
237
+ lineRef.current = '';
238
+ setLine('');
239
+ setRemountTick((t) => t + 1);
240
+ setSideEffectTick((t) => t + 1);
241
+ break;
242
+ }
243
+ case 'cancel': {
244
+ cursorRef.current = 0;
245
+ lineRef.current = '';
246
+ setLine('');
247
+ setRemountTick((t) => t + 1);
248
+ break;
249
+ }
250
+ case 'noop': {
251
+ // No-op result still may have advanced `pending` (e.g. the
252
+ // first `d` of `dd` arms the pending state). The pending ref
253
+ // and state were already updated above, so nothing else to
254
+ // do here.
255
+ break;
256
+ }
257
+ }
258
+ });
259
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(VimStatusBar, { mode: mode, pending: pending }), _jsx(InputBox, { ...inputBoxProps,
260
+ // In normal mode we seed the (possibly mutated) shadow line +
261
+ // suppress the blink so the operator's caret is unambiguously
262
+ // controlled by h/l/0/$/w/b. In insert mode we hand the
263
+ // buffer back to InputBox as-is so the legacy typing path
264
+ // takes over.
265
+ initial: mode === 'normal' ? line : (props.initial ?? line), blinkCursor: mode === 'normal' ? false : (props.blinkCursor ?? true) }, `vim-${remountTick}`)] }));
266
+ }
267
+ //# sourceMappingURL=vim-input.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pugi/cli",
3
- "version": "0.1.0-beta.21",
3
+ "version": "0.1.0-beta.23",
4
4
  "description": "Pugi CLI - terminal-native software execution system",
5
5
  "homepage": "https://pugi.io",
6
6
  "repository": {
@@ -54,7 +54,7 @@
54
54
  "undici": "^8.3.0",
55
55
  "zod": "^3.23.0",
56
56
  "@pugi/personas": "0.1.2",
57
- "@pugi/sdk": "0.1.0-beta.21"
57
+ "@pugi/sdk": "0.1.0-beta.23"
58
58
  },
59
59
  "devDependencies": {
60
60
  "@types/node": "^22.0.0",
@@ -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
@@ -1,195 +0,0 @@
1
- /**
2
- * Workspace scaffold — extracted from `pugi init` so the bare REPL boot
3
- * can call it automatically when the operator launches `pugi` in a
4
- * fresh directory (CEO directive 2026-05-26).
5
- *
6
- * Before this module, `pugi init` was the only path that materialised
7
- * `.pugi/` + the canonical config files. Launching the REPL in an empty
8
- * directory printed `workspace: (not bound - run /init OR cd into
9
- * project)` and instructed the operator to Ctrl+C, run `pugi init`,
10
- * relaunch. That round trip is hostile on a first-touch install — CEO
11
- * escalated "auto = решение" on 2026-05-26.
12
- *
13
- * The module is intentionally side-effect free at import time: the
14
- * scaffold runs only when `ensureWorkspaceInitialized` is called. The
15
- * scaffold is also idempotent — every file write is gated by an
16
- * `existsSync` check, so re-running against a workspace that already has
17
- * `.pugi/settings.json` (e.g. a manual `pugi init` followed by auto-init
18
- * on next REPL launch) is a no-op. The function is safe to call before
19
- * any other init logic.
20
- *
21
- * Two CRITICAL invariants:
22
- *
23
- * 1. **Atomic per-file.** Every write uses `existsSync` + `writeFileSync`
24
- * against the final path. There is no read-modify-write pattern that
25
- * could lose data on a concurrent `pugi init` race. The one path
26
- * that DOES mutate an existing file — `.gitignore` (append `.pugi/`
27
- * marker) — also gates on the marker being absent before appending,
28
- * so the worst-case race is a duplicate marker line that the next
29
- * run skips.
30
- *
31
- * 2. **Silent by default.** When `opts.silent` is true (the REPL
32
- * auto-init path) the scaffold writes NOTHING to stderr/stdout.
33
- * The REPL bootstrap runs before Ink mounts, and a stray
34
- * stdout/stderr write at that point would land on the operator's
35
- * shell ABOVE the alt-screen entry — visible until they scroll up,
36
- * and noisy in a CI tail. The explicit `pugi init` path stays
37
- * verbose via the standalone command in `runtime/cli.ts`.
38
- */
39
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
40
- import { resolve } from 'node:path';
41
- import { emptyIndex } from '../index-store.js';
42
- /**
43
- * Materialise the canonical `.pugi/` workspace scaffold under `cwd`.
44
- * Returns a `{created, dir, createdPaths, skippedPaths}` summary so the
45
- * caller can log a one-shot "initialized" line on the first call without
46
- * re-checking the filesystem.
47
- *
48
- * The scaffold mirrors `pugi init` minus the bundled default-skills
49
- * install (that is a heavier operation gated on the `--no-defaults`
50
- * flag, and the standalone `pugi init` command keeps owning it).
51
- *
52
- * Idempotent: every file write gates on `existsSync`, so re-running
53
- * against an existing workspace is a no-op and returns
54
- * `{created: false}` with every path in `skippedPaths`.
55
- */
56
- export function ensureWorkspaceInitialized(cwd, opts = {}) {
57
- const silent = opts.silent !== false;
58
- const pugiDir = resolve(cwd, '.pugi');
59
- // Local trackers so the existing helpers (mkdirIfMissing /
60
- // writeJsonIfMissing / writeTextIfMissing) keep their (created, skipped)
61
- // signature. The explicit `pugi init` command forwards these straight
62
- // into its JSON payload.
63
- const created = [];
64
- const skipped = [];
65
- mkdirIfMissing(pugiDir, created, skipped);
66
- mkdirIfMissing(resolve(pugiDir, 'artifacts'), created, skipped);
67
- mkdirIfMissing(resolve(pugiDir, 'sessions'), created, skipped);
68
- mkdirIfMissing(resolve(pugiDir, 'skills'), created, skipped);
69
- writeJsonIfMissing(resolve(pugiDir, 'settings.json'), {
70
- schema: 1,
71
- workflow: {
72
- brand: 'pugi',
73
- legacyName: 'codeforge',
74
- approvals: 'auto',
75
- notAutomatic: [],
76
- defaultBaseBranch: 'dev',
77
- branchPrefixes: ['feature', 'fix', 'refactor', 'chore'],
78
- aiCoAuthorTrailers: false,
79
- },
80
- permissions: {
81
- mode: 'auto',
82
- allow: [],
83
- deny: [],
84
- notAutomatic: [],
85
- },
86
- privacy: {
87
- mode: 'balanced',
88
- telemetry: 'off',
89
- },
90
- artifacts: {
91
- defaultPath: '.pugi/artifacts',
92
- promoteExplicitly: true,
93
- },
94
- }, created, skipped);
95
- writeJsonIfMissing(resolve(pugiDir, 'mcp.json'), { schema: 1, servers: [] }, created, skipped);
96
- writeJsonIfMissing(resolve(pugiDir, 'index.json'), emptyIndex(), created, skipped);
97
- writeTextIfMissing(resolve(pugiDir, 'PUGI.md'), [
98
- '# Pugi Project Context',
99
- '',
100
- '## Product Workflow',
101
- '',
102
- '- Public product name: Pugi',
103
- '- Default flow: idea -> build -> review',
104
- '- Approvals are automatic by default until a repo, environment, workflow, or action is marked notAutomatic.',
105
- '- Do not add AI Co-Authored-By trailers.',
106
- '- Generated code, comments, commits, PR text, and technical docs default to English.',
107
- '',
108
- '## Project Notes',
109
- '',
110
- '- Add repo-specific architecture, commands, and business rules here.',
111
- '- Do not store secrets, real IPs, private key paths, tokens, or credentials here.',
112
- '',
113
- ].join('\n'), created, skipped);
114
- writeTextIfMissing(resolve(cwd, '.pugiignore'), [
115
- '# Pugi ignore rules',
116
- '.env',
117
- '.env.*',
118
- '!.env.example',
119
- 'node_modules/',
120
- 'dist/',
121
- '.next/',
122
- 'coverage/',
123
- '*.log',
124
- '*.pem',
125
- '*.key',
126
- '*.crt',
127
- '*.p12',
128
- '*.sql',
129
- '*.dump',
130
- '',
131
- ].join('\n'), created, skipped);
132
- ensurePugiGitIgnore(cwd, created, skipped);
133
- // `silent` is honoured implicitly — this module never writes to
134
- // stdout/stderr. The flag exists so the standalone `pugi init` command
135
- // can layer its own logger on top (it does, in runtime/cli.ts), while
136
- // the auto-init REPL path leaves the boot stream untouched. We
137
- // reference the flag here to defeat the lint "unused" warning and to
138
- // document the contract in the source.
139
- void silent;
140
- return {
141
- created: created.length > 0,
142
- dir: pugiDir,
143
- createdPaths: created,
144
- skippedPaths: skipped,
145
- };
146
- }
147
- /* ------------------------------------------------------------------ */
148
- /* Helpers (mirror the previous in-file implementations in cli.ts) */
149
- /* ------------------------------------------------------------------ */
150
- function mkdirIfMissing(path, created, skipped) {
151
- if (existsSync(path)) {
152
- skipped.push(path);
153
- return;
154
- }
155
- mkdirSync(path, { recursive: true });
156
- created.push(path);
157
- }
158
- function writeJsonIfMissing(path, value, created, skipped) {
159
- writeTextIfMissing(path, `${JSON.stringify(value, null, 2)}\n`, created, skipped);
160
- }
161
- function writeTextIfMissing(path, value, created, skipped) {
162
- if (existsSync(path)) {
163
- skipped.push(path);
164
- return;
165
- }
166
- writeFileSync(path, value, { encoding: 'utf8', mode: 0o600 });
167
- created.push(path);
168
- }
169
- /**
170
- * Ensure the workspace `.gitignore` ignores `.pugi/`. The function is
171
- * additive: it leaves an existing `.gitignore` body intact and appends
172
- * the marker only when none of `.pugi/`, `/.pugi/`, or `.pugi` is
173
- * already present. On a fresh repo with no `.gitignore` it creates the
174
- * file with the single marker line. Mode 0o600 matches the rest of the
175
- * scaffold so a paranoid CI does not surface "world-readable" warnings.
176
- */
177
- function ensurePugiGitIgnore(cwd, created, skipped) {
178
- const gitignorePath = resolve(cwd, '.gitignore');
179
- const marker = '.pugi/';
180
- if (!existsSync(gitignorePath)) {
181
- writeFileSync(gitignorePath, `${marker}\n`, { encoding: 'utf8', mode: 0o600 });
182
- created.push(gitignorePath);
183
- return;
184
- }
185
- const current = readFileSync(gitignorePath, 'utf8');
186
- const lines = current.split('\n').map((line) => line.trim());
187
- if (lines.includes(marker) || lines.includes('/.pugi/') || lines.includes('.pugi')) {
188
- skipped.push(gitignorePath);
189
- return;
190
- }
191
- const next = current.endsWith('\n') ? `${current}${marker}\n` : `${current}\n${marker}\n`;
192
- writeFileSync(gitignorePath, next, { encoding: 'utf8' });
193
- created.push(`${gitignorePath} (+${marker})`);
194
- }
195
- //# sourceMappingURL=scaffold.js.map