company-skill 4.6.8 → 4.6.9

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.
@@ -1,7 +1,11 @@
1
1
  # My Company
2
2
 
3
3
  <!--
4
- INSTRUCTIONS:
4
+ OPTIONAL hint pool. /company runs fine with NO COMPANY.md and derives the team
5
+ from your goal. The active roster is goal-derived, not staffed from this file.
6
+ Use this only to bias role names, pin a model to a role, or seed the dashboard pool.
7
+
8
+ INSTRUCTIONS (optional):
5
9
  1. Rename this file to COMPANY.md
6
10
  2. Fill in your departments and roles
7
11
  3. Add/remove departments as needed
@@ -18,6 +18,13 @@
18
18
  # every inheriting role uses (lead, reviewer, critic inherit by
19
19
  # omission, so the session choice propagates automatically).
20
20
  #
21
+ # CHEAP Every Agent call passes haiku and MODEL: tags are ignored, so
22
+ # strong, mid and cheap all resolve to the cheap family. The whole
23
+ # run lands on the cheapest tier. Because the session family is
24
+ # effectively the cheap tier, the orchestrator opens its briefing
25
+ # and chat reply with the weak-verify-layer warning. Use it only
26
+ # for bulk low-stakes work, and flip back to TIERED when done.
27
+ #
21
28
  # A missing or unparseable file means TIERED. Never write a versioned model
22
29
  # name here, only the policy word. The launch-time alternative is the env
23
30
  # var CLAUDE_CODE_SUBAGENT_MODEL, which forces every sub-agent to a named
package/README.md CHANGED
@@ -24,9 +24,9 @@ Reach for it when "looks done" is not good enough and you want the loop to finis
24
24
  Compared to a single agent or a plain prompt loop, the difference is the gate. A single agent stops when it decides it is finished. A company of agents keeps running, re-checking its own claims each cycle, until the evidence is there. It does not invent work outside the goal, and it tells you plainly when it is blocked.
25
25
 
26
26
  <p align="center">
27
- <img src="assets/dashboard.png" alt="Company dashboard showing org tree, context gauge, active agents, and criteria" width="900">
27
+ <img src="assets/dashboard.png" alt="Company dashboard showing a tokens box and a separate cost box with a model-policy toggle, a horizontal cycles-and-memory row, context gauge, active agents, the delegation tree, and criteria" width="900">
28
28
  <br>
29
- <em>Live dashboard: org tree, context gauge, agent table, and criteria checklist - auto-starts with every /company run.</em>
29
+ <em>Live dashboard: a tokens box and a separate cost box (cache savings, hit rate and cycles live with the dollars), a horizontal cycles-and-memory row, context gauge, agent table, delegation tree, and criteria checklist - auto-starts with every /company run.</em>
30
30
  </p>
31
31
 
32
32
  ```bash
@@ -72,21 +72,23 @@ The dashboard starts automatically when you run `/company` and prints its URL in
72
72
  http://127.0.0.1:7421 <- your session's link, printed at startup and in the status bar
73
73
  ```
74
74
 
75
- A per-session identity header sits at the top: the project, the session id, the active model, and an "All projects" link to the cross-project roll-up.
75
+ A per-session identity header sits at the top: the project, the session id, the active model, and an "All projects" link to the cross-project roll-up. The project name is read from your `.company` directory, so two companies launched from the same folder stay labelled apart.
76
76
 
77
- What you see, panel by panel:
77
+ Every block is a card with the same tile look, so cost, cycles, agents, and criteria all read the same way. What you see, panel by panel:
78
+
79
+ **Cost and usage** - two stacked bands so dollars never interleave with token counts. The Tokens band shows the volumes (input, output, cache-write, this session) as plain counts. The Cost band shows every dollar figure together (today, this session, and "Saved by cheaper models vs all-opus"), with the list-price "not billed" note attached only to the dollars. The dollars come from ccusage at public list prices and are notional on a subscription plan, which the card says plainly. A model-policy toggle sits on the savings tile, reusing the same Apple-style pill as the auto-restart control: off is adaptive (cheaper models run sub-tasks for more savings), on forces the best model everywhere. It writes `.company/MODEL_POLICY` (`TIERED` or `FORCE_BEST`), the file the orchestrator reads at the start of each cycle, so the change applies next cycle.
78
80
 
79
81
  **Context fill** - the live fill percentage, computed with the same formula the context-guard uses. When the session hits the restart threshold (default 50%), the gauge marks the gate before it fires. Next to it sits an Apple-style auto-restart pill, locked on - the restart block is always enforced and the toggle cannot turn it off.
80
82
 
81
- **Delegation tree** - SVG tree of orchestrator, department leads, and workers, with org-chart context filled from COMPANY.md. Click any node to expand its current task and status. Zoom only with the pill-shaped +/- buttons, or use the expand button to blow the tree up to fill the screen and the contract button to bring it back. Drag to pan. A refresh resets the tree to its default view. Zero external JS libraries.
83
+ **Delegation tree** - SVG tree of orchestrator, department leads, and workers, showing only the agents running right now. COMPANY.md is the source pool and the activated roster decides which departments are eligible, but a role node paints only when a currently-active agent maps to it, and a department appears only when at least one of its roles is live, so a finished or stale agent leaves no node behind. The orchestrator (CEO) root always shows. Long role names wrap inside their node instead of spilling out. Click any node to expand its current task and status. Zoom only with the pill-shaped +/- buttons, or use the expand button to blow the tree up to fill the screen and the contract button to bring it back. Drag to pan. A refresh resets the tree to its default view. Zero external JS libraries.
82
84
 
83
- **Cycles and memory** - a savings card that shows human cycle and memory counts plus the dollars saved by model tiering and prompt caching, each marked approximate.
85
+ **Cycles and memory** - a savings card that shows cycle and memory counts plus the cache-read volume and the dollars saved by prompt caching, marked approximate. The model-tiering saving lives in the Cost band above, next to the policy toggle.
84
86
 
85
87
  **Active agents** - centered live table of every agent the orchestrator has spawned this session, with model, status, and token count.
86
88
 
87
89
  **Criteria** - compact progress view with a click-to-expand toggle for the full pass/fail list and reproduced evidence.
88
90
 
89
- **All projects** - the "All projects" link near the session header opens `/all`, a cross-project roll-up. It reads `~/.claude/company-dashboards.json`, a small index every dashboard writes itself into, and shows aggregate cost, tokens, and cache reuse grouped by project. It lists only dashboards seen in the last 5 minutes, costs come from ccusage and are notional on a subscription plan, and a session whose usage row is missing shows `?` rather than zero. Each row links back to that session's own dashboard.
91
+ **All projects** - the "All projects" link near the session header opens `/all`, a cross-project roll-up. It reads `~/.claude/company-dashboards.json`, a small index every dashboard writes itself into, and shows aggregate cost, tokens, and cache reuse grouped by project. It lists only dashboards seen in the last 5 minutes, costs come from ccusage and are notional on a subscription plan, and a session whose usage row is missing shows `?` rather than zero. Each row links back to that session's own dashboard. A dashboard whose owning session has ended drops itself from the index, so `/all` never lists a stale one.
90
92
 
91
93
  The dashboard binds 127.0.0.1 only, reads local files, and sends nothing anywhere. Override the port with `COMPANY_DASHBOARD_PORT`.
92
94
 
@@ -25,6 +25,7 @@ Probe checklist, applied to every passing criterion and every merged-or-mergeabl
25
25
  9. ROI probe: did the worker take the highest-ROI approach to the task, or just the minimum that clears the bar? A trivially better approach within the same scope is a soft flag. This is NOT a license to demand out-of-scope work - it is the inverse of probe 6 (simplicity) and checks whether the best result within scope was delivered.
26
26
  10. Anti-vacuous test (SKILL.md ANTI-VACUOUS TEST): does the new test FAIL against the pre-change code? If the test passes unconditionally (before and after the fix), it is vacuous - REJECT.
27
27
  11. Feature reachability (SKILL.md FEATURE REACHABILITY): for any feature that gates on a field or condition, is there an authoring or runtime path that sets that field? Probe by reading the skill/agent authoring instructions - if no instruction tells the orchestrator to write the gating field, the feature is dead - REJECT.
28
+ 12. Parallelization (SKILL.md CONCURRENCY FLOOR): read the cycle review's REALIZED-WIDTH against the briefing's PARALLEL-WIDTH. If independent contracts existed (DEPENDS-ON: none) but the realized width fell materially below that independent-contract count, or independent agents took a blocking join with no named downstream consumer, flag UNDER-PARALLELIZED with the count gap. This is a SOFT flag, not a hard REJECT: correct work shipped serially is slower than the plan declared, not wrong, and a hard block would punish a harness-cap-limited run. Record it in the review so the next cycle's effort scaling corrects it.
28
29
 
29
30
  Audit each probe claim against a tool result from THIS session. Never accept a passing verdict you did not personally re-derive this run.
30
31
 
@@ -18,6 +18,8 @@ Your prompt names the finished cycle's findings files, its review file (`.compan
18
18
  5. The review's feedback for the next cycle.
19
19
  6. Append this cycle's FAILED -> USE INSTEAD and INEFFICIENT -> FASTER lessons to `.company/playbook.md` now. Dedup gate: grep the playbook for the lesson's key tokens first. On a hit, update the existing line (append "seen again {date}") instead of appending a near-duplicate.
20
20
  7. Cost line: run `npx ccusage@latest session --id "$CLAUDE_CODE_SESSION_ID" --json` (if it fails for any reason, write `COST: unavailable` and continue), write `.company/cycles/cycle-{N}-cost.json` (totalCost, totalTokens), and put a one-line `COST:` delta in the briefing. Never paste the raw JSON anywhere.
21
+ 8. PARALLEL-DEBT: compare the review's REALIZED-WIDTH against the briefing's planned PARALLEL-WIDTH. When realized fell below planned for 2 consecutive cycles with no harness-cap reason, write `PARALLEL-DEBT: {gap}` into the next briefing so the next THINK must explain or fix it.
22
+ 9. PLAYBOOK-HITS: count the `PLAYBOOK-CITED` tags in the cycle review and write `PLAYBOOK-HITS: {n}` into the next briefing. After two goals with zero hits, add a line that the playbook is not influencing planning.
21
23
 
22
24
  Never drop a SOURCE line when compressing an importance 4-5 finding, and never write a pointer whose anchor does not appear in the file it points to. A compressed claim without its source is unverifiable and worse than dropping the claim. Never editorialize and never add new claims.
23
25
 
@@ -30,8 +30,10 @@ Rules that bind you:
30
30
  - No command, no task. If you cannot write a VERIFY-WITH command (or an equally concrete check, like a named URL to screenshot), the task is not ready and you must not emit it.
31
31
  - ROI is required on every contract. It is your value rationale: why this task over an alternative. State it in one line. After writing all contracts, rank them by ROI and call out that ranking in your reply so the orchestrator sequences waves highest-value-first.
32
32
  - Contracts must be self-contained. Paste the needed playbook lines and paths in. A worker never sees this conversation or the skill text.
33
- - List the surfaces (files, pages, endpoints) each task touches so the orchestrator can dedup. Two of your own tasks must not touch the same surface.
33
+ - List the surfaces (files, pages, endpoints) each task touches so the orchestrator can dedup. Two of your own tasks must not touch the same surface. Define surfaces at the finest safe granularity (a file, an endpoint, a page), never a whole directory or repo. Coarse surfaces collapse independent work into one serialized worker and narrow the wave.
34
+ - DEPENDS-ON only for a genuine output-to-input need. If task B does not consume task A's produced artifact, B's DEPENDS-ON is `none`. Over-declaring dependencies serializes waves that could run in parallel. Default DEPENDS-ON to none and justify any edge you add in one line.
34
35
  - If you see a skill gap on your team, add a line `HIRE: {role}, {why}`.
36
+ - If a pasted playbook lesson shaped a contract's plan, tag that contract `PLAYBOOK-CITED: {lesson anchor}` so the digest can count playbook usage (PLAYBOOK-HITS) and surface a playbook that has stopped influencing planning.
35
37
  - If a needed check or fact is missing, you may use Read, Grep, Bash, or WebFetch to inspect state before writing contracts. Verify external facts before baking them into a contract. Never write a contract around a guess.
36
38
  - **Tool-use heuristics.** Grep/Bash for local state, WebFetch for a known URL, WebSearch when you
37
39
  do not know the URL. Make independent lookups in parallel. Read only the slice you need.
@@ -15,11 +15,12 @@ Execution rules, all binding:
15
15
  - **Scope.** Do ONLY the assigned task. Respect OUT-OF-SCOPE literally. Adjacent problems get one line in your findings (`ALSO-FOUND: ...`) and nothing else. Never fix unbidden. For genuinely high-leverage opportunities spotted during the work, add: `PROPOSE: {opportunity} - ROI: {why high value}`. The orchestrator triages it at the next THINK. Surface it, do not execute it.
16
16
  - **Maximize within scope.** Within the assigned task, deliver the best-achievable result, not the literal minimum that clears DONE-WHEN. If a higher-ROI approach to the SAME task exists (same surfaces, same scope), take it. Example: if the contract says "fix the bug", also add a regression test if one is trivially missing - that is best-achievable on the same surface, not scope creep.
17
17
  - **Skill first.** If the contract assigns a skill, invoke it via the Skill tool before anything else. If it is not installed, fall back to raw tools and note `SKILL-MISSING` in your findings. Never loop retrying a skill that does not exist.
18
- - **Git isolation.** If the task touches a repo: work in your own worktree on your own branch (`git worktree add ../wt-{task-id} -b company/{task-id}`), commit there, push the branch, open a DRAFT PR. NEVER commit to a shared checkout, NEVER push to main, NEVER merge anything. Merging happens after review, by the orchestrator, not by you. Every draft PR body ends with a `Proof of work` block: the VERIFY-WITH command + its pasted output, the CI link, and the diff stat. Evidence stays verbatim inside the block, no humanizing.
18
+ - **Git isolation.** If the task touches a repo: work in your own worktree on your own branch (`git worktree add "${COMPANY_DIR:-$HOME/.company}/worktrees/{repo-name}/{task-id}" -b company/{task-id}`, the grouped worktree home, never a cryptic `../wt-*` sibling that clutters the repo list), commit there, push the branch, open a DRAFT PR. NEVER commit to a shared checkout, NEVER push to main, NEVER merge anything. Merging happens after review, by the orchestrator, not by you. Every draft PR body ends with a `Proof of work` block: the VERIFY-WITH command + its pasted output, the CI link, and the diff stat. Evidence stays verbatim inside the block, no humanizing.
19
19
  - **Run your check.** Before reporting done, run the contract's VERIFY-WITH command and paste its real output in your findings. If the output does not prove DONE-WHEN, you are not done.
20
20
  - **EXTERNAL FACT RULE (highest priority).** Before writing ANY public-facing output (GitHub comments, PR descriptions, emails, posts) that states a specific fact about an external project (versions, APIs, features, architecture), verify it first with WebFetch or `gh api` against their actual docs, source, or README. If you cannot verify, write "not sure" instead of guessing. Never cite external numbers from memory. ONE STRIKE: if corrected, post a one-line factual correction and stop. Never argue and never guess a second time.
21
21
  - **Blocked is a result.** If the task is impossible or blocked, report `BLOCKED: reason + what would unblock it`. Never return nothing and never expand scope to compensate.
22
22
  - **Ask, don't guess.** If the contract is executable but ambiguous on a point that changes the output, do not guess: report `BLOCKED: NEEDS-SPEC: {one concrete question}` with `STATUS: blocked` and stop. One question, not a list.
23
+ - **Pre-act checkpoint.** If your criterion is stakes: high AND the action is irreversible (the VERIFY-WITH has side effects, or you are about to publish, deploy, or delete), emit `CHECKPOINT: about to {action} on {target}; PLAN: {1 line}; ROLLBACK: {1 line or NONE}; CONFIRM?` and STOP before the irreversible step. The orchestrator confirms or returns a corrected plan, same as NEEDS-SPEC. Do not fire a one-way door before the confirm.
23
24
  - **Long waits.** For CI, builds, or deploys, start a background watcher and read its output. Never blind-sleep and never assume success. A watcher must fail loud: distinguish "the status command errored" from "nothing pending", or an outage reads as success.
24
25
  - **You cannot spawn agents.** You are a leaf: the platform gives sub-agents no agent-spawning tool. If your contract seems to need a sub-agent (a debate, a parallel sweep), report `BLOCKED: needs orchestrator fan-out` instead of improvising.
25
26
  - **Deferred tools.** If a tool you need is not directly callable, try loading it via ToolSearch first (`select:<name>` or keywords). Only after ToolSearch returns nothing do you report the gap.
package/bin/install.js CHANGED
@@ -24,12 +24,19 @@ const INSTALL_SCRIPTS = [
24
24
  'codegraph.js',
25
25
  'check-contracts.js',
26
26
  'check-findings.js',
27
+ 'check-criteria.js',
28
+ 'check-playbook.js',
27
29
  'restart-debate.js',
28
30
  'dashboard.js',
29
31
  'secret-scan.js',
30
32
  'reset-company-guard.js',
31
33
  'cleanup.js',
32
34
  'statusline.js',
35
+ 'workspace-guard.js',
36
+ 'migrate-state.js',
37
+ 'check-isolation.js',
38
+ 'check-roster.js',
39
+ 'clean-stale-dashboards.js',
33
40
  ];
34
41
  const scriptsDestDir = path.join(skillDir, 'scripts');
35
42
  for (const script of INSTALL_SCRIPTS) {
@@ -51,7 +58,12 @@ const hookFiles = {
51
58
  'stop-guard.js': 'company-stop-guard.js',
52
59
  'context-guard.js': 'company-context-guard.js',
53
60
  'precompact.js': 'company-precompact.js',
54
- 'session-restore.js': 'company-session-restore.js'
61
+ 'session-restore.js': 'company-session-restore.js',
62
+ 'intake-log.js': 'company-intake-log.js',
63
+ 'company-prepush-guard.js': 'company-prepush-guard.js',
64
+ 'orchestrator-guard.js': 'company-orchestrator-guard.js',
65
+ 'post-merge-cleanup.js': 'company-post-merge-cleanup.js',
66
+ 'owner-bootstrap.js': 'company-owner-bootstrap.js'
55
67
  };
56
68
 
57
69
  for (const [src, dest] of Object.entries(hookFiles)) {
@@ -85,12 +97,41 @@ try {
85
97
  if (!settings.hooks.SessionStart.some(h => h.hooks?.some(hh => hh.command?.includes('company-session-restore')))) {
86
98
  settings.hooks.SessionStart.push({ matcher: 'compact', hooks: [{ type: 'command', command: `node "${path.join(hooksDir, 'company-session-restore.js')}"`, timeout: 10 }] });
87
99
  }
100
+ // SessionStart on a fresh start or resume: bootstrap OWNER so the owning session is
101
+ // recorded mechanically. Fail-open, idempotent, only writes inside an active run.
102
+ if (!settings.hooks.SessionStart.some(h => h.hooks?.some(hh => hh.command?.includes('company-owner-bootstrap')))) {
103
+ settings.hooks.SessionStart.push({ matcher: 'startup|resume', hooks: [{ type: 'command', command: `node "${path.join(hooksDir, 'company-owner-bootstrap.js')}"`, timeout: 10 }] });
104
+ }
105
+
106
+ if (!settings.hooks.UserPromptSubmit) settings.hooks.UserPromptSubmit = [];
107
+ if (!settings.hooks.UserPromptSubmit.some(h => h.hooks?.some(hh => hh.command?.includes('company-intake-log')))) {
108
+ settings.hooks.UserPromptSubmit.push({ hooks: [{ type: 'command', command: `node "${path.join(hooksDir, 'company-intake-log.js')}"`, timeout: 10 }] });
109
+ }
110
+
111
+ // PreToolUse: mechanical secret-scan-before-push gate. Matcher 'Bash' so it only
112
+ // fires on shell commands; the hook itself filters to git push / gh pr create|ready.
113
+ if (!settings.hooks.PreToolUse) settings.hooks.PreToolUse = [];
114
+ if (!settings.hooks.PreToolUse.some(h => h.hooks?.some(hh => hh.command?.includes('company-prepush-guard')))) {
115
+ settings.hooks.PreToolUse.push({ matcher: 'Bash', hooks: [{ type: 'command', command: `node "${path.join(hooksDir, 'company-prepush-guard.js')}"`, timeout: 15 }] });
116
+ }
117
+
118
+ // PostToolUse: lean-orchestrator advisory guard. Warns (never blocks) when the owner
119
+ // session loads oversized content inline instead of delegating it to a worker.
120
+ if (!settings.hooks.PostToolUse) settings.hooks.PostToolUse = [];
121
+ if (!settings.hooks.PostToolUse.some(h => h.hooks?.some(hh => hh.command?.includes('company-orchestrator-guard')))) {
122
+ settings.hooks.PostToolUse.push({ hooks: [{ type: 'command', command: `node "${path.join(hooksDir, 'company-orchestrator-guard.js')}"`, timeout: 10 }] });
123
+ }
124
+ // PostToolUse: post-merge cleanup reminder. Fires after a Bash gh pr merge to nudge
125
+ // the orchestrator to prune the merged branch + worktree. Advisory, fail-open.
126
+ if (!settings.hooks.PostToolUse.some(h => h.hooks?.some(hh => hh.command?.includes('company-post-merge-cleanup')))) {
127
+ settings.hooks.PostToolUse.push({ matcher: 'Bash', hooks: [{ type: 'command', command: `node "${path.join(hooksDir, 'company-post-merge-cleanup.js')}"`, timeout: 10 }] });
128
+ }
88
129
 
89
130
  // Atomic write: a crash mid-write must not corrupt the user's settings.
90
131
  const tmpPath = settingsPath + '.tmp';
91
132
  fs.writeFileSync(tmpPath, JSON.stringify(settings, null, 2));
92
133
  fs.renameSync(tmpPath, settingsPath);
93
- console.log('Hooks installed: Stop guard + Context guard + PreCompact + SessionStart restore');
134
+ console.log('Hooks installed: Stop guard + Context guard + PreCompact + SessionStart restore + intake log + PreToolUse prepush guard');
94
135
  } catch (e) {
95
136
  console.log('Could not register hooks. Add manually to settings.json.');
96
137
  }
@@ -0,0 +1,110 @@
1
+ #!/usr/bin/env node
2
+
3
+ // PreToolUse hook: mechanical secret-scan-before-push gate. The SKILL.md rule to run
4
+ // scripts/secret-scan.js before every push is prose-only, so a weak model that forgets
5
+ // it can push secrets ungated. This hook enforces it in the harness, not the model: it
6
+ // matches Bash commands that publish (git push, gh pr create, gh pr ready), runs the
7
+ // secret scanner over the worktree, and DENIES the tool call on a confirmed secret.
8
+ //
9
+ // FAIL-OPEN-EXCEPT-SECRET: a bug in this guard must never brick all pushes, so every
10
+ // error path (bad stdin, missing scanner, scanner crash) ALLOWS the command. The one
11
+ // hard block is a scanner exit 1 (BLOCKED-SECRET): there it fails CLOSED and denies.
12
+ //
13
+ // Output contract (PreToolUse): exit 0 with
14
+ // { hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny'|'allow', permissionDecisionReason } }
15
+ // A bare exit 0 with no JSON also allows (normal permission flow), which is the
16
+ // fail-open default when the command is not a push.
17
+
18
+ const fs = require('fs');
19
+ const path = require('path');
20
+ const { spawnSync } = require('child_process');
21
+
22
+ function allow(reason) {
23
+ // permissionDecision 'allow' lets the call proceed; emit a reason for auditability.
24
+ process.stdout.write(JSON.stringify({
25
+ hookSpecificOutput: {
26
+ hookEventName: 'PreToolUse',
27
+ permissionDecision: 'allow',
28
+ permissionDecisionReason: reason || 'company-prepush-guard: allowed'
29
+ }
30
+ }));
31
+ process.exit(0);
32
+ }
33
+
34
+ function allowSilently() {
35
+ // No JSON: normal permission flow decides. Used for non-push commands so this guard
36
+ // never inserts itself into unrelated tool calls.
37
+ process.exit(0);
38
+ }
39
+
40
+ function deny(reason) {
41
+ process.stdout.write(JSON.stringify({
42
+ hookSpecificOutput: {
43
+ hookEventName: 'PreToolUse',
44
+ permissionDecision: 'deny',
45
+ permissionDecisionReason: reason
46
+ }
47
+ }));
48
+ process.exit(0);
49
+ }
50
+
51
+ // 1. Parse stdin. Any failure => fail open (allow silently).
52
+ let input;
53
+ try {
54
+ input = JSON.parse(fs.readFileSync(0, 'utf8'));
55
+ } catch (e) {
56
+ allowSilently();
57
+ }
58
+
59
+ // 2. Only gate Bash tool calls. Anything else is none of this guard's business.
60
+ if (!input || input.tool_name !== 'Bash') allowSilently();
61
+
62
+ const command = input.tool_input && typeof input.tool_input.command === 'string'
63
+ ? input.tool_input.command : '';
64
+ if (!command) allowSilently();
65
+
66
+ // 3. Match publish commands: git push, gh pr create, gh pr ready. Word-boundary-ish
67
+ // matching so "git pushd" or a path containing "git push" in a comment is unlikely
68
+ // to false-match while real pushes are caught. Fail-open bias: when in doubt, allow.
69
+ const isPush = /\bgit\s+push\b/.test(command) ||
70
+ /\bgh\s+pr\s+create\b/.test(command) ||
71
+ /\bgh\s+pr\s+ready\b/.test(command);
72
+ if (!isPush) allowSilently();
73
+
74
+ // 4. Resolve the worktree to scan. Prefer the cwd the harness reports; else this
75
+ // process cwd. A bad value just scans the wrong-but-harmless dir (fail open).
76
+ const worktree = (input.cwd && typeof input.cwd === 'string') ? input.cwd : process.cwd();
77
+
78
+ // 5. Locate secret-scan.js. In the repo it sits at ../scripts/ relative to this hook;
79
+ // once installed the hook lives in ~/.claude/hooks/ and the scanner in
80
+ // ~/.claude/skills/company/scripts/. Try both; if neither exists, fail OPEN.
81
+ function findScanner() {
82
+ const candidates = [
83
+ path.join(__dirname, '..', 'scripts', 'secret-scan.js'),
84
+ path.join(process.env.HOME || '', '.claude', 'skills', 'company', 'scripts', 'secret-scan.js')
85
+ ];
86
+ for (const c of candidates) {
87
+ try { if (fs.existsSync(c)) return c; } catch (e) {}
88
+ }
89
+ return null;
90
+ }
91
+ const scanner = findScanner();
92
+ if (!scanner) allow('company-prepush-guard: secret-scan.js not found, failing open');
93
+
94
+ // 6. Run the scanner. Exit 1 = confirmed secret => DENY (fail closed). Exit 0 = clean
95
+ // or SCANNER-MISSING => allow. Any other exit (scanner crash) => allow (fail open).
96
+ let r;
97
+ try {
98
+ r = spawnSync(process.execPath, [scanner, '--worktree', worktree], { encoding: 'utf8' });
99
+ } catch (e) {
100
+ allow('company-prepush-guard: scanner failed to spawn, failing open');
101
+ }
102
+
103
+ if (r && r.status === 1) {
104
+ const detail = ((r.stderr || '') + (r.stdout || '')).trim().split('\n').slice(0, 4).join(' ');
105
+ deny('company-prepush-guard BLOCKED-SECRET: secret-scan.js flagged a secret in this worktree. ' +
106
+ 'Do not push. ' + detail);
107
+ }
108
+
109
+ // Clean, SCANNER-MISSING, or any non-1 exit: allow.
110
+ allow('company-prepush-guard: secret scan passed (exit ' + (r ? r.status : 'n/a') + ')');
@@ -103,6 +103,7 @@ function resolveCompanyDir() {
103
103
  const companyDir = resolveCompanyDir();
104
104
  const ownerPath = path.join(companyDir, 'OWNER');
105
105
  const cancelPath = path.join(companyDir, 'CANCEL');
106
+ const handoffPath = path.join(companyDir, 'HANDOFF');
106
107
 
107
108
  // Respect CANCEL: a cancelled run must not be force-restarted.
108
109
  try {
@@ -111,6 +112,15 @@ try {
111
112
  process.exit(0);
112
113
  }
113
114
 
115
+ // A handed-off session is never re-demanded a restart (symmetric with stop-guard).
116
+ try {
117
+ if (sessionId) {
118
+ const handed = fs.readFileSync(handoffPath, 'utf8')
119
+ .split('\n').map(function (l) { return l.trim(); }).filter(Boolean);
120
+ if (handed.indexOf(sessionId) !== -1) process.exit(0);
121
+ }
122
+ } catch (e) {}
123
+
114
124
  // Check session ownership before doing any work.
115
125
  // Null sessionId with a clean OWNER list = unidentifiable session, fail-open (allow).
116
126
  // Null sessionId with no OWNER / garbled OWNER = legacy gate-all mode, fall through.
@@ -159,22 +169,54 @@ try {
159
169
  // No usage found: fail-open.
160
170
  if (!lastUsage) process.exit(0);
161
171
 
172
+ // Token-sum fill must match the dashboard gauge + statusline EXACTLY:
173
+ // input + cache_read + cache_creation + output. Omitting output_tokens made the
174
+ // guard compute a LOWER % than the number the user sees, so a session displayed at
175
+ // >= threshold could sit just under the guard and never fire (criterion 38).
162
176
  const used =
163
177
  (lastUsage.input_tokens || 0) +
164
178
  (lastUsage.cache_read_input_tokens || 0) +
165
- (lastUsage.cache_creation_input_tokens || 0);
179
+ (lastUsage.cache_creation_input_tokens || 0) +
180
+ (lastUsage.output_tokens || 0);
166
181
 
167
182
  const contextWindow = detectWindow(lastModelId);
168
183
  const threshold = parseThreshold();
169
184
  const hardCeiling = parseHardCeiling(threshold);
170
- const fill = used / contextWindow;
185
+
186
+ // Prefer Claude's native context_window.used_percentage from the Stop-hook stdin when
187
+ // present (mirrors statusline.js resolveContextPct: native first, then token-sum). This
188
+ // is the authoritative number the user sees. Native may be ABSENT on Stop hooks, so it
189
+ // is used only when it is a finite number; otherwise fall back to the token-sum fill.
190
+ const nativePct = stdinData.context_window && stdinData.context_window.used_percentage;
191
+ const fill = (typeof nativePct === 'number' && isFinite(nativePct))
192
+ ? nativePct / 100
193
+ : used / contextWindow;
171
194
 
172
195
  if (fill < threshold) process.exit(0);
173
196
 
174
- // At or above the soft threshold the restart block is UNCONDITIONAL.
175
- // No per-session suppression exists: the enforceRestart toggle was removed because
176
- // a session at or above the threshold must always restart (founder-firm invariant).
177
- // The hard ceiling only changes the wording of the block, never whether it fires.
197
+ // Per-session SOFT toggle (founder decision): the dashboard pill can turn OFF the early
198
+ // 50% soft restart for THIS session only. Pref lives in <companyDir>/restart-pref.json as
199
+ // { sessions: { "<sid>": { soft: false } } }; default (absent/true/garbled) = ON.
200
+ // The toggle suppresses ONLY the band between the soft threshold and the hard ceiling.
201
+ // The 80% hard ceiling (parseHardCeiling) stays ALWAYS active even when soft is off, so a
202
+ // runaway session still hard-blocks. Below the hard ceiling, soft:false allows the stop.
203
+ function softRestartOff(dir, sid) {
204
+ if (!sid) return false; // unidentifiable session never reads a per-session pref
205
+ try {
206
+ const raw = fs.readFileSync(path.join(dir, 'restart-pref.json'), 'utf8');
207
+ const parsed = JSON.parse(raw);
208
+ if (!parsed || typeof parsed !== 'object' || !parsed.sessions) return false;
209
+ const entry = parsed.sessions[sid];
210
+ return !!(entry && entry.soft === false);
211
+ } catch (e) {
212
+ return false; // missing or garbled pref = ON (fail toward the restart)
213
+ }
214
+ }
215
+ if (fill < hardCeiling && softRestartOff(companyDir, sessionId)) process.exit(0);
216
+
217
+ // At or above the hard ceiling the restart block is UNCONDITIONAL: the per-session soft
218
+ // toggle above can never reach here. Between soft and hard, an ON session falls through and
219
+ // blocks as before. The hard ceiling only changes the wording of the block, not whether it fires.
178
220
 
179
221
  // Fill is at or above threshold. Check the de-loop state file before blocking.
180
222
  // De-loop state is SESSION-SCOPED JSON { "sessionId", "tokens", "releasedAtTokens"? }.
@@ -0,0 +1,104 @@
1
+ #!/usr/bin/env node
2
+
3
+ // UserPromptSubmit hook for /company runs: durably captures every operator request
4
+ // into .company/intake-log.md so an ask can never be lost to model memory. This is
5
+ // the mechanical complement to the criteria-gate (stop-guard): the stop-guard keeps
6
+ // a run open until every captured ask is triaged, this hook makes sure the ask is
7
+ // captured in the first place by a hook rather than by the model remembering.
8
+ //
9
+ // CONTRACT: ALWAYS exit 0, never print a decision, fail-OPEN on every error. A
10
+ // UserPromptSubmit hook that blocked or crashed would wedge the operator's own
11
+ // prompt; logging is best-effort and never gates input. The stop-guard does the
12
+ // gating; this hook only records.
13
+ //
14
+ // Owner-scoping mirrors stop-guard.js: if OWNER is clean and non-empty and this
15
+ // session is NOT listed, do not log (a foreign session sharing the dir must not
16
+ // pollute this run's intake log). A garbled or missing OWNER falls through and logs
17
+ // (fail-open: better a spurious line than a dropped ask).
18
+
19
+ const fs = require('fs');
20
+ const path = require('path');
21
+ const crypto = require('crypto');
22
+
23
+ // Resolve companyDir robustly: COMPANY_DIR env wins; else prefer the dir that holds
24
+ // a clean OWNER (at least one valid session-id line); fall back to cwd/.company.
25
+ // A blank/garbled OWNER does NOT qualify a dir as the active run (BLOCKER-1 fix).
26
+ function hasCleanOwner(ownerPath) {
27
+ try {
28
+ const lines = fs.readFileSync(ownerPath, 'utf8')
29
+ .split('\n').map(function (l) { return l.trim(); }).filter(Boolean);
30
+ return lines.length > 0 &&
31
+ lines.every(function (l) { return /^[A-Za-z0-9][A-Za-z0-9._-]{7,}$/.test(l); });
32
+ } catch (e) { return false; }
33
+ }
34
+ function resolveCompanyDir() {
35
+ if (process.env.COMPANY_DIR) return process.env.COMPANY_DIR;
36
+ const home = process.env.HOME || '';
37
+ const cwdDir = path.join(process.cwd(), '.company');
38
+ const homeDir = path.join(home, '.company');
39
+ const cwdHasOwner = hasCleanOwner(path.join(cwdDir, 'OWNER'));
40
+ const homeHasOwner = home && hasCleanOwner(path.join(homeDir, 'OWNER'));
41
+ // cwd/.company wins when it has a clean OWNER (project-local run, or both have OWNER).
42
+ if (cwdHasOwner) return cwdDir;
43
+ if (homeHasOwner) return homeDir;
44
+ return cwdDir; // new-run default: preserves original single-project behavior
45
+ }
46
+
47
+ function main() {
48
+ const companyDir = resolveCompanyDir();
49
+ // No company dir: nothing to log against.
50
+ if (!companyDir || !fs.existsSync(companyDir)) return;
51
+
52
+ let input = null;
53
+ try {
54
+ input = JSON.parse(fs.readFileSync(0, 'utf8'));
55
+ } catch (e) { return; } // malformed stdin: fail-open, no line, no crash.
56
+ if (!input || typeof input !== 'object') return;
57
+
58
+ const sessionId = typeof input.session_id === 'string' ? input.session_id : '';
59
+ const prompt = input.prompt;
60
+ // Missing/empty/non-string prompt: nothing to capture.
61
+ if (typeof prompt !== 'string' || prompt.trim() === '') return;
62
+
63
+ // Owner-scope: clean+non-empty OWNER that does NOT list this session -> do not log.
64
+ // Garbled/missing OWNER -> fall through (log). Mirrors stop-guard's regex guard.
65
+ try {
66
+ const ownerPath = path.join(companyDir, 'OWNER');
67
+ const rawOwners = fs.readFileSync(ownerPath, 'utf8')
68
+ .split('\n').map(function (l) { return l.trim(); }).filter(Boolean);
69
+ const valid = rawOwners.filter(function (l) { return /^[A-Za-z0-9][A-Za-z0-9._-]{7,}$/.test(l); });
70
+ // Clean OWNER (every line valid, at least one line): scope strictly.
71
+ if (rawOwners.length > 0 && valid.length === rawOwners.length) {
72
+ if (valid.indexOf(sessionId) === -1) return; // foreign session, do not log.
73
+ }
74
+ // Garbled OWNER (some invalid line) or missing OWNER (read threw): fall through.
75
+ } catch (e) { /* no OWNER file: fall through and log */ }
76
+
77
+ // Stable id: first 8 hex of sha256(timestamp + sessionId + prompt).
78
+ const ts = new Date().toISOString();
79
+ const id8 = crypto.createHash('sha256')
80
+ .update(ts + sessionId + prompt).digest('hex').slice(0, 8);
81
+
82
+ // One-line the prompt: newlines -> space, pipe -> '/', collapse whitespace, truncate 500.
83
+ let oneLine = prompt.replace(/[\r\n]+/g, ' ').replace(/\|/g, '/').replace(/\s+/g, ' ').trim();
84
+ if (oneLine.length > 500) oneLine = oneLine.slice(0, 500);
85
+
86
+ const logPath = path.join(companyDir, 'intake-log.md');
87
+ const header =
88
+ '<!-- company intake log: append-only operator-request capture. Each line is one ' +
89
+ 'UserPromptSubmit event. `- [ ]` = un-triaged, the THINK phase folds it into a ' +
90
+ 'criterion and rewrites the marker: `- [x] captured: <criterion-id>`, ' +
91
+ '`- [~] declined: <reason>`, or `- [>] deferred: <target>`. The stop-guard blocks ' +
92
+ 'a stop while any stale `- [ ]` line remains. Do not hand-edit the id or timestamp. -->\n';
93
+ const line = '- [ ] ' + ts + ' ' + id8 + ' | ' + sessionId + ' | ' + oneLine + '\n';
94
+
95
+ try {
96
+ if (!fs.existsSync(logPath)) {
97
+ fs.writeFileSync(logPath, header);
98
+ }
99
+ fs.appendFileSync(logPath, line);
100
+ } catch (e) { /* fail-open: a write error must never gate the operator's prompt */ }
101
+ }
102
+
103
+ try { main(); } catch (e) { /* fail-open on anything unexpected */ }
104
+ process.exit(0);