@haposoft/cafekit 0.16.7 → 0.18.0

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 (193) hide show
  1. package/README.md +14 -22
  2. package/bin/__tests__/codex-hooks-ownership.test.js +2 -2
  3. package/bin/__tests__/codex-hooks.test.js +0 -36
  4. package/bin/__tests__/codex-native.test.js +46 -88
  5. package/bin/__tests__/develop-contract.test.js +26 -57
  6. package/bin/__tests__/docs-sync-on-demand.test.js +93 -0
  7. package/bin/__tests__/grok-envelope.test.js +0 -56
  8. package/bin/__tests__/hook-payload.test.js +4 -6
  9. package/bin/__tests__/legacy-authority-decoupled.test.js +159 -0
  10. package/bin/__tests__/legacy-hooks-retired.test.js +221 -0
  11. package/bin/__tests__/omp-bridge.test.js +20 -17
  12. package/bin/__tests__/omp-hooks.test.js +0 -19
  13. package/bin/__tests__/optional-skill-inventory.test.js +112 -4
  14. package/bin/__tests__/orca-session.test.js +0 -2
  15. package/bin/__tests__/package-inventory.test.js +100 -541
  16. package/bin/__tests__/runtime-dir.test.js +0 -3
  17. package/bin/__tests__/runtime-schema.test.js +3 -2
  18. package/bin/__tests__/skill-routing-source.test.js +27 -15
  19. package/bin/__tests__/spec-narrowing.test.js +0 -27
  20. package/bin/__tests__/specs-v2-execution-closeout.test.js +4 -69
  21. package/bin/__tests__/ui-ux-skill.test.js +101 -0
  22. package/bin/__tests__/usage-hook-retired.test.js +114 -0
  23. package/bin/__tests__/validator-grounding.test.js +9 -28
  24. package/bin/phases/codex-runtime.js +24 -0
  25. package/bin/phases/omp-runtime.js +22 -0
  26. package/package.json +1 -1
  27. package/src/claude/CLAUDE.md +3 -10
  28. package/src/claude/agents/brainstormer.md +7 -3
  29. package/src/claude/agents/code-auditor.md +35 -50
  30. package/src/claude/agents/debugger.md +5 -79
  31. package/src/claude/agents/deployer.md +3 -5
  32. package/src/claude/agents/docs-keeper.md +2 -7
  33. package/src/claude/agents/implementer.md +6 -9
  34. package/src/claude/agents/inspector.md +2 -3
  35. package/src/claude/agents/project-manager.md +7 -15
  36. package/src/claude/agents/researcher.md +9 -3
  37. package/src/claude/agents/spec-maker.md +11 -11
  38. package/src/claude/hooks/__tests__/fixtures/statusline-default.golden +1 -1
  39. package/src/claude/hooks/__tests__/spec-gate.test.js +74 -3
  40. package/src/claude/hooks/__tests__/spec-state-touched.test.js +134 -0
  41. package/src/claude/hooks/__tests__/state.test.js +68 -0
  42. package/src/claude/hooks/__tests__/statusline.test.js +140 -11
  43. package/src/claude/hooks/agent.cjs +2 -1
  44. package/src/claude/hooks/lib/context.cjs +0 -89
  45. package/src/claude/hooks/lib/hook-payload.cjs +2 -2
  46. package/src/claude/hooks/rules.cjs +2 -1
  47. package/src/claude/hooks/session.cjs +0 -1
  48. package/src/claude/hooks/spec-gate.cjs +17 -28
  49. package/src/claude/hooks/spec-state.cjs +95 -1
  50. package/src/claude/hooks/state.cjs +21 -2
  51. package/src/claude/migration-manifest.json +30 -15
  52. package/src/claude/rules/ai-dev-rules.md +1 -1
  53. package/src/claude/rules/hook-protocols.md +8 -15
  54. package/src/claude/rules/manage-docs.md +9 -23
  55. package/src/claude/rules/orchestrator.md +3 -4
  56. package/src/claude/rules/review-audit-self-decision.md +3 -3
  57. package/src/claude/rules/skill-domain-routing.md +1 -3
  58. package/src/claude/rules/skill-workflow-routing.md +4 -5
  59. package/src/claude/rules/state-sync.md +3 -12
  60. package/src/claude/rules/workflow.md +10 -18
  61. package/src/claude/runtime.schema.json +5 -3
  62. package/src/claude/scripts/generate-skill-catalog.cjs +7 -6
  63. package/src/claude/scripts/provenance.cjs +23 -5
  64. package/src/claude/scripts/spec-receipt.cjs +24 -7
  65. package/src/claude/scripts/spec-resolver.cjs +1 -2
  66. package/src/claude/scripts/validate-spec-output.cjs +5 -32
  67. package/src/claude/settings/settings.json +1 -32
  68. package/src/claude/skills/{question → ask}/SKILL.md +9 -75
  69. package/src/claude/skills/brainstorm/SKILL.md +43 -131
  70. package/src/claude/skills/code-review/SKILL.md +48 -17
  71. package/src/claude/skills/code-review/references/spec-compliance-review.md +2 -3
  72. package/src/claude/skills/code-review/references/verification-gate.md +22 -27
  73. package/src/claude/skills/debug/SKILL.md +29 -149
  74. package/src/claude/skills/develop/SKILL.md +27 -36
  75. package/src/claude/skills/develop/references/parallel-waves.md +0 -8
  76. package/src/claude/skills/develop/references/quality-gate.md +15 -13
  77. package/src/claude/skills/docs/SKILL.md +2 -2
  78. package/src/claude/skills/docs/references/update-workflow.md +1 -1
  79. package/src/claude/skills/fix/SKILL.md +153 -0
  80. package/src/claude/skills/{hotfix → fix}/references/diagnosis-protocol.md +2 -2
  81. package/src/claude/skills/{hotfix → fix}/references/parallel-patterns.md +2 -2
  82. package/src/claude/skills/{hotfix → fix}/references/prevention-gate.md +3 -3
  83. package/src/claude/skills/{hotfix → fix}/references/review-cycle.md +1 -1
  84. package/src/claude/skills/git/SKILL.md +48 -9
  85. package/src/claude/skills/git/references/commit-protocols.md +35 -16
  86. package/src/claude/skills/git/references/worktree-blueprint.md +42 -6
  87. package/src/claude/skills/orca/SKILL.md +9 -8
  88. package/src/claude/skills/research/SKILL.md +2 -0
  89. package/src/claude/skills/scout/SKILL.md +61 -0
  90. package/src/claude/skills/{inspect → scout}/references/internal-inspection.md +26 -10
  91. package/src/claude/skills/specs/SKILL.md +22 -29
  92. package/src/claude/skills/specs/references/review.md +8 -6
  93. package/src/claude/skills/specs/references/templates.md +34 -41
  94. package/src/claude/skills/sync/SKILL.md +8 -15
  95. package/src/claude/skills/sync/references/rebind-and-audit.md +59 -0
  96. package/src/claude/skills/sync/references/sync-protocols.md +4 -13
  97. package/src/claude/skills/test/SKILL.md +22 -3
  98. package/src/claude/skills/test/references/execution-strategy.md +83 -0
  99. package/src/claude/skills/test/references/failure-triage.md +1 -1
  100. package/src/claude/skills/ui-ux/LICENSE +21 -0
  101. package/src/claude/skills/ui-ux/SKILL.md +497 -0
  102. package/src/claude/skills/ui-ux/references/brand-tokens.md +90 -0
  103. package/src/claude/skills/ui-ux/references/budgets.md +113 -0
  104. package/src/claude/skills/ui-ux/references/checklist.md +271 -0
  105. package/src/claude/skills/ui-ux/references/components/accordion.md +246 -0
  106. package/src/claude/skills/ui-ux/references/components/avatar.md +153 -0
  107. package/src/claude/skills/ui-ux/references/components/banner.md +43 -0
  108. package/src/claude/skills/ui-ux/references/components/breadcrumb.md +201 -0
  109. package/src/claude/skills/ui-ux/references/components/button.md +142 -0
  110. package/src/claude/skills/ui-ux/references/components/card.md +113 -0
  111. package/src/claude/skills/ui-ux/references/components/charts.md +376 -0
  112. package/src/claude/skills/ui-ux/references/components/chat.md +132 -0
  113. package/src/claude/skills/ui-ux/references/components/choice-controls.md +369 -0
  114. package/src/claude/skills/ui-ux/references/components/comment-thread.md +17 -0
  115. package/src/claude/skills/ui-ux/references/components/description-list.md +116 -0
  116. package/src/claude/skills/ui-ux/references/components/empty-state.md +98 -0
  117. package/src/claude/skills/ui-ux/references/components/file-upload.md +107 -0
  118. package/src/claude/skills/ui-ux/references/components/inline-edit.md +128 -0
  119. package/src/claude/skills/ui-ux/references/components/input.md +161 -0
  120. package/src/claude/skills/ui-ux/references/components/list-row.md +56 -0
  121. package/src/claude/skills/ui-ux/references/components/loading.md +198 -0
  122. package/src/claude/skills/ui-ux/references/components/logo.md +147 -0
  123. package/src/claude/skills/ui-ux/references/components/otp-input.md +43 -0
  124. package/src/claude/skills/ui-ux/references/components/quantity-input.md +100 -0
  125. package/src/claude/skills/ui-ux/references/components/range-slider.md +16 -0
  126. package/src/claude/skills/ui-ux/references/components/small-controls.md +340 -0
  127. package/src/claude/skills/ui-ux/references/components/sortable-header.md +86 -0
  128. package/src/claude/skills/ui-ux/references/components/tag-input.md +14 -0
  129. package/src/claude/skills/ui-ux/references/components/timeline.md +38 -0
  130. package/src/claude/skills/ui-ux/references/components/tree.md +30 -0
  131. package/src/claude/skills/ui-ux/references/design-process.md +650 -0
  132. package/src/claude/skills/ui-ux/references/layouts/app-kanban.html +303 -0
  133. package/src/claude/skills/ui-ux/references/layouts/app.md +1369 -0
  134. package/src/claude/skills/ui-ux/references/layouts/form.md +430 -0
  135. package/src/claude/skills/ui-ux/references/layouts/overlay.md +552 -0
  136. package/src/claude/skills/ui-ux/references/layouts/pricing.md +204 -0
  137. package/src/claude/skills/ui-ux/references/locked-rules.md +35 -0
  138. package/src/claude/skills/ui-ux/references/principles.md +408 -0
  139. package/src/claude/skills/ui-ux/references/refactor.md +245 -0
  140. package/src/claude/skills/ui-ux/references/responsive.md +195 -0
  141. package/src/claude/skills/ui-ux/references/review.md +528 -0
  142. package/src/claude/skills/ui-ux/references/rules-color.md +623 -0
  143. package/src/claude/skills/ui-ux/references/rules-form.md +235 -0
  144. package/src/claude/skills/ui-ux/references/rules-state.md +680 -0
  145. package/src/claude/skills/ui-ux/references/rules-type.md +369 -0
  146. package/src/claude/skills/ui-ux/references/styles.md +311 -0
  147. package/src/claude/skills/ui-ux/references/system.md +219 -0
  148. package/src/claude/skills/ui-ux/references/tailwind-v4-traps.md +237 -0
  149. package/src/claude/skills/ui-ux/references/tokens.css +362 -0
  150. package/src/claude/skills/ui-ux/scripts/lint-skill.mjs +216 -0
  151. package/src/claude/skills/ui-ux/scripts/package.json +9 -0
  152. package/src/claude/skills/ui-ux/scripts/probe.mjs +4447 -0
  153. package/src/claude/skills/ui-ux-pro-max/SKILL.md +1 -1
  154. package/src/claude/status.cjs +149 -144
  155. package/src/codex/AGENTS.md +8 -13
  156. package/src/codex/hooks/agent.cjs +2 -1
  157. package/src/codex/hooks/lib/spec-receipt.cjs +2 -2
  158. package/src/codex/hooks/lib/spec-utils.cjs +0 -7
  159. package/src/codex/hooks/rules.cjs +2 -1
  160. package/src/codex/hooks/session.cjs +0 -2
  161. package/src/codex/hooks/spec-gate.cjs +7 -18
  162. package/src/codex/hooks.json +0 -20
  163. package/src/codex/rules/state-sync.md +3 -12
  164. package/src/common/AGENTS.md +2 -2
  165. package/src/omp/extensions/cafekit-bridge.mjs +6 -6
  166. package/src/claude/hooks/__tests__/completion-authority.test.js +0 -990
  167. package/src/claude/hooks/__tests__/semantic-review-authority.test.js +0 -259
  168. package/src/claude/hooks/completion-authority-check.cjs +0 -56
  169. package/src/claude/hooks/completion-authority-state.cjs +0 -527
  170. package/src/claude/hooks/completion-authority.cjs +0 -163
  171. package/src/claude/hooks/semantic-review-authority.cjs +0 -154
  172. package/src/claude/hooks/task-scaffold-guard.cjs +0 -114
  173. package/src/claude/hooks/usage.cjs +0 -197
  174. package/src/claude/skills/delegate/SKILL.md +0 -124
  175. package/src/claude/skills/delegate/references/codex-delegation.md +0 -71
  176. package/src/claude/skills/delegate/references/grok-delegation.md +0 -84
  177. package/src/claude/skills/hotfix/SKILL.md +0 -289
  178. package/src/claude/skills/inspect/SKILL.md +0 -142
  179. package/src/claude/skills/loop/SKILL.md +0 -114
  180. package/src/claude/skills/loop/references/bounded-loop-protocol.md +0 -104
  181. package/src/claude/skills/loop/references/metric-and-guard-contract.md +0 -76
  182. package/src/claude/skills/route/SKILL.md +0 -94
  183. package/src/claude/skills/route/references/agent-timing.md +0 -46
  184. package/src/claude/skills/route/references/chaining-patterns.md +0 -53
  185. package/src/claude/skills/route/references/task-taxonomy.md +0 -48
  186. package/src/codex/hooks/completion-authority-check.cjs +0 -67
  187. package/src/codex/hooks/completion-authority-state.cjs +0 -527
  188. package/src/codex/hooks/completion-authority.cjs +0 -109
  189. package/src/codex/hooks/semantic-review-authority.cjs +0 -130
  190. package/src/codex/hooks/task-scaffold-guard.cjs +0 -63
  191. /package/src/claude/skills/{question → ask}/templates/question.md +0 -0
  192. /package/src/claude/skills/{hotfix → fix}/references/escalation-tactics.md +0 -0
  193. /package/src/claude/skills/{hotfix → fix}/references/workflow-specialized.md +0 -0
@@ -295,9 +295,6 @@ function installClaudeGate(root, policyMode = 'valid') {
295
295
  path.join(__dirname, '..', 'lib', 'hook-state-dir.cjs'),
296
296
  path.join(hooks, 'lib', 'hook-state-dir.cjs'),
297
297
  );
298
- for (const name of [
299
- 'completion-authority-check.cjs', 'completion-authority-state.cjs', 'semantic-review-authority.cjs',
300
- ]) fs.copyFileSync(path.join(__dirname, '..', name), path.join(hooks, name));
301
298
  for (const name of ['spec-resolver.cjs', 'spec-receipt.cjs', 'validate-spec-output.cjs', 'spec-ground.cjs', 'spec-semantic-model.cjs', 'spec-final-state.cjs']) {
302
299
  fs.copyFileSync(path.join(__dirname, '..', '..', 'scripts', name), path.join(scripts, name));
303
300
  }
@@ -1880,6 +1877,26 @@ test('52. the block names what to change, not just which check failed', () => {
1880
1877
  }
1881
1878
  });
1882
1879
 
1880
+ test('53. a hand-typed provenance pair is told to re-derive, not to add lines', () => {
1881
+ // Reported from a project whose receipt carried git short SHAs. Telling someone to
1882
+ // add `Base:` and `Head:` lines they had already written sends them looking in the
1883
+ // wrong place; the pair is there, it just is not the one the runtime derives.
1884
+ const dir = makeWorkflowFixture([
1885
+ '## Receipt', '', '- Verification: PASS', '- Command: node --test', '- Exit: 0',
1886
+ '- Base: d72bb57', '- Head: 5da1eca', '```text', '$ node --test', 'pass: 1', '```',
1887
+ ]);
1888
+ try {
1889
+ clearCache();
1890
+ const body = parseBlock(runHook({}, dir).stdout);
1891
+ assert.ok(body && body.decision === 'block');
1892
+ assert.match(body.reason, /present but are not the pair this runtime derives/);
1893
+ assert.match(body.reason, /short SHAs/);
1894
+ assert.doesNotMatch(body.reason, /add runtime-derived/, 'the lines exist; do not ask for them again');
1895
+ } finally {
1896
+ fs.rmSync(dir, { recursive: true, force: true });
1897
+ }
1898
+ });
1899
+
1883
1900
  test('49. an unversioned specs root does not keep every receipt bound', () => {
1884
1901
  // Committing specs/ is the user's choice, not the tool's requirement. Without a
1885
1902
  // committed copy of the task file there is no baseline, so the selector used to keep
@@ -1955,3 +1972,57 @@ test('48. the gate\'s own runtime state does not defeat structure mode', () => {
1955
1972
  fs.rmSync(dir, { recursive: true, force: true });
1956
1973
  }
1957
1974
  });
1975
+
1976
+ test('52. a plan with several commands binds the Receipt to the last one', () => {
1977
+ // A proof that takes several steps declares several commands. The Receipt must name the
1978
+ // last: naming an earlier, cheaper step would let the task close without its real proof.
1979
+ const planned = 'node -v\n- Command: node --test';
1980
+ const earlier = boundReceipt().map((line) => line
1981
+ .replace('Command: node --test', 'Command: node -v').replace('$ node --test', '$ node -v'));
1982
+ const absent = boundReceipt().map((line) => line
1983
+ .replace('Command: node --test', 'Command: npm test').replace('$ node --test', '$ npm test'));
1984
+ const cases = [
1985
+ [boundReceipt(), null, 'the last declared command closes the task'],
1986
+ [earlier, /command_identity/, 'an earlier declared command must not close the task'],
1987
+ [absent, /command_identity/, 'a command the plan never declared must not close the task'],
1988
+ ];
1989
+ for (const [receipt, expected, message] of cases) {
1990
+ const dir = makeWorkflowFixture(receipt, planned);
1991
+ try {
1992
+ clearCache();
1993
+ const out = runHook({}, dir).stdout;
1994
+ if (expected === null) {
1995
+ assert.strictEqual(out, '', message);
1996
+ } else {
1997
+ const body = parseBlock(out);
1998
+ assert.ok(body && body.decision === 'block', message);
1999
+ assert.match(body.reason, expected, message);
2000
+ }
2001
+ } finally {
2002
+ fs.rmSync(dir, { recursive: true, force: true });
2003
+ }
2004
+ }
2005
+ });
2006
+
2007
+ test('53. an indented or sub-heading Command never becomes the canonical one', () => {
2008
+ // A cheap command nested under a Counterexample, or placed under a ### step after the main
2009
+ // block, must not become the command a Receipt can close on.
2010
+ const cheap = boundReceipt().map((line) => line
2011
+ .replace('Command: node --test', 'Command: true').replace('$ node --test', '$ true'));
2012
+ for (const planned of [
2013
+ 'node --test\n- Counterexample: none\n - Command: true',
2014
+ 'node --test\n\n### Cleanup\n\n- Command: true',
2015
+ ]) {
2016
+ for (const [receipt, expected] of [[cheap, /command_identity/], [boundReceipt(), null]]) {
2017
+ const dir = makeWorkflowFixture(receipt, planned);
2018
+ try {
2019
+ clearCache();
2020
+ const out = runHook({}, dir).stdout;
2021
+ if (expected === null) assert.strictEqual(out, '', `the main command must still close: ${planned}`);
2022
+ else assert.match(parseBlock(out).reason, expected, `a nested command must not close: ${planned}`);
2023
+ } finally {
2024
+ fs.rmSync(dir, { recursive: true, force: true });
2025
+ }
2026
+ }
2027
+ }
2028
+ });
@@ -0,0 +1,134 @@
1
+ 'use strict';
2
+
3
+ // spec-state injects the active packet's tollgate on every prompt. With several sessions in
4
+ // one checkout, a Claude session kept seeing a packet another session was working on. On
5
+ // Claude the hook now speaks only about packets this session touched; omp, grok and
6
+ // payloads without a Claude session id keep the old behaviour. Each case names the failure
7
+ // it prevents.
8
+
9
+ const { test } = require('node:test');
10
+ const assert = require('node:assert');
11
+ const { spawnSync } = require('node:child_process');
12
+ const fs = require('node:fs');
13
+ const os = require('node:os');
14
+ const path = require('node:path');
15
+
16
+ const CLAUDE_ROOT = path.join(__dirname, '..', '..');
17
+
18
+ function taskContent(status) {
19
+ return ['# Task 01 — work', '', `Status: ${status}`, '', '## Dependencies', '', '- none', '',
20
+ '## Verification Plan', '', '- Command: node --test', ''].join('\n');
21
+ }
22
+
23
+ function writePacket(root, name) {
24
+ const dir = path.join(root, 'specs', name);
25
+ fs.mkdirSync(dir, { recursive: true });
26
+ fs.writeFileSync(path.join(dir, 'plan.md'), `# ${name}\nSpecs-Contract: process-first-ready-v1\n`);
27
+ fs.writeFileSync(path.join(dir, 'task-01-work.md'), taskContent('pending'));
28
+ return dir;
29
+ }
30
+
31
+ /** A project with the installed hook tree under `<root>/<runtimeDir>/hooks` and the given packets. */
32
+ function fixture(packets, runtimeDir = '.claude') {
33
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'ck-spec-touched-'));
34
+ const hooks = path.join(root, runtimeDir, 'hooks');
35
+ const scripts = path.join(root, runtimeDir, 'scripts');
36
+ fs.mkdirSync(path.join(hooks, 'lib'), { recursive: true });
37
+ fs.mkdirSync(scripts, { recursive: true });
38
+ fs.copyFileSync(path.join(CLAUDE_ROOT, 'hooks', 'spec-state.cjs'), path.join(hooks, 'spec-state.cjs'));
39
+ for (const lib of ['runtime-dir.cjs', 'hook-payload.cjs', 'hook-state-dir.cjs']) {
40
+ fs.copyFileSync(path.join(CLAUDE_ROOT, 'hooks', 'lib', lib), path.join(hooks, 'lib', lib));
41
+ }
42
+ for (const file of ['provenance.cjs', 'spec-receipt.cjs', 'spec-resolver.cjs', 'workflow-policy.cjs']) {
43
+ fs.copyFileSync(path.join(CLAUDE_ROOT, 'scripts', file), path.join(scripts, file));
44
+ }
45
+ for (const name of packets) writePacket(root, name);
46
+ for (const args of [['init', '-q'], ['config', 'user.email', 'cafekit@example.invalid'],
47
+ ['config', 'user.name', 'CafeKit Test'], ['commit', '--allow-empty', '-qm', 'fixture']]) {
48
+ const git = spawnSync('git', ['-C', root, ...args], { encoding: 'utf8' });
49
+ assert.strictEqual(git.status, 0, git.stderr);
50
+ }
51
+ const run = (payload) => {
52
+ const result = spawnSync(process.execPath, [path.join(hooks, 'spec-state.cjs')], {
53
+ cwd: root, env: { ...process.env, PROJECT_ROOT: root },
54
+ input: JSON.stringify({ cwd: root, ...payload }), encoding: 'utf8',
55
+ });
56
+ assert.strictEqual(result.status, 0, result.stderr);
57
+ return result.stdout.trim();
58
+ };
59
+ const prompt = (session, text) => run({ hook_event_name: 'UserPromptSubmit', session_id: session, prompt: text });
60
+ const cacheFile = path.join(hooks, '.logs', 'tollgate-last.txt');
61
+ return { root, hooks, run, prompt, cacheFile, cleanup: () => fs.rmSync(root, { recursive: true, force: true }) };
62
+ }
63
+
64
+ function within(fx, fn) {
65
+ try { return fn(fx); } finally { fx.cleanup(); }
66
+ }
67
+
68
+ test('an untouched packet prints nothing on Claude', () => {
69
+ within(fixture(['auth-flow']), (fx) => {
70
+ assert.strictEqual(fx.prompt('s1', 'Continue'), '');
71
+ assert.strictEqual(fs.existsSync(fx.cacheFile), false, 'a filtered session must not claim the cache slot');
72
+ });
73
+ });
74
+
75
+ test('naming the packet in the prompt touches it and keeps the full block', () => {
76
+ within(fixture(['auth-flow']), (fx) => {
77
+ assert.strictEqual(fx.prompt('s1', 'Continue'), '');
78
+ assert.match(fx.prompt('s1', 'continue auth-flow'), /Spec state changed/);
79
+ assert.match(fx.prompt('s1', 'Continue'), /Tollgate active/);
80
+ });
81
+ });
82
+
83
+ test('editing a file inside the packet touches it silently', () => {
84
+ within(fixture(['auth-flow']), (fx) => {
85
+ // macOS tmp dirs live under /private/var behind the /var symlink; send the other spelling.
86
+ const real = fs.realpathSync(fx.root);
87
+ const alias = real.startsWith('/private/var/') ? real.slice('/private'.length) : (() => {
88
+ const link = path.join(os.tmpdir(), `ck-alias-${process.pid}-${Date.now()}`);
89
+ fs.symlinkSync(real, link);
90
+ return link;
91
+ })();
92
+ const edited = path.join(alias, 'specs', 'auth-flow', 'plan.md');
93
+ assert.notStrictEqual(edited, fs.realpathSync(edited), 'the path sent must not be canonical');
94
+ const out = fx.run({ hook_event_name: 'PostToolUse', session_id: 's2', tool_name: 'Edit', tool_input: { file_path: edited } });
95
+ assert.strictEqual(out, '');
96
+ assert.strictEqual(fs.existsSync(fx.cacheFile), false, 'a touch record must not run the tollgate');
97
+ assert.match(fx.prompt('s2', 'Continue'), /Spec state changed/);
98
+ if (!real.startsWith('/private/var/')) fs.rmSync(alias, { force: true });
99
+ });
100
+ });
101
+
102
+ test('a plain one-word slug needs specs/ prefix', () => {
103
+ within(fixture(['docs']), (fx) => {
104
+ assert.strictEqual(fx.prompt('s1', 'fix the docs'), '');
105
+ assert.match(fx.prompt('s1', 'look at specs/docs'), /Spec state changed/);
106
+ });
107
+ });
108
+
109
+ test("another session's touch does not leak", () => {
110
+ within(fixture(['auth-flow']), (fx) => {
111
+ assert.match(fx.prompt('s3', 'specs/auth-flow'), /Spec state changed/);
112
+ assert.strictEqual(fx.prompt('s4', 'Continue'), '');
113
+ });
114
+ });
115
+
116
+ test('omp, grok and session-less payloads keep the old behaviour', () => {
117
+ within(fixture(['auth-flow'], '.omp'), (fx) => {
118
+ assert.match(fx.prompt('s5', 'Continue'), /Spec state changed/, 'omp is unchanged');
119
+ });
120
+ within(fixture(['auth-flow']), (fx) => {
121
+ assert.match(fx.run({ hook_event_name: 'UserPromptSubmit', sessionId: 's6', prompt: 'Continue' }), /Spec/, 'grok sends sessionId');
122
+ });
123
+ within(fixture(['auth-flow']), (fx) => {
124
+ assert.match(fx.run({ hook_event_name: 'UserPromptSubmit', prompt: 'Continue' }), /Spec/, 'no session id');
125
+ });
126
+ });
127
+
128
+ test('a prompt touch is remembered even when two packets are active', () => {
129
+ within(fixture(['auth-flow', 'billing-2']), (fx) => {
130
+ assert.match(fx.prompt('s7', 'work on specs/auth-flow'), /Multiple active specs/);
131
+ fs.rmSync(path.join(fx.root, 'specs', 'billing-2'), { recursive: true, force: true });
132
+ assert.match(fx.prompt('s7', 'Continue'), /auth-flow/);
133
+ });
134
+ });
@@ -316,3 +316,71 @@ test('Stop snapshot includes untracked files before the first commit', () => {
316
316
  fs.rmSync(dir, { recursive: true, force: true });
317
317
  }
318
318
  });
319
+
320
+ // The SessionStart block used to print even when it held only headings, placeholders and
321
+ // timestamp-only agent results, which told a resumed Claude session nothing.
322
+ const PLACEHOLDER_STATE = [
323
+ '# Session State',
324
+ '<!-- Generated: GENERATED -->',
325
+ '<!-- Branch: unknown -->',
326
+ '',
327
+ '## What Worked (Verified)',
328
+ '- (No completed tasks recorded)',
329
+ '',
330
+ "## What's Left",
331
+ '- (All tasks completed)',
332
+ '',
333
+ '## Key Files Modified',
334
+ '- (No file changes detected)',
335
+ '',
336
+ '## Agent Result: code-auditor (01:44:00)',
337
+ '- Completed at 01:44:00',
338
+ '',
339
+ '## Agent Result: code-auditor (01:44:21)',
340
+ '- Completed at 01:44:21',
341
+ '',
342
+ '## Agent Result: code-auditor (01:44:50)',
343
+ '- Completed at 01:44:50',
344
+ '',
345
+ ].join('\n');
346
+
347
+ function sessionStartWith(content, runtimeFolder = '.claude') {
348
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ck-state-start-'));
349
+ try {
350
+ let hook = STATE_HOOK;
351
+ if (runtimeFolder !== '.claude') {
352
+ const hooks = path.join(dir, runtimeFolder, 'hooks');
353
+ fs.mkdirSync(path.join(hooks, 'lib'), { recursive: true });
354
+ fs.copyFileSync(STATE_HOOK, path.join(hooks, 'state.cjs'));
355
+ for (const lib of ['parser.cjs', 'runtime-dir.cjs', 'hook-payload.cjs', 'hook-state-dir.cjs']) {
356
+ fs.copyFileSync(path.join(path.dirname(STATE_HOOK), 'lib', lib), path.join(hooks, 'lib', lib));
357
+ }
358
+ hook = path.join(hooks, 'state.cjs');
359
+ }
360
+ const stateFolder = path.join(dir, runtimeFolder, 'session-state');
361
+ fs.mkdirSync(stateFolder, { recursive: true });
362
+ fs.writeFileSync(path.join(stateFolder, 'latest.md'), content.replace('GENERATED', new Date().toISOString()));
363
+ const result = spawnSync(process.execPath, [hook], {
364
+ cwd: dir,
365
+ input: JSON.stringify({ hook_event_name: 'SessionStart', session_id: 'start-1', cwd: dir }),
366
+ encoding: 'utf8',
367
+ });
368
+ assert.strictEqual(result.status, 0, result.stderr);
369
+ return result.stdout.trim();
370
+ } finally {
371
+ fs.rmSync(dir, { recursive: true, force: true });
372
+ }
373
+ }
374
+
375
+ test('SessionStart prints nothing when prior context is only placeholders', () => {
376
+ assert.strictEqual(sessionStartWith(PLACEHOLDER_STATE), '');
377
+ });
378
+
379
+ test('SessionStart prints prior context that has a completed todo', () => {
380
+ const withTodo = PLACEHOLDER_STATE.replace('- (No completed tasks recorded)', '- Ship the statusline');
381
+ assert.match(sessionStartWith(withTodo), /Prior Execution Context[\s\S]*Ship the statusline/);
382
+ });
383
+
384
+ test('SessionStart under .omp keeps printing placeholder-only context', () => {
385
+ assert.match(sessionStartWith(PLACEHOLDER_STATE, '.omp'), /Prior Execution Context/);
386
+ });
@@ -10,6 +10,9 @@ const path = require('node:path');
10
10
  const STATUS_PATH = path.resolve(__dirname, '..', '..', 'status.cjs');
11
11
  const GOLDEN_PATH = path.join(__dirname, 'fixtures', 'statusline-default.golden');
12
12
  const NBSP = / /g;
13
+ const ANSI = /\x1b\[[0-9;]*m/g;
14
+ /** Visible text: colour codes removed, non-breaking spaces read as spaces, reset countdown masked. */
15
+ const visible = (text) => text.replace(ANSI, '').replace(NBSP, ' ').replace(/\d+h\d+m/g, '<reset>');
13
16
 
14
17
  const cleanupDirs = [];
15
18
  process.on('exit', () => {
@@ -79,6 +82,16 @@ function runStatus({ runtime = { statusline: 'full', statuslineColors: true }, p
79
82
  }
80
83
  }
81
84
 
85
+ /** Claude Code's own quota payload: used percentages and epoch-second resets 2h29m and 2d5h ahead. */
86
+ function rateLimits({ fiveHour = 20, weekly = 45 } = {}) {
87
+ const nowSec = Math.floor(Date.now() / 1000);
88
+ return {
89
+ five_hour: { used_percentage: fiveHour, resets_at: nowSec + 2 * 3600 + 29 * 60 + 30 },
90
+ seven_day: { used_percentage: weekly, resets_at: nowSec + 2 * 86400 + 5 * 3600 },
91
+ };
92
+ }
93
+
94
+ /** The file the retired usage hook used to write; the statusline must ignore it. */
82
95
  function freshUsageCache(now = Date.now()) {
83
96
  return {
84
97
  status: 'available',
@@ -138,21 +151,19 @@ test('statusline empty or invalid layout falls back to the default renderers', (
138
151
  }
139
152
  });
140
153
 
141
- test('statusline shows five-hour and weekly windows with countdowns when the cache is fresh', () => {
142
- const run = runStatus({ tmpFiles: { 'ck-usage-limits-cache.json': freshUsageCache() } });
154
+ test('statusline shows five-hour and weekly windows with countdowns from the payload', () => {
155
+ const run = runStatus({ payload: basePayload({ rate_limits: rateLimits() }) });
143
156
  assert.equal(run.status, 0, run.stderr);
144
- const plain = run.stdout.replace(NBSP, ' ');
145
- assert.match(plain, /⌛ \d+h \d+m left \(20% used\)/, plain);
146
- assert.match(plain, /wk 45% \(\d+d \d+h\)/, plain);
157
+ const plain = visible(run.stdout);
158
+ assert.match(plain, /⧗ 20% <reset>/, plain);
159
+ assert.match(plain, /◷ 45%/, plain);
147
160
  });
148
161
 
149
- test('statusline hides the quota area when the cache is stale', () => {
150
- const stale = freshUsageCache(Date.now() - 10 * 60 * 1000);
151
- stale.timestamp = Date.now() - 10 * 60 * 1000;
152
- const run = runStatus({ tmpFiles: { 'ck-usage-limits-cache.json': stale } });
162
+ test('statusline shows no quota when the payload has no rate_limits, even with a fresh usage cache', () => {
163
+ const run = runStatus({ tmpFiles: { 'ck-usage-limits-cache.json': freshUsageCache() } });
153
164
  assert.equal(run.status, 0, run.stderr);
154
- assert.doesNotMatch(run.stdout, /⌛/);
155
- assert.doesNotMatch(run.stdout, /wk /);
165
+ assert.doesNotMatch(run.stdout, /⧗/);
166
+ assert.doesNotMatch(run.stdout, /◷/);
156
167
  });
157
168
 
158
169
  test('statusline cost renders only when the layout enables it, billing is api, and data exists', () => {
@@ -194,3 +205,121 @@ test('statusline honors NO_COLOR with no ANSI escapes in output', () => {
194
205
  assert.equal(run.status, 0, run.stderr);
195
206
  assert.doesNotMatch(run.stdout, /\[/);
196
207
  });
208
+
209
+ test('statusline default is one labelled line and narrow terminals drop the least useful parts first', () => {
210
+ const payload = basePayload({ rate_limits: rateLimits(), cost: { total_lines_added: 120, total_lines_removed: 30 } });
211
+ const plainLines = (run) => visible(run.stdout).trimEnd().split('\n');
212
+ const runtime = { statusline: 'full', statuslineColors: false };
213
+
214
+ const wide = runStatus({ runtime, payload });
215
+ assert.equal(wide.status, 0, wide.stderr);
216
+ assert.deepEqual(plainLines(wide), ['◆ TestModel ◑ 53% ⧗ 20% <reset> ◷ 45% ± +120/-30']);
217
+
218
+ const narrow = runStatus({ runtime, payload, envExtra: { COLUMNS: '34' } });
219
+ assert.deepEqual(plainLines(narrow), ['◆ TestModel ◑ 53% ⧗ 20% <reset>']);
220
+ const tiny = runStatus({ runtime, payload, envExtra: { COLUMNS: '10' } });
221
+ assert.deepEqual(plainLines(tiny), ['◆ TestModel ◑ 53%']);
222
+ });
223
+
224
+ test('statusline compact is the default line alone and minimal keeps model and context', () => {
225
+ const payload = basePayload({ rate_limits: rateLimits() });
226
+ const plain = (run) => visible(run.stdout).trimEnd();
227
+ const compact = runStatus({ runtime: { statusline: 'compact', statuslineColors: false }, payload });
228
+ assert.equal(plain(compact), '◆ TestModel ◑ 53% ⧗ 20% <reset> ◷ 45%');
229
+ const minimal = runStatus({ runtime: { statusline: 'minimal', statuslineColors: false }, payload });
230
+ assert.equal(plain(minimal), '◆ TestModel ◑ 53%');
231
+ });
232
+
233
+ test('statusline colours percentages by threshold and prints no escape when statuslineColors is false', () => {
234
+ const hot = basePayload({ rate_limits: rateLimits({ fiveHour: 90 }) });
235
+ const coloured = runStatus({ payload: hot });
236
+ assert.match(coloured.stdout, /\x1b\[31m90%/, 'a five-hour quota at 90% is red');
237
+ assert.match(coloured.stdout, /\x1b\[32m◑[ \u00A0]53%/, 'context at 53% is green');
238
+ const plain = runStatus({ runtime: { statusline: 'full', statuslineColors: false }, payload: hot });
239
+ assert.doesNotMatch(plain.stdout, /\x1b\[/);
240
+ });
241
+
242
+ test('statusline shows the live effort level next to the model, and nothing when the payload has none', () => {
243
+ const runtime = { statusline: 'minimal', statuslineColors: false };
244
+ const withEffort = runStatus({ runtime, payload: basePayload({ effort: { level: 'xhigh' } }) });
245
+ assert.equal(visible(withEffort.stdout).trimEnd(), '◆ TestModel (xhigh) ◑ 53%');
246
+ const narrow = runStatus({ runtime: { statusline: 'full', statuslineColors: false }, payload: basePayload({ effort: { level: 'high' } }), envExtra: { COLUMNS: '10' } });
247
+ assert.equal(visible(narrow.stdout).trimEnd(), '◆ TestModel (high) ◑ 53%', 'effort travels with the model and never drops');
248
+ const without = runStatus({ runtime });
249
+ assert.equal(visible(without.stdout).trimEnd(), '◆ TestModel ◑ 53%');
250
+ });
251
+
252
+ test('statusline reads quota from the payload rate_limits', () => {
253
+ const nowSec = Math.floor(Date.now() / 1000);
254
+ const payload = basePayload({ rate_limits: {
255
+ five_hour: { used_percentage: 36, resets_at: nowSec + 2 * 3600 + 12 * 60 + 30 },
256
+ seven_day: { used_percentage: 43, resets_at: nowSec + 2 * 86400 },
257
+ } });
258
+ const run = runStatus({ runtime: { statusline: 'compact', statuslineColors: false }, payload,
259
+ tmpFiles: { 'ck-usage-limits-cache.json': freshUsageCache() } });
260
+ assert.equal(visible(run.stdout).trimEnd(), '◆ TestModel ◑ 53% ⧗ 36% <reset> ◷ 43%', 'the payload is the only source (the cache says 20%/45%)');
261
+ });
262
+
263
+ test('statusline layout quota section renders from the payload', () => {
264
+ const run = runStatus({
265
+ runtime: { statusline: 'full', statuslineColors: false, statuslineLayout: { lines: [['quota']] } },
266
+ payload: basePayload({ rate_limits: rateLimits({ fiveHour: 36, weekly: 43 }) }),
267
+ });
268
+ assert.equal(run.status, 0, run.stderr);
269
+ // Not visible(): its mask matches only \d+h\d+m, and the layout prints "2h 29m" plus a
270
+ // weekly countdown that can roll over between seconds.
271
+ const line = run.stdout.replace(ANSI, '').replace(NBSP, ' ').trimEnd();
272
+ assert.match(line, /⌛ \d+h \d+m left \(36% used\) wk 43% \(\d+d \d+h\)/, line);
273
+ });
274
+
275
+ /** A throwaway git repo on branch `feat`, so git, PR and worktree marks have a branch to ride on. */
276
+ function gitRepo() {
277
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'sl-git-'));
278
+ cleanupDirs.push(dir);
279
+ for (const args of [['init', '-q', '-b', 'feat'], ['-c', 'user.email=t@t', '-c', 'user.name=t', 'commit', '-q', '--allow-empty', '-m', 'init']]) {
280
+ const r = spawnSync('git', ['-C', dir, ...args], { encoding: 'utf8' });
281
+ assert.equal(r.status, 0, r.stderr);
282
+ }
283
+ return dir;
284
+ }
285
+
286
+ const stripAnsi = (text) => text.replace(ANSI, '').replace(NBSP, ' ').trimEnd();
287
+
288
+ test('statusline shows fast mode, thinking, PR state, worktree, session time and cache hit only when present', () => {
289
+ const runtime = { statusline: 'compact', statuslineColors: false };
290
+ const repo = gitRepo();
291
+ const rich = basePayload({
292
+ fast_mode: true,
293
+ thinking: { enabled: true },
294
+ pr: { number: 80, review_state: 'approved' },
295
+ workspace: { current_dir: repo, git_worktree: 'feat-x' },
296
+ cost: { total_duration_ms: 28147114 },
297
+ prompt_cache: { requests: 77, hit_ratio: 0.986 },
298
+ });
299
+ assert.equal(stripAnsi(runStatus({ runtime, payload: rich }).stdout), '◆ TestModel ϟ✱ ◑ 53% ⎇ feat #80✓ ⊟ ⏱ 7h49m ↻ 99%');
300
+
301
+ const off = basePayload({
302
+ fast_mode: false, thinking: { enabled: false }, workspace: { current_dir: repo },
303
+ cost: { total_duration_ms: 30000 }, prompt_cache: { requests: 0, hit_ratio: 0 },
304
+ });
305
+ assert.equal(stripAnsi(runStatus({ runtime, payload: off }).stdout), '◆ TestModel ◑ 53% ⎇ feat');
306
+ });
307
+
308
+ test('statusline marks each PR review state and a GitLab merge request', () => {
309
+ const runtime = { statusline: 'minimal', statuslineColors: true };
310
+ const repo = gitRepo();
311
+ const line = (pr) => runStatus({ runtime: { statusline: 'compact', statuslineColors: true }, payload: basePayload({ pr, workspace: { current_dir: repo } }) }).stdout;
312
+ assert.match(line({ number: 7, review_state: 'approved' }), /#7\x1b\[32m✓/);
313
+ assert.match(line({ number: 7, review_state: 'changes_requested' }), /#7\x1b\[31m✗/);
314
+ assert.match(line({ number: 7, review_state: 'draft' }), /\x1b\[2m#7\x1b\[0m/);
315
+ assert.match(stripAnsi(line({ number: 7, review_state: 'pending' })), /⎇ feat #7$/);
316
+ assert.match(stripAnsi(line({ number: 12, kind: 'mr' })), /⎇ feat !12$/);
317
+ assert.doesNotMatch(runStatus({ runtime, payload: basePayload({ pr: { number: 7 }, workspace: { current_dir: repo } }) }).stdout, /#7/, 'minimal keeps only the branch name');
318
+ });
319
+
320
+ test('statusline drops session time and cache before changes on a narrow terminal', () => {
321
+ const payload = basePayload({ cost: { total_duration_ms: 28147114, total_lines_added: 5, total_lines_removed: 1 }, prompt_cache: { requests: 3, hit_ratio: 0.4 } });
322
+ const runtime = { statusline: 'full', statuslineColors: false };
323
+ assert.equal(stripAnsi(runStatus({ runtime, payload, envExtra: { COLUMNS: '34' } }).stdout), '◆ TestModel ◑ 53% ± +5/-1');
324
+ assert.equal(stripAnsi(runStatus({ runtime, payload, envExtra: { COLUMNS: '44' } }).stdout), '◆ TestModel ◑ 53% ± +5/-1 ⏱ 7h49m');
325
+ });
@@ -59,6 +59,7 @@ try {
59
59
  const baseDir = agentCwd;
60
60
  const plansPath = path.join(baseDir, runtime.paths?.plans || 'plans');
61
61
  const docsPath = path.join(baseDir, runtime.paths?.docs || 'docs');
62
+ const specsPath = path.join(baseDir, runtime.paths?.specs || 'specs');
62
63
 
63
64
  // Build context block
64
65
  const lines = [];
@@ -76,7 +77,7 @@ try {
76
77
  const venv = resolveVenv(agentCwd);
77
78
 
78
79
  lines.push('## Rules');
79
- lines.push(`- Plans → ${plansPath}/ | Docs → ${docsPath}/`);
80
+ lines.push(`- Specs → ${specsPath}/ | Plans → ${plansPath}/ | Docs → ${docsPath}/`);
80
81
  if (venv) {
81
82
  lines.push(`- Python in ${skillsDir()}/: use \`${venv}\``);
82
83
  lines.push('- Never use global pip install');
@@ -13,8 +13,6 @@ const os = require('os');
13
13
  const path = require('path');
14
14
  const { execSync } = require('child_process');
15
15
 
16
- // Usage cache file path (written by usage.cjs hook)
17
- const USAGE_CACHE_FILE = path.join(os.tmpdir(), 'ck-usage-limits-cache.json');
18
16
  const WARN_THRESHOLD = 70;
19
17
  const CRITICAL_THRESHOLD = 90;
20
18
  const {
@@ -214,51 +212,6 @@ function buildSessionSection(staticEnv = {}) {
214
212
  ];
215
213
  }
216
214
 
217
- /**
218
- * Read usage limits from cache file (written by usage.cjs)
219
- * @returns {Object|null} Usage data or null if unavailable
220
- */
221
- function readUsageCache() {
222
- try {
223
- if (fs.existsSync(USAGE_CACHE_FILE)) {
224
- const cache = JSON.parse(fs.readFileSync(USAGE_CACHE_FILE, 'utf-8'));
225
- // Cache is valid for 5 minutes for injection purposes
226
- if (Date.now() - cache.timestamp < 300000 && cache.data) {
227
- return cache.data;
228
- }
229
- }
230
- } catch { }
231
- return null;
232
- }
233
-
234
- /**
235
- * Format time until reset
236
- * @param {string} resetAt - ISO timestamp
237
- * @returns {string|null} Formatted time or null
238
- */
239
- function formatTimeUntilReset(resetAt) {
240
- if (!resetAt) return null;
241
- const resetTime = new Date(resetAt);
242
- const remaining = Math.floor(resetTime.getTime() / 1000) - Math.floor(Date.now() / 1000);
243
- if (remaining <= 0 || remaining > 18000) return null; // Only show if < 5 hours
244
- const hours = Math.floor(remaining / 3600);
245
- const mins = Math.floor((remaining % 3600) / 60);
246
- return `${hours}h ${mins}m`;
247
- }
248
-
249
- /**
250
- * Format percentage with warning level
251
- * @param {number} value - Percentage value
252
- * @param {string} label - Label prefix
253
- * @returns {string} Formatted string with warning if applicable
254
- */
255
- function formatUsagePercent(value, label) {
256
- const pct = Math.round(value);
257
- if (pct >= CRITICAL_THRESHOLD) return `${label}: ${pct}% [CRITICAL]`;
258
- if (pct >= WARN_THRESHOLD) return `${label}: ${pct}% [WARNING]`;
259
- return `${label}: ${pct}%`;
260
- }
261
-
262
215
  /**
263
216
  * Build context window section from statusline cache
264
217
  * @param {string} sessionId - Session ID
@@ -297,43 +250,6 @@ function buildContextSection(sessionId) {
297
250
  }
298
251
  }
299
252
 
300
- /**
301
- * Build usage section from cache
302
- * @returns {string[]} Lines for usage section
303
- */
304
- function buildUsageSection() {
305
- const usage = readUsageCache();
306
- if (!usage) return [];
307
-
308
- const lines = [];
309
- const parts = [];
310
-
311
- // 5-hour limit
312
- if (usage.five_hour) {
313
- const util = usage.five_hour.utilization;
314
- if (typeof util === 'number') {
315
- parts.push(formatUsagePercent(util, '5h'));
316
- }
317
- const timeLeft = formatTimeUntilReset(usage.five_hour.resets_at);
318
- if (timeLeft) {
319
- parts.push(`resets in ${timeLeft}`);
320
- }
321
- }
322
-
323
- // 7-day limit
324
- if (usage.seven_day?.utilization != null) {
325
- parts.push(formatUsagePercent(usage.seven_day.utilization, '7d'));
326
- }
327
-
328
- if (parts.length > 0) {
329
- lines.push(`## Usage Limits`);
330
- lines.push(`- ${parts.join(' | ')}`);
331
- lines.push(``);
332
- }
333
-
334
- return lines;
335
- }
336
-
337
253
  /**
338
254
  * Build rules section
339
255
  * @param {Object} params
@@ -489,13 +405,11 @@ function buildReminder(params) {
489
405
  // Respect hooks config — skip sections when their corresponding hook is disabled
490
406
  const hooksConfig = hooks || {};
491
407
  const contextEnabled = hooksConfig['context-tracking'] !== false;
492
- const usageEnabled = hooksConfig['usage'] !== false;
493
408
 
494
409
  return [
495
410
  ...buildLanguageSection({ thinkingLanguage, responseLanguage }),
496
411
  ...buildSessionSection(staticEnv),
497
412
  ...(contextEnabled ? buildContextSection(sessionId) : []),
498
- ...(usageEnabled ? buildUsageSection() : []),
499
413
  ...buildRulesSection({ devRulesPath, catalogScript, skillsVenv, plansPath, docsPath }),
500
414
  ...buildModularizationSection(),
501
415
  ...buildPathsSection({ reportsPath, plansPath, docsPath, docsMaxLoc }),
@@ -564,7 +478,6 @@ function buildReminderContext({ sessionId, config, staticEnv, configDirName = '.
564
478
  // Respect hooks config for sections object too
565
479
  const hooksConfig = cfg.hooks || {};
566
480
  const contextEnabled = hooksConfig['context-tracking'] !== false;
567
- const usageEnabled = hooksConfig['usage'] !== false;
568
481
 
569
482
  return {
570
483
  content: lines.join('\n'),
@@ -573,7 +486,6 @@ function buildReminderContext({ sessionId, config, staticEnv, configDirName = '.
573
486
  language: buildLanguageSection({ thinkingLanguage: params.thinkingLanguage, responseLanguage: params.responseLanguage }),
574
487
  session: buildSessionSection(staticEnv),
575
488
  context: contextEnabled ? buildContextSection(sessionId) : [],
576
- usage: usageEnabled ? buildUsageSection() : [],
577
489
  rules: buildRulesSection({ devRulesPath, catalogScript, skillsVenv, plansPath: params.plansPath, docsPath: params.docsPath }),
578
490
  modularization: buildModularizationSection(),
579
491
  paths: buildPathsSection({ reportsPath: params.reportsPath, plansPath: params.plansPath, docsPath: params.docsPath, docsMaxLoc: params.docsMaxLoc }),
@@ -596,7 +508,6 @@ module.exports = {
596
508
  buildLanguageSection,
597
509
  buildSessionSection,
598
510
  buildContextSection,
599
- buildUsageSection,
600
511
  buildRulesSection,
601
512
  buildModularizationSection,
602
513
  buildPathsSection,
@@ -54,8 +54,8 @@ const EVENT_NAMES = Object.freeze({
54
54
  * Grok's registry and matcher aliases come first, then omp's lowercase registry. Where
55
55
  * both hosts use a spelling (`write`, `grep`) they agree on the destination. `write` is
56
56
  * grok's file-creating tool and maps to `Write`, while `search_replace` edits an existing
57
- * file and maps to `Edit`; conflating them would make the scaffold guard reject ordinary
58
- * edits. `delete_file` maps to `Write` so a delete is gated like any other file write.
57
+ * file and maps to `Edit`, which keeps grok's file creation distinct from an edit.
58
+ * `delete_file` maps to `Write` so a delete is gated like any other file write.
59
59
  */
60
60
  const TOOL_ALIASES = Object.freeze({
61
61
  // grok — every spelling that reaches a Bash matcher