peaks-loop 4.0.46 → 4.0.47

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 (130) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/README-en.md +1 -1
  3. package/README.md +1 -1
  4. package/dist/cli/commands/code-runtime-commands.d.ts +22 -0
  5. package/dist/cli/commands/code-runtime-commands.js +99 -16
  6. package/dist/cli/commands/compact-command.js +129 -1
  7. package/dist/cli/commands/container-commands.js +3 -3
  8. package/dist/cli/commands/core/skill-command.js +45 -10
  9. package/dist/cli/commands/cron-commands.js +2 -1
  10. package/dist/cli/commands/e2e-verify.js +3 -3
  11. package/dist/cli/commands/governance-classify-contract-commands.js +1 -0
  12. package/dist/cli/commands/hooks-commands.js +10 -1
  13. package/dist/cli/commands/loop-commands.js +1 -0
  14. package/dist/cli/commands/playwright-commands.js +2 -1
  15. package/dist/cli/commands/reinject-command.d.ts +72 -0
  16. package/dist/cli/commands/reinject-command.js +174 -0
  17. package/dist/cli/commands/request-commands.js +6 -3
  18. package/dist/cli/commands/shadcn-commands.js +1 -0
  19. package/dist/cli/commands/test-commands.js +2 -1
  20. package/dist/cli/commands/vm-commands.js +7 -7
  21. package/dist/cli/commands/workspace/init-command.js +24 -2
  22. package/dist/cli/commands/worktree-lease-commands.js +4 -4
  23. package/dist/cli/index.js +49 -2
  24. package/dist/cli/program.js +5 -0
  25. package/dist/hooks/pre-tool-use-sub-agent.js +1 -1
  26. package/dist/services/adapter/adapter-registry.js +1 -1
  27. package/dist/services/artifacts/artifact-service.js +1 -1
  28. package/dist/services/capability-guard-runner/contracts/J01.js +2 -1
  29. package/dist/services/capability-guard-runner/contracts/J02.js +3 -3
  30. package/dist/services/capability-guard-runner/contracts/J04.js +4 -2
  31. package/dist/services/capability-guard-runner/contracts/J07.js +2 -1
  32. package/dist/services/code/auto-compact-lifecycle.d.ts +11 -1
  33. package/dist/services/code/auto-compact-lifecycle.js +11 -4
  34. package/dist/services/code/auto-compact-orchestrator.d.ts +53 -9
  35. package/dist/services/code/auto-compact-orchestrator.js +153 -32
  36. package/dist/services/code/orchestrator-can-do.d.ts +4 -2
  37. package/dist/services/code/orchestrator-can-do.js +37 -5
  38. package/dist/services/codegraph/codegraph-exclude-reconciler.js +2 -1
  39. package/dist/services/codegraph/codegraph-process-runner.js +3 -2
  40. package/dist/services/compact/request-transition-hook.js +5 -2
  41. package/dist/services/compact-history/compact-history-service.d.ts +61 -0
  42. package/dist/services/compact-history/compact-history-service.js +49 -0
  43. package/dist/services/config/config-safety.d.ts +52 -0
  44. package/dist/services/config/config-safety.js +75 -1
  45. package/dist/services/context/auto-compact-dispatcher.d.ts +7 -37
  46. package/dist/services/context/auto-compact-dispatcher.js +113 -40
  47. package/dist/services/context/auto-compact-reader.d.ts +68 -28
  48. package/dist/services/context/auto-compact-reader.js +155 -1
  49. package/dist/services/context/auto-compact-types.d.ts +89 -12
  50. package/dist/services/context/auto-compact-types.js +16 -32
  51. package/dist/services/context/harness-window-config.d.ts +412 -0
  52. package/dist/services/context/harness-window-config.js +607 -0
  53. package/dist/services/context/main-session-monitor.d.ts +27 -0
  54. package/dist/services/context/main-session-monitor.js +32 -1
  55. package/dist/services/context/post-compact-reinjection.d.ts +221 -0
  56. package/dist/services/context/post-compact-reinjection.js +491 -0
  57. package/dist/services/dispatch/merge-back-runner.js +5 -5
  58. package/dist/services/dispatch/service-shutdown.js +3 -3
  59. package/dist/services/doc/doc-generator.js +2 -1
  60. package/dist/services/env/shell-probe.js +1 -1
  61. package/dist/services/fuzzy-matching/fzf-pick-service.js +2 -0
  62. package/dist/services/hooks/auto-compact-hook-install.d.ts +10 -2
  63. package/dist/services/hooks/auto-compact-hook-install.js +8 -0
  64. package/dist/services/ide/adapters/claude-code-adapter.d.ts +107 -3
  65. package/dist/services/ide/adapters/claude-code-adapter.js +154 -7
  66. package/dist/services/ide/ide-registry.d.ts +12 -0
  67. package/dist/services/ide/ide-registry.js +14 -0
  68. package/dist/services/ide/ide-types.d.ts +59 -0
  69. package/dist/services/lint/detect-eslint.js +2 -2
  70. package/dist/services/lint/eslint-runner.js +3 -1
  71. package/dist/services/loop/evaluator-dispatcher.js +2 -1
  72. package/dist/services/memory/project-memory-service/index/kind-dispatch.js +1 -1
  73. package/dist/services/memory/project-memory-service/store/paths.d.ts +9 -1
  74. package/dist/services/memory/project-memory-service/store/paths.js +15 -6
  75. package/dist/services/prd/best-practice-auto-trigger.js +1 -0
  76. package/dist/services/release/version-precheck-service.d.ts +2 -1
  77. package/dist/services/release/version-precheck-service.js +82 -12
  78. package/dist/services/runtime/vendor-adapter.d.ts +29 -4
  79. package/dist/services/runtime/vendors/claude-code.js +1 -1
  80. package/dist/services/runtime/vendors/codex.js +1 -1
  81. package/dist/services/runtime/vendors/copilot.js +1 -1
  82. package/dist/services/sc/sc-service.js +1 -1
  83. package/dist/services/scan/diff-scope-service.js +2 -2
  84. package/dist/services/scan/file-size-scan.js +2 -2
  85. package/dist/services/scan/orphan-service.js +2 -1
  86. package/dist/services/scan/type-sanity-service.js +2 -2
  87. package/dist/services/skillhub/tar-runtime.js +1 -0
  88. package/dist/services/skills/hooks-codegate-superpowers.d.ts +8 -0
  89. package/dist/services/skills/hooks-codegate-superpowers.js +40 -3
  90. package/dist/services/skills/hooks-settings-service.d.ts +12 -0
  91. package/dist/services/skills/hooks-settings-service.js +77 -10
  92. package/dist/services/skills/session-start-hook-constants.d.ts +41 -0
  93. package/dist/services/skills/session-start-hook-constants.js +41 -0
  94. package/dist/services/skills/skill-presence-service.js +9 -0
  95. package/dist/services/slice/slice-check-service.js +2 -1
  96. package/dist/services/slice/slice-decompose-runners.js +2 -1
  97. package/dist/services/upgrade/upgrade-service.js +1 -0
  98. package/dist/services/workflow/workflow-skip-service.js +2 -1
  99. package/dist/services/workspace/migrate-service.js +1 -1
  100. package/dist/services/workspace/workspace-claude-settings-materializer.js +51 -7
  101. package/dist/services/workspace/workspace-service.js +8 -0
  102. package/dist/services/worktree/host-worktree-reconciler.js +1 -0
  103. package/dist/services/worktree/long-path-cleanup.js +3 -2
  104. package/dist/shared/process.js +1 -1
  105. package/package.json +5 -5
  106. package/scripts/install-skills.mjs +1 -0
  107. package/scripts/watch.mjs +3 -1
  108. package/skills/bee/peaks-perf-audit/SKILL.md +1 -1
  109. package/skills/bee/peaks-prd/SKILL.md +1 -1
  110. package/skills/bee/peaks-qa/SKILL.md +2 -2
  111. package/skills/bee/peaks-rd/SKILL.md +2 -2
  112. package/skills/bee/peaks-reviewer/SKILL.md +1 -1
  113. package/skills/bee/peaks-sc/SKILL.md +1 -1
  114. package/skills/bee/peaks-security-audit/SKILL.md +1 -1
  115. package/skills/bee/peaks-txt/SKILL.md +1 -1
  116. package/skills/bee/peaks-ui/SKILL.md +1 -1
  117. package/skills/peaks-audit/SKILL.md +1 -1
  118. package/skills/peaks-code/SKILL.md +2 -2
  119. package/skills/peaks-code/references/sub-agent-dispatch.md +1 -1
  120. package/skills/peaks-content/SKILL.md +1 -1
  121. package/skills/peaks-doctor/SKILL.md +1 -1
  122. package/skills/peaks-final-review/SKILL.md +1 -1
  123. package/skills/peaks-ide/SKILL.md +1 -1
  124. package/skills/peaks-issue-fix-orchestrator/SKILL.md +1 -1
  125. package/skills/peaks-resume/SKILL.md +1 -1
  126. package/skills/peaks-slice-decompose/SKILL.md +1 -1
  127. package/skills/peaks-solo/SKILL.md +1 -1
  128. package/skills/peaks-sop/SKILL.md +1 -1
  129. package/skills/peaks-status/SKILL.md +1 -1
  130. package/skills/peaks-test/SKILL.md +1 -1
@@ -0,0 +1,72 @@
1
+ /**
2
+ * rid `2026-09-13-a2-post-compact-reinject` —
3
+ * `peaks session reinject --project <path>`.
4
+ *
5
+ * The transport for `buildPostCompactReinjectionCard`. A Claude Code
6
+ * `SessionStart` hook (installed by `peaks hooks install`, matcher `compact`)
7
+ * runs this and Claude Code adds its stdout to the context — which is the
8
+ * whole mechanism by which the engineering state comes back after a
9
+ * compaction. See `src/services/context/post-compact-reinjection.ts` for what
10
+ * the card says and why it has a byte budget.
11
+ *
12
+ * THE ONE THING THIS FILE HAS TO GET RIGHT
13
+ *
14
+ * stdout IS context here. Every other peaks command treats stdout as a
15
+ * channel for a human or a JSON consumer; this one's stdout is pasted in
16
+ * front of the model as though it were something peaks-loop meant to say.
17
+ * Two consequences, and both are the reason the code below looks the way it
18
+ * does:
19
+ *
20
+ * 1. The card is printed VERBATIM — no `printResult`, no envelope, no
21
+ * `next:` lines, no JSON on the card path. Anything else would be
22
+ * injected too, and a `{"ok":true,...}` blob in the context is noise the
23
+ * model then has to interpret.
24
+ * 2. A FAILURE PRINTS NOTHING. Not an error, not a warning, not a hint. An
25
+ * error message on stdout would be injected into the context as a fact,
26
+ * and a model that reads `REINJECT_FAILED: project root must be
27
+ * canonical` in its own context has been handed a false problem to solve
28
+ * mid-slice. So every failure path exits 0 with empty stdout, and the
29
+ * reason is reported only through `--json` (which the hook never passes).
30
+ * This is `failure-soft` for a SessionStart hook: the session must start
31
+ * whether or not there is any state to re-inject.
32
+ *
33
+ * `--json` exists for the human/diagnostic path — `peaks session reinject
34
+ * --json` is how you inspect what the hook would inject without reading a
35
+ * transcript. It is deliberately NOT what the hook runs.
36
+ */
37
+ import type { Command } from 'commander';
38
+ import type { ProgramIO } from '../cli-helpers.js';
39
+ import { buildPostCompactReinjectionCard } from '../../services/context/post-compact-reinjection.js';
40
+ export type ReinjectOptions = {
41
+ project: string;
42
+ json?: boolean;
43
+ budgetBytes?: string;
44
+ };
45
+ /** Why no card was produced. Reported through `--json` only. */
46
+ export type ReinjectSkipReason = 'empty-project' | 'invalid-project-root' | 'unreadable-project-root';
47
+ export type ReinjectResult = {
48
+ readonly ok: true;
49
+ readonly card: ReturnType<typeof buildPostCompactReinjectionCard>;
50
+ } | {
51
+ readonly ok: false;
52
+ readonly reason: ReinjectSkipReason;
53
+ readonly message: string;
54
+ };
55
+ /**
56
+ * Pure-ish action body, exported so tests can drive it with inputs Commander
57
+ * would mangle (trailing NUL bytes, whitespace-only paths). Never throws and
58
+ * never writes to stdout itself — `runReinjectAction` is the IO edge.
59
+ */
60
+ export declare function buildReinjectResult(opts: ReinjectOptions): ReinjectResult;
61
+ /**
62
+ * The IO edge. Returns the process exit code so callers (CLI path or test
63
+ * path) apply it uniformly. NEVER calls `process.exit`.
64
+ *
65
+ * Both branches return 0 on the failure path — see this file's header. The
66
+ * only way a caller sees `exitCode: 1` is a bug in the `--json` serializer
67
+ * itself, and even that is caught.
68
+ */
69
+ export declare function runReinjectAction(opts: ReinjectOptions, io: ProgramIO): {
70
+ exitCode: 0 | 1;
71
+ };
72
+ export declare function registerReinjectCommand(program: Command, io: ProgramIO): void;
@@ -0,0 +1,174 @@
1
+ /**
2
+ * rid `2026-09-13-a2-post-compact-reinject` —
3
+ * `peaks session reinject --project <path>`.
4
+ *
5
+ * The transport for `buildPostCompactReinjectionCard`. A Claude Code
6
+ * `SessionStart` hook (installed by `peaks hooks install`, matcher `compact`)
7
+ * runs this and Claude Code adds its stdout to the context — which is the
8
+ * whole mechanism by which the engineering state comes back after a
9
+ * compaction. See `src/services/context/post-compact-reinjection.ts` for what
10
+ * the card says and why it has a byte budget.
11
+ *
12
+ * THE ONE THING THIS FILE HAS TO GET RIGHT
13
+ *
14
+ * stdout IS context here. Every other peaks command treats stdout as a
15
+ * channel for a human or a JSON consumer; this one's stdout is pasted in
16
+ * front of the model as though it were something peaks-loop meant to say.
17
+ * Two consequences, and both are the reason the code below looks the way it
18
+ * does:
19
+ *
20
+ * 1. The card is printed VERBATIM — no `printResult`, no envelope, no
21
+ * `next:` lines, no JSON on the card path. Anything else would be
22
+ * injected too, and a `{"ok":true,...}` blob in the context is noise the
23
+ * model then has to interpret.
24
+ * 2. A FAILURE PRINTS NOTHING. Not an error, not a warning, not a hint. An
25
+ * error message on stdout would be injected into the context as a fact,
26
+ * and a model that reads `REINJECT_FAILED: project root must be
27
+ * canonical` in its own context has been handed a false problem to solve
28
+ * mid-slice. So every failure path exits 0 with empty stdout, and the
29
+ * reason is reported only through `--json` (which the hook never passes).
30
+ * This is `failure-soft` for a SessionStart hook: the session must start
31
+ * whether or not there is any state to re-inject.
32
+ *
33
+ * `--json` exists for the human/diagnostic path — `peaks session reinject
34
+ * --json` is how you inspect what the hook would inject without reading a
35
+ * transcript. It is deliberately NOT what the hook runs.
36
+ */
37
+ import { printResult } from '../cli-helpers.js';
38
+ import { fail, ok } from 'peaks-loop-shared/result';
39
+ import { resolveCanonicalProjectRootStrict, InvalidProjectRootError } from '../../services/config/config-safety.js';
40
+ import { POST_COMPACT_REINJECTION_BYTE_BUDGET, buildPostCompactReinjectionCard } from '../../services/context/post-compact-reinjection.js';
41
+ /**
42
+ * Parse `--budget-bytes`. Diagnostic only — the production ceiling is the
43
+ * constant. A value that is not a non-negative integer is REFUSED rather than
44
+ * coerced, because a coerced budget would silently change what the card
45
+ * proves (`--budget-bytes 4KiB` becoming `NaN` and then the default would
46
+ * report a full card while the caller believed they had constrained it).
47
+ */
48
+ function parseBudget(raw) {
49
+ if (raw === undefined)
50
+ return { ok: true, value: null };
51
+ const parsed = Number(raw);
52
+ if (!Number.isInteger(parsed) || parsed < 0)
53
+ return { ok: false };
54
+ return { ok: true, value: parsed };
55
+ }
56
+ /**
57
+ * Pure-ish action body, exported so tests can drive it with inputs Commander
58
+ * would mangle (trailing NUL bytes, whitespace-only paths). Never throws and
59
+ * never writes to stdout itself — `runReinjectAction` is the IO edge.
60
+ */
61
+ export function buildReinjectResult(opts) {
62
+ if (!opts.project || opts.project.trim() === '') {
63
+ return {
64
+ ok: false,
65
+ reason: 'empty-project',
66
+ message: '--project <path> is required and must be non-empty'
67
+ };
68
+ }
69
+ let projectRoot;
70
+ try {
71
+ projectRoot = resolveCanonicalProjectRootStrict(opts.project);
72
+ }
73
+ catch (error) {
74
+ if (error instanceof InvalidProjectRootError) {
75
+ return {
76
+ ok: false,
77
+ reason: 'invalid-project-root',
78
+ message: `--project is not a usable project root (${error.reason})`
79
+ };
80
+ }
81
+ return {
82
+ ok: false,
83
+ reason: 'unreadable-project-root',
84
+ message: error instanceof Error ? error.message : String(error)
85
+ };
86
+ }
87
+ const budget = parseBudget(opts.budgetBytes);
88
+ if (!budget.ok) {
89
+ return {
90
+ ok: false,
91
+ reason: 'invalid-project-root',
92
+ message: `--budget-bytes must be a non-negative integer, got ${String(opts.budgetBytes)}`
93
+ };
94
+ }
95
+ const card = buildPostCompactReinjectionCard({
96
+ projectRoot,
97
+ ...(budget.value !== null ? { budgetBytes: budget.value } : {})
98
+ });
99
+ return { ok: true, card };
100
+ }
101
+ /**
102
+ * The IO edge. Returns the process exit code so callers (CLI path or test
103
+ * path) apply it uniformly. NEVER calls `process.exit`.
104
+ *
105
+ * Both branches return 0 on the failure path — see this file's header. The
106
+ * only way a caller sees `exitCode: 1` is a bug in the `--json` serializer
107
+ * itself, and even that is caught.
108
+ */
109
+ export function runReinjectAction(opts, io) {
110
+ let result;
111
+ try {
112
+ result = buildReinjectResult(opts);
113
+ }
114
+ catch (error) {
115
+ result = {
116
+ ok: false,
117
+ reason: 'unreadable-project-root',
118
+ message: error instanceof Error ? error.message : String(error)
119
+ };
120
+ }
121
+ if (opts.json === true) {
122
+ if (!result.ok) {
123
+ printResult(io, fail('session.reinject', `REINJECT_SKIPPED_${result.reason.replace(/-/g, '_').toUpperCase()}`, result.message, {
124
+ project: opts.project
125
+ }, []), true);
126
+ // A skipped card is a legitimate answer, not a failed command: the hook
127
+ // that runs this must never report a non-zero exit to the harness.
128
+ return { exitCode: 0 };
129
+ }
130
+ const card = result.card;
131
+ printResult(io, ok('session.reinject', {
132
+ sessionId: card.sessionId,
133
+ budgetBytes: card.budgetBytes,
134
+ bytes: card.bytes,
135
+ emittedRanks: card.emittedRanks,
136
+ droppedRanks: card.droppedRanks,
137
+ unresolved: card.unresolved,
138
+ // The payload itself, so a caller can read it without re-running.
139
+ card: card.text
140
+ }), true);
141
+ return { exitCode: 0 };
142
+ }
143
+ // The card path — the one the hook uses. Verbatim, and nothing on failure.
144
+ if (!result.ok)
145
+ return { exitCode: 0 };
146
+ if (result.card.text.length > 0)
147
+ io.stdout(result.card.text);
148
+ return { exitCode: 0 };
149
+ }
150
+ export function registerReinjectCommand(program, io) {
151
+ // Reuse the `session` commander group registered by the auto-registration
152
+ // pass, exactly as `registerPrimerCommand` does — `program.command('session
153
+ // reinject')` would register a single literal command name instead of a
154
+ // child of the group, and creating a second `session` group throws.
155
+ const session = program.commands.find((c) => c.name() === 'session');
156
+ if (!session) {
157
+ throw new Error('registerReinjectCommand: session group not registered; ' +
158
+ 'autoRegisterAllCommands must run before registerReinjectCommand');
159
+ }
160
+ session
161
+ .command('reinject')
162
+ .description('Print the post-compact engineering-state card to stdout. ' +
163
+ 'Run by the SessionStart hook (matcher: compact) so Claude Code re-injects it ' +
164
+ 'as context after a compaction. Read-only; prints nothing (exit 0) when there is ' +
165
+ 'no state to re-inject.')
166
+ .requiredOption('--project <path>', 'Project root (must be a non-empty canonical path)')
167
+ .option('--budget-bytes <n>', `Diagnostic override for the card's byte ceiling (default ${POST_COMPACT_REINJECTION_BYTE_BUDGET})`)
168
+ .option('--json', 'emit a JSON envelope { ok, data } to stdout instead of the card')
169
+ .action((opts) => {
170
+ const { exitCode } = runReinjectAction(opts, io);
171
+ if (exitCode !== 0)
172
+ process.exitCode = exitCode;
173
+ });
174
+ }
@@ -277,9 +277,12 @@ export function registerRequestCommands(program, io) {
277
277
  // 0.85–0.95 pre-compact zone, write a checkpoint BEFORE the
278
278
  // transition completes and attach `preCompactCheckpoint: true` to
279
279
  // the response envelope. The LLM remains the decision-maker; this
280
- // hook only surfaces the signal. The 0.95 red-line behaviour is
281
- // unchanged — the existing auto-compact orchestrator continues to
282
- // refuse dispatch at ratio ≥ 0.95.
280
+ // hook only surfaces the signal. At ratio ≥ 0.95 the auto-compact
281
+ // orchestrator ASKS the harness to compact and reports that it is
282
+ // waiting; it does not refuse anything, because peaks-loop has no
283
+ // executor for a running session and a refusal it cannot enforce only
284
+ // deadlocked the runner (slice
285
+ // 2026-09-13-auto-compact-trigger-ownership).
283
286
  let preCompact = null;
284
287
  // 2026-09-03-codegraph-autorefresh: auto codegraph re-index on the
285
288
  // RD → QA slice-complete boundary. Set only for rd:qa-handoff; null
@@ -46,6 +46,7 @@ export function runShadcnInit(options) {
46
46
  const result = spawnSync(invocation.command, [...invocation.args], {
47
47
  cwd: options.cwd,
48
48
  encoding: 'utf8',
49
+ windowsHide: true,
49
50
  timeout: options.timeoutMs ?? 300_000,
50
51
  maxBuffer: 32 * 1024 * 1024,
51
52
  env: invocation.baseEnv
@@ -203,7 +203,8 @@ export function runRunner(framework, argv, projectRoot, deps = {}) {
203
203
  const proc = spawnFn(resolution.command, resolution.args, {
204
204
  cwd: projectRoot,
205
205
  env: process.env,
206
- stdio: ['ignore', 'pipe', 'pipe']
206
+ stdio: ['ignore', 'pipe', 'pipe'],
207
+ windowsHide: true
207
208
  });
208
209
  let stdout = '';
209
210
  let stderr = '';
@@ -59,7 +59,7 @@ function detectHypervisor(requested) {
59
59
  break;
60
60
  }
61
61
  try {
62
- const v = execSync(`${binary} --version`, { stdio: ['ignore', 'pipe', 'pipe'], encoding: 'utf8' });
62
+ const v = execSync(`${binary} --version`, { stdio: ['ignore', 'pipe', 'pipe'], encoding: 'utf8', windowsHide: true });
63
63
  if (requested === 'kvm' && !existsSync('/dev/kvm')) {
64
64
  return { ok: false, stderr: 'KVM kernel module not loaded (/dev/kvm absent)' };
65
65
  }
@@ -87,27 +87,27 @@ function spawnVmWithHypervisor(args) {
87
87
  </domain>`;
88
88
  const xmlPath = `${args.workdir}/.peaks-vm-${args.leaseId}.xml`;
89
89
  require('node:fs').writeFileSync(xmlPath, xml, 'utf8');
90
- const out = execSync(`virsh create ${xmlPath}`, { cwd: args.workdir, stdio: ['ignore', 'pipe', 'pipe'], encoding: 'utf8' });
90
+ const out = execSync(`virsh create ${xmlPath}`, { cwd: args.workdir, stdio: ['ignore', 'pipe', 'pipe'], encoding: 'utf8', windowsHide: true });
91
91
  return { vmId: out.trim() };
92
92
  }
93
93
  if (args.hypervisor === 'hyperkit') {
94
- const out = execSync(`hvftool create --image ${args.image} --mount ${args.mount}:/work --label peaks.leaseId=${args.leaseId} --label peaks.rid=${args.rid} --entrypoint sleep -- infinity`, { cwd: args.workdir, stdio: ['ignore', 'pipe', 'pipe'], encoding: 'utf8' });
94
+ const out = execSync(`hvftool create --image ${args.image} --mount ${args.mount}:/work --label peaks.leaseId=${args.leaseId} --label peaks.rid=${args.rid} --entrypoint sleep -- infinity`, { cwd: args.workdir, stdio: ['ignore', 'pipe', 'pipe'], encoding: 'utf8', windowsHide: true });
95
95
  return { vmId: out.trim() };
96
96
  }
97
97
  // hyperv: the hvc shim writes a vhdx + emits the new VM id.
98
- const out = execSync(`hvc create --image ${args.image} --mount ${args.mount} --label peaks.leaseId=${args.leaseId} --label peaks.rid=${args.rid}`, { cwd: args.workdir, stdio: ['ignore', 'pipe', 'pipe'], encoding: 'utf8' });
98
+ const out = execSync(`hvc create --image ${args.image} --mount ${args.mount} --label peaks.leaseId=${args.leaseId} --label peaks.rid=${args.rid}`, { cwd: args.workdir, stdio: ['ignore', 'pipe', 'pipe'], encoding: 'utf8', windowsHide: true });
99
99
  return { vmId: out.trim() };
100
100
  }
101
101
  function destroyVmWithHypervisor(args) {
102
102
  try {
103
103
  if (args.hypervisor === 'kvm') {
104
- execSync(`virsh destroy ${args.vmId}`, { stdio: ['ignore', 'pipe', 'pipe'] });
104
+ execSync(`virsh destroy ${args.vmId}`, { stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true });
105
105
  }
106
106
  else if (args.hypervisor === 'hyperkit') {
107
- execSync(`hvftool stop ${args.vmId}`, { stdio: ['ignore', 'pipe', 'pipe'] });
107
+ execSync(`hvftool stop ${args.vmId}`, { stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true });
108
108
  }
109
109
  else {
110
- execSync(`hvc stop ${args.vmId}`, { stdio: ['ignore', 'pipe', 'pipe'] });
110
+ execSync(`hvc stop ${args.vmId}`, { stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true });
111
111
  }
112
112
  return true;
113
113
  }
@@ -22,7 +22,7 @@ import { initWorkspace, InvalidSessionIdError, ConflictingSessionError, LegacyCh
22
22
  // session-id-shaped directories at the `.peaks/_runtime/` sibling
23
23
  // level.
24
24
  import { ensureSessionWithRotation } from '../../../services/session/session-manager.js';
25
- import { resolveCanonicalProjectRoot } from '../../../services/config/config-service.js';
25
+ import { resolveWritableProjectRoot, UnsafeProjectRootError } from '../../../services/config/config-safety.js';
26
26
  import { applyHookInstall, readHookStatus } from '../../../services/skills/hooks-settings-service.js';
27
27
  import { clearStalePresenceOnRotation } from '../../../services/skills/skill-presence-service.js';
28
28
  import { gcStalePresenceLeases } from '../../../services/skills/presence-lease-service.js';
@@ -69,7 +69,12 @@ export function registerWorkspaceInitCommand(workspace, io) {
69
69
  // (the same regression that produced prompt-project/.peaks/ in
70
70
  // the 5/27-5/29 sessions). When startPath is not inside any
71
71
  // git repo, the helper falls through to the cwd verbatim.
72
- const projectRoot = resolveCanonicalProjectRoot(options.project);
72
+ // `resolveWritableProjectRoot` = `resolveCanonicalProjectRoot` plus the
73
+ // home-directory refusal. It has to be this one, and it has to run here:
74
+ // `ensureSessionWithRotation` below WRITES (it mints/binds a session), so
75
+ // a guard placed after it would leave a `.peaks/_runtime/` tree in the
76
+ // user's home even though the init was refused.
77
+ const projectRoot = resolveWritableProjectRoot(options.project);
73
78
  // Slice 018: outer-session-mismatch auto-rotation. When the
74
79
  // user did NOT pass --session-id explicitly, run
75
80
  // `ensureSessionWithRotation` so the binding is rotated on
@@ -408,6 +413,23 @@ export function registerWorkspaceInitCommand(workspace, io) {
408
413
  }, warningsForEnvelope, nextActions), options.json);
409
414
  }
410
415
  catch (error) {
416
+ if (error instanceof UnsafeProjectRootError) {
417
+ // A typed refusal, not a crash: "you are standing in your home
418
+ // directory, which is not a project" is a thing the caller can fix in
419
+ // one step, and the envelope has to say which directory was refused and
420
+ // what to pass instead. `WORKSPACE_INIT_FAILED` below would have buried
421
+ // it as an unexplained failure.
422
+ printResult(io, fail('workspace.init', 'UNSAFE_PROJECT_ROOT', error.message, {
423
+ resolvedProjectRoot: error.projectRoot,
424
+ projectOption: options.project ?? null
425
+ }, [
426
+ 'Pass --project <path-to-your-project> instead of a path that resolves to your home directory.',
427
+ 'This command creates .peaks/, .gitignore, .claude/settings.local.json and a codegraph index; none of those belong in $HOME.',
428
+ 'If you meant to initialize the current directory, cd into a project directory first — peaks will not write into the home directory itself.'
429
+ ]), options.json);
430
+ process.exitCode = 1;
431
+ return;
432
+ }
411
433
  if (error instanceof InvalidSessionIdError) {
412
434
  printResult(io, fail('workspace.init', error.code, error.message, { sessionId: options.sessionId }, ['Use a date-prefixed kebab slug like 2026-05-25-add-user-auth']), options.json);
413
435
  process.exitCode = 1;
@@ -91,7 +91,7 @@ export function registerWorktreeLeaseCommands(auth, io) {
91
91
  // and authorize this very `git worktree add`; for Part 1 we still
92
92
  // require a current `peaks worktree auth grant` token to remain
93
93
  // consistent with the slice-027 hard gate contract.
94
- execSync(`git worktree add "${wtPath}" -b "${branch}"`, { cwd: projectRoot, stdio: 'pipe', encoding: 'utf8' });
94
+ execSync(`git worktree add "${wtPath}" -b "${branch}"`, { cwd: projectRoot, stdio: 'pipe', encoding: 'utf8', windowsHide: true });
95
95
  atomicWriteJson(leaseFilePath(joinPathSession(projectRoot, sessionId), leaseId), lease);
96
96
  // Part 4.A: emit spawn metric (fire-and-forget; never blocks).
97
97
  emitLeaseEvent({
@@ -167,7 +167,7 @@ export function registerWorktreeLeaseCommands(auth, io) {
167
167
  // released.
168
168
  let gitWorktreeRemoveFailed = false;
169
169
  try {
170
- execSync(`git worktree remove --force "${lease.path}"`, { cwd: projectRoot, stdio: 'pipe', encoding: 'utf8' });
170
+ execSync(`git worktree remove --force "${lease.path}"`, { cwd: projectRoot, stdio: 'pipe', encoding: 'utf8', windowsHide: true });
171
171
  }
172
172
  catch {
173
173
  gitWorktreeRemoveFailed = true;
@@ -379,14 +379,14 @@ export function registerWorktreeLeaseCommands(auth, io) {
379
379
  // already gone we still mark the lease gc.
380
380
  let gitWorktreeRemoveFailed = false;
381
381
  try {
382
- execSync(`git worktree remove --force "${updated.path}"`, { cwd: projectRoot, stdio: 'pipe', encoding: 'utf8' });
382
+ execSync(`git worktree remove --force "${updated.path}"`, { cwd: projectRoot, stdio: 'pipe', encoding: 'utf8', windowsHide: true });
383
383
  }
384
384
  catch {
385
385
  gitWorktreeRemoveFailed = true;
386
386
  }
387
387
  // `git worktree prune` clears any stale admin entries. Best-effort.
388
388
  try {
389
- execSync('git worktree prune', { cwd: projectRoot, stdio: 'pipe', encoding: 'utf8' });
389
+ execSync('git worktree prune', { cwd: projectRoot, stdio: 'pipe', encoding: 'utf8', windowsHide: true });
390
390
  }
391
391
  catch {
392
392
  // ignore — prune is idempotent
package/dist/cli/index.js CHANGED
@@ -29,8 +29,34 @@ const defaultIo = {
29
29
  const argv = process.argv.slice(2);
30
30
  const hasHelp = argv.some((arg) => arg === '--help' || arg === '-h');
31
31
  const firstPositional = argv.find((arg) => !arg.startsWith('-'));
32
+ const program = createProgram();
33
+ /**
34
+ * The subcommand path the caller actually invoked, in the same tokens they
35
+ * typed (`release canary`). Derived from the registered command tree by
36
+ * consuming leading non-`-` argv tokens, so it never disagrees with what
37
+ * Commander dispatched on.
38
+ *
39
+ * Commander's `CommanderError` carries a code and a message but no command
40
+ * reference — `missingMandatoryOptionValue` is raised by the leaf command and
41
+ * throws out of `parseAsync` with nothing naming it. Without this walk the
42
+ * missing-option envelope could only say `command: "cli"`, which is the field
43
+ * the caller needs to act on.
44
+ */
45
+ function resolveInvokedCommandPath() {
46
+ let cmd = program;
47
+ const parts = [];
48
+ for (const token of argv) {
49
+ if (token.startsWith('-'))
50
+ break;
51
+ const next = cmd.commands.find((c) => c.name() === token || c.aliases().includes(token));
52
+ if (next === undefined)
53
+ break;
54
+ parts.push(next.name());
55
+ cmd = next;
56
+ }
57
+ return parts.length > 0 ? parts.join(' ') : program.name();
58
+ }
32
59
  if (hasHelp && firstPositional !== undefined) {
33
- const program = createProgram();
34
60
  const registered = new Set(program.commands.map((c) => c.name()));
35
61
  if (!registered.has(firstPositional)) {
36
62
  // Defer the check by 0ms so Commander's own help handler runs first
@@ -41,7 +67,7 @@ if (hasHelp && firstPositional !== undefined) {
41
67
  });
42
68
  }
43
69
  }
44
- createProgram().parseAsync(process.argv).catch((error) => {
70
+ program.parseAsync(process.argv).catch((error) => {
45
71
  if (error instanceof CommanderError && error.code === 'commander.version') {
46
72
  return;
47
73
  }
@@ -59,6 +85,27 @@ createProgram().parseAsync(process.argv).catch((error) => {
59
85
  if (error.code === 'commander.help' || error.code === 'commander.helpDisplayed') {
60
86
  return;
61
87
  }
88
+ if (error.code === 'commander.missingMandatoryOptionValue') {
89
+ // A `.requiredOption()` the caller did not supply. Commander raises this
90
+ // as a plain `Error`-shaped `CommanderError`, so the `.catch()` below used
91
+ // to file it under `UNHANDLED_ERROR` — "you left out an argument" reported
92
+ // as a crash, with empty `nextActions` and no way to see WHICH option or
93
+ // what values it takes. The flags string Commander formats carries both
94
+ // (`--percent <10|50>`), so it is worth extracting rather than
95
+ // paraphrasing: it is the option's own declaration.
96
+ const message = getErrorMessage(error);
97
+ const option = /required option '([^']+)'/.exec(message)?.[1];
98
+ const invoked = resolveInvokedCommandPath();
99
+ printErrorEnvelope(defaultIo, invoked, 'MISSING_REQUIRED_OPTION', option === undefined
100
+ ? message
101
+ : `Missing required option '${option}' for \`peaks ${invoked}\`.`, { option: option ?? null }, [
102
+ option === undefined
103
+ ? `Run \`peaks ${invoked} --help\` to see the options this command requires.`
104
+ : `Supply ${option} — it is required, so the command has no default for it.`,
105
+ `Run \`peaks ${invoked} --help\` for the option's accepted values and its siblings.`
106
+ ]);
107
+ return;
108
+ }
62
109
  if (error.code === 'commander.missingArgument' || error.code === 'commander.unknownCommand' || error.code === 'commander.unknownOption') {
63
110
  // Emit a `COMMAND_NOT_FOUND` JSON envelope for the unknown-command
64
111
  // path. The error text already went to stderr via Commander's
@@ -17,6 +17,7 @@ import { registerWorkspaceCommands } from './commands/workspace-commands.js';
17
17
  import { registerSopCommands } from './commands/sop-commands.js';
18
18
  import { registerSkillVisibilityCommand } from './commands/skill-visibility.js';
19
19
  import { registerPrimerCommand } from './commands/primer-command.js';
20
+ import { registerReinjectCommand } from './commands/reinject-command.js';
20
21
  import { applyRetention, cleanupEccCache } from '../services/log/retention.js';
21
22
  import { writeLogEntry, maybeWriteStderr } from '../services/log/logger.js';
22
23
  import { printSuperCommandCatalog } from './cli-helpers.js';
@@ -217,5 +218,9 @@ Run peaks (no arguments) for a quickstart. You likely want one of:
217
218
  // `program.command('session primer')` (that registers a single
218
219
  // literal command name, not a child of the session group).
219
220
  registerPrimerCommand(program, io);
221
+ // rid 2026-09-13-a2-post-compact-reinject: register `peaks session reinject`
222
+ // as a sibling child of the same `session` group. It is the transport for
223
+ // the SessionStart (matcher `compact`) hook's post-compaction card.
224
+ registerReinjectCommand(program, io);
220
225
  return program;
221
226
  }
@@ -59,7 +59,7 @@ export function runHookGuardSubprocess(prompt) {
59
59
  'sub-agent-dispatch-guard',
60
60
  '--prompt', prompt,
61
61
  '--json'
62
- ], { encoding: 'utf8' });
62
+ ], { encoding: 'utf8', windowsHide: true });
63
63
  if (result.status !== 0) {
64
64
  // Fallback: allow (don't block the dispatch on a guard subprocess failure).
65
65
  return {
@@ -53,7 +53,7 @@ class ProcessVendorAdapter {
53
53
  if (args.force === true)
54
54
  argv.push('--force');
55
55
  return new Promise((resolveRun) => {
56
- const proc = spawn(this.binary, argv, { stdio: ['ignore', 'pipe', 'pipe'] });
56
+ const proc = spawn(this.binary, argv, { stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true });
57
57
  let stdout = '';
58
58
  let stderr = '';
59
59
  proc.stdout.on('data', (chunk) => { stdout += chunk.toString('utf8'); });
@@ -14,7 +14,7 @@ function getRemoteUrl(artifactRepo) {
14
14
  }
15
15
  function hasGit() {
16
16
  try {
17
- execFileSync('git', ['--version']);
17
+ execFileSync('git', ['--version'], { windowsHide: true });
18
18
  return true;
19
19
  }
20
20
  catch {
@@ -16,7 +16,8 @@ export async function runJ01Contract(ctx) {
16
16
  try {
17
17
  const stdout = execFileSync('node', [bin, command, input], {
18
18
  cwd: ctx.projectRoot,
19
- env: { ...process.env, PEAKS_CALLER_ID: `guard-J01-${ctx.sessionId}` }
19
+ env: { ...process.env, PEAKS_CALLER_ID: `guard-J01-${ctx.sessionId}` },
20
+ windowsHide: true
20
21
  }).toString('utf8');
21
22
  const env = JSON.parse(stdout);
22
23
  if (!env.ok) {
@@ -6,10 +6,10 @@ const STATES = ['spec-locked', 'implemented', 'qa-handoff', 'handed-off'];
6
6
  export async function runJ02Contract(ctx, projectRoot = ctx.projectRoot) {
7
7
  const bin = join(ctx.projectRoot, 'bin', 'peaks.js');
8
8
  const tmp = mkdtempSync(join(tmpdir(), 'cbl-J02-'));
9
- const ws = execFileSync('node', [bin, 'workspace', 'init', '--project', tmp, '--json'], { cwd: tmp }).toString('utf8');
9
+ const ws = execFileSync('node', [bin, 'workspace', 'init', '--project', tmp, '--json'], { cwd: tmp, windowsHide: true }).toString('utf8');
10
10
  const { data: { sessionId } } = JSON.parse(ws);
11
11
  const rid = '2026-08-03-j02-fixture';
12
- const initOut = execFileSync('node', [bin, 'request', 'init', '--role', 'rd', '--id', rid, '--project', tmp, '--session-id', sessionId, '--apply', '--json'], { cwd: tmp }).toString('utf8');
12
+ const initOut = execFileSync('node', [bin, 'request', 'init', '--role', 'rd', '--id', rid, '--project', tmp, '--session-id', sessionId, '--apply', '--json'], { cwd: tmp, windowsHide: true }).toString('utf8');
13
13
  const initEnv = JSON.parse(initOut);
14
14
  // `request init` writes the file as `NNN-<id-slug>.md`. The transition CLI accepts
15
15
  // the file's basename (without .md) as the requestId. Derive it from data.path.
@@ -19,7 +19,7 @@ export async function runJ02Contract(ctx, projectRoot = ctx.projectRoot) {
19
19
  for (const s of STATES) {
20
20
  const out = execFileSync('node', [bin, 'request', 'transition', requestId, '--role', 'rd', '--state', s,
21
21
  '--project', tmp, '--session-id', sessionId, '--confirm', '--allow-incomplete',
22
- '--reason', 'J02 contract fixture', '--json'], { cwd: tmp }).toString('utf8');
22
+ '--reason', 'J02 contract fixture', '--json'], { cwd: tmp, windowsHide: true }).toString('utf8');
23
23
  const env = JSON.parse(out);
24
24
  last = env.data.state;
25
25
  }
@@ -10,7 +10,8 @@ export async function runJ04Contract(ctx) {
10
10
  try {
11
11
  const stdout = execFileSync('node', [bin, 'audit', 'goal', '--help'], {
12
12
  cwd: ctx.projectRoot,
13
- env: { ...process.env, PEAKS_CALLER_ID: `guard-J04-${ctx.sessionId}` }
13
+ env: { ...process.env, PEAKS_CALLER_ID: `guard-J04-${ctx.sessionId}` },
14
+ windowsHide: true
14
15
  }).toString('utf8');
15
16
  helpOk = stdout.length > 0 && !stdout.includes('unknown command');
16
17
  }
@@ -23,7 +24,8 @@ export async function runJ04Contract(ctx) {
23
24
  execFileSync('node', [bin, 'audit', 'goal', '--project', tmp, '--json'], {
24
25
  cwd: ctx.projectRoot,
25
26
  env: { ...process.env, PEAKS_CALLER_ID: `guard-J04-${ctx.sessionId}` },
26
- stdio: ['ignore', 'pipe', 'pipe']
27
+ stdio: ['ignore', 'pipe', 'pipe'],
28
+ windowsHide: true
27
29
  });
28
30
  }
29
31
  catch (e) {
@@ -19,7 +19,8 @@ export async function runJ07Contract(ctx) {
19
19
  const out = execFileSync('node', [bin, 'test', '--help'], {
20
20
  cwd: ctx.projectRoot,
21
21
  env: { ...process.env, PEAKS_CALLER_ID: `guard-J07-${ctx.sessionId}` },
22
- stdio: ['ignore', 'pipe', 'pipe']
22
+ stdio: ['ignore', 'pipe', 'pipe'],
23
+ windowsHide: true
23
24
  }).toString('utf8');
24
25
  helpOk = out.length > 0 && !out.includes('unknown command');
25
26
  }
@@ -126,6 +126,12 @@ export declare function summarizeLifecycleError(error: unknown): string;
126
126
  * `afterRatio: 0` there would publish a fabricated number;
127
127
  * - the ratio is still at or above the auto-fire threshold — the
128
128
  * compact has not landed, so the run stays open.
129
+ *
130
+ * Returns the settled record, or `null` when nothing settled. Slice
131
+ * 2026-09-13-auto-compact-trigger-ownership: the caller needs the settled
132
+ * `afterRatio` to append the "observed compaction point" row that makes the
133
+ * intent-vs-observed gap readable after a real session. The return value is
134
+ * telemetry only — callers that ignore it are unaffected.
129
135
  */
130
136
  export declare function settleOpenLifecycleRun(input: {
131
137
  readonly projectRoot: string;
@@ -134,5 +140,9 @@ export declare function settleOpenLifecycleRun(input: {
134
140
  readonly source: string;
135
141
  readonly autoFireThreshold: number;
136
142
  readonly onLifecycleStage?: ((stage: CompactLifecycleStage, record: CompactLifecycleRecord) => void) | undefined;
137
- }): void;
143
+ }): {
144
+ readonly runId: string;
145
+ readonly triggerRatio: number;
146
+ readonly afterRatio: number;
147
+ } | null;
138
148
  export {};