company-skill 4.6.7 → 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
@@ -2,14 +2,31 @@
2
2
 
3
3
  [![npm](https://img.shields.io/npm/v/company-skill)](https://www.npmjs.com/package/company-skill) [![npm downloads](https://img.shields.io/npm/dw/company-skill)](https://www.npmjs.com/package/company-skill) [![CI](https://github.com/jagmarques/company-skill/actions/workflows/check.yml/badge.svg)](https://github.com/jagmarques/company-skill/actions/workflows/check.yml) [![license](https://img.shields.io/npm/l/company-skill)](LICENSE)
4
4
 
5
- **The agent company that can't stop until the work is verified done.**
5
+ **The agent company that can't stop until the work is verified done**
6
6
 
7
7
  Your agent stops when it feels done. This makes it stop only when the work is actually done.
8
8
 
9
+ company-skill is a Claude Code skill for multi-agent orchestration. It runs your agents as a company with a verification gate, so a `/company` run cannot stop until every acceptance criterion is reproduced.
10
+
11
+ ## What it is
12
+
13
+ You give it a goal. It splits the work across sub-agents, runs them in dependency waves, and keeps going across cycles until the result is proven instead of just claimed. It runs unattended. Each cycle re-runs every check, a reviewer attacks anything marked passing, and a stop guard blocks exit while any criterion still fails. Sub-agents get scoped delegation contracts rather than the whole problem. A digest writer compresses each finished cycle so the orchestrator stays under its context window, and work is split across models to keep cost down. Nothing is done until it is verified.
14
+
15
+ ## Who it's for
16
+
17
+ Reach for it when "looks done" is not good enough and you want the loop to finish the job on its own:
18
+
19
+ - Build a REST API and prove the tests pass before it stops.
20
+ - Run an overnight refactor that won't quit half-done.
21
+ - Ship a feature where every acceptance criterion is reproduced, not asserted.
22
+ - Hand off a long task and come back to verified output instead of a half-finished draft.
23
+
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
+
9
26
  <p align="center">
10
- <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">
11
28
  <br>
12
- <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>
13
30
  </p>
14
31
 
15
32
  ```bash
@@ -55,18 +72,44 @@ The dashboard starts automatically when you run `/company` and prints its URL in
55
72
  http://127.0.0.1:7421 <- your session's link, printed at startup and in the status bar
56
73
  ```
57
74
 
58
- What you see, panel by panel:
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
+
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.
59
80
 
60
- **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 bar shows "restart due" so you can see the gate before it fires. A toggle in the dashboard controls the auto-restart per session.
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.
61
82
 
62
- **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 with +/- buttons or the mouse wheel. Drag to pan. Fullscreen button expands it. 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.
84
+
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.
63
86
 
64
87
  **Active agents** - centered live table of every agent the orchestrator has spawned this session, with model, status, and token count.
65
88
 
66
89
  **Criteria** - compact progress view with a click-to-expand toggle for the full pass/fail list and reproduced evidence.
67
90
 
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.
92
+
68
93
  The dashboard binds 127.0.0.1 only, reads local files, and sends nothing anywhere. Override the port with `COMPANY_DASHBOARD_PORT`.
69
94
 
95
+ **You get this automatically.** Installing company-skill (npm or the install script) copies `dashboard.js` and `statusline.js` alongside the rest of the skill. Running `/company` then starts the dashboard for you and prints its localhost URL at startup and in the status line. No extra setup.
96
+
97
+ ### Status line
98
+
99
+ `scripts/statusline.js` appends a labelled link to the per-session dashboard so the URL is one glance away on every turn. The appended segment is `📊 company dashboard <url>`.
100
+
101
+ With no status line of your own configured, it also renders the model and context fill. The context fill draws as a progress bar using Claude's native used_percentage so the number matches the session indicator, so the full line reads:
102
+
103
+ ```
104
+ Opus 4.8 (1M context) | [████░░░░░░] 25% | 📊 company dashboard http://127.0.0.1:7421
105
+ ```
106
+
107
+ If you already have a status line, the setup step stores your prior command in `.company/statusline-base.json` and runs it first. Your line leads, the script appends only the link, and it does not re-print model or context since your own line owns those:
108
+
109
+ ```
110
+ <your status line> | 📊 company dashboard http://127.0.0.1:7421
111
+ ```
112
+
70
113
 
71
114
  ## Unattended auto-restart loop
72
115
 
@@ -95,26 +138,26 @@ It runs with `--permission-mode bypassPermissions` for the autonomy an unattende
95
138
 
96
139
  Multi-agent orchestration buys quality with tokens. /company's answer to the token cost: spend strong-model tokens only where they buy quality, and report the bill every cycle.
97
140
 
98
- **Tiered model delegation.** Each delegation contract carries a `MODEL: cheap|mid|strong` tag. The orchestrator maps the tag to a model at spawn time. Effort scales with both ROI and stakes - high-stakes or high-value work gets heavier spawn. Override every sub-agent with `CLAUDE_CODE_SUBAGENT_MODEL` at launch, or write `FORCE_BEST` into `.company/MODEL_POLICY` mid-run.
141
+ **Tiered model delegation** - each delegation contract carries a `MODEL: cheap|mid|strong` tag. The orchestrator maps the tag to a model at spawn time. Effort scales with both ROI and stakes - high-stakes or high-value work gets heavier spawn. Override every sub-agent with `CLAUDE_CODE_SUBAGENT_MODEL` at launch, or write `FORCE_BEST` into `.company/MODEL_POLICY` mid-run.
99
142
 
100
- **Per-cycle cost reporting.** Every cycle produces a `COST:` line in the briefing and a `cycles/cycle-{N}-cost.json` artifact.
143
+ **Per-cycle cost reporting** - every cycle produces a `COST:` line in the briefing and a `cycles/cycle-{N}-cost.json` artifact.
101
144
 
102
- **Prompt caching.** Agent prompts are laid out stable-first so repeated spawns hit a shared cache prefix.
145
+ **Prompt caching** - agent prompts are laid out stable-first so repeated spawns hit a shared cache prefix.
103
146
 
104
- **Fable 5 / adaptive thinking.** On models that support adaptive thinking (Fable 5 and later), the orchestrator and verify layers run with thinking enabled. No `budget_tokens` param - reflection depth is model-controlled.
147
+ **Fable 5 / adaptive thinking** - on models that support adaptive thinking (Fable 5 and later), the orchestrator and verify layers run with thinking enabled. No `budget_tokens` param - reflection depth is model-controlled.
105
148
 
106
149
 
107
150
  ## Key features
108
151
 
109
- **Stop guard** - blocks session exit until every criterion has `passes: true` and reproduced evidence. Malformed state blocks rather than fails open. Deleting a hard criterion blocks instead of unlocking. [34-check test](tests/stop-guard.test.js).
152
+ **Stop guard** - blocks session exit until every criterion has `passes: true` and reproduced evidence. Malformed state blocks rather than fails open. Deleting a hard criterion blocks instead of unlocking. [42-check test](tests/stop-guard.test.js).
110
153
 
111
- **Context-fill guard** - a second Stop hook forces `/company restart` once context reaches the threshold (default 50%). Reads the model id from the transcript to detect the context window. Per-session auto-restart toggle in the dashboard. [37-check test](tests/context-guard.test.js).
154
+ **Context-fill guard** - a second Stop hook forces `/company restart` once context reaches the threshold (default 50%). Reads the model id from the transcript to detect the context window. The restart block is always on and the dashboard shows it as a locked Apple-style pill toggle. [63-check test](tests/context-guard.test.js).
112
155
 
113
- **Delegation contracts** - a task does not exist without a filled contract. `check-contracts.js` rejects missing fields, vacuous VERIFY-WITH commands, invalid MODEL tiers, and cyclic dependencies. [17-check test](tests/check-contracts.test.js).
156
+ **Delegation contracts** - a task does not exist without a filled contract. `check-contracts.js` rejects missing fields, vacuous VERIFY-WITH commands, invalid MODEL tiers, and cyclic dependencies. [29-check test](tests/check-contracts.test.js).
114
157
 
115
- **Multi-level verification.** The Internal Reviewer re-runs every VERIFY-WITH command independently. The Devil's Advocate attacks everything marked passing. For criteria tagged `stakes: "high"` in `criteria.json` (irreversible action, security surface, or public-facing claim), the critic runs in three fresh contexts with distinct lenses - correctness, security, reproducibility - and unanimous ACCEPT is required. Normal criteria keep the single critic. The completeness probe enumerates every surface the GOAL names and auto-rejects any unchecked one.
158
+ **Multi-level verification** - the Internal Reviewer re-runs every VERIFY-WITH command independently. The Devil's Advocate attacks everything marked passing. For criteria tagged `stakes: "high"` in `criteria.json` (irreversible action, security surface, or public-facing claim), the critic runs in three fresh contexts with distinct lenses - correctness, security, reproducibility - and unanimous ACCEPT is required. Normal criteria keep the single critic. The completeness probe enumerates every surface the GOAL names and auto-rejects any unchecked one.
116
159
 
117
- **Design judge-panel.** For criteria tagged `kind: design`, the lead may emit up to three contracts from materially different angles plus one synthesis contract, reserved for genuine design forks.
160
+ **Design judge-panel** - for criteria tagged `kind: design`, the lead may emit up to three contracts from materially different angles plus one synthesis contract, reserved for genuine design forks.
118
161
 
119
162
  **Git isolation** - workers never push to main and never merge. Every code change lands as a draft PR. The merge gate is yours.
120
163
 
@@ -122,7 +165,7 @@ Multi-agent orchestration buys quality with tokens. /company's answer to the tok
122
165
 
123
166
  **Codebase graph** - on repos with >200 tracked files, `scripts/codegraph.js` builds a commit-keyed ranked symbol map into `.company/codegraph/` for lead prompts.
124
167
 
125
- **Status-line link** - `scripts/statusline.js` appends the per-session dashboard URL to the Claude Code status bar, enforced idempotently on every `/company` run.
168
+ **Status-line link** - `scripts/statusline.js` appends a labelled `📊 company dashboard <url>` segment to the Claude Code status bar, enforced idempotently on every `/company` run. The link resolves in any session through a cwd-independent global registry, with an `/all` fallback. It chains any status line you already have and never re-prints model or context when one is present. See [Status line](#status-line).
126
169
 
127
170
 
128
171
  ## Commands
@@ -154,7 +197,7 @@ State lives in `./.company/` (relocate with `COMPANY_DIR`):
154
197
 
155
198
  ## Examples
156
199
 
157
- [`startup.md`](examples/startup.md), [`research-lab.md`](examples/research-lab.md), [`dev-team.md`](examples/dev-team.md), [`nexusquant.md`](examples/nexusquant.md).
200
+ [`startup.md`](examples/startup.md), [`research-lab.md`](examples/research-lab.md), [`dev-team.md`](examples/dev-team.md).
158
201
 
159
202
 
160
203
  ## Contributing
@@ -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') + ')');
@@ -48,7 +48,7 @@ function parseThreshold() {
48
48
  return v > 1 ? v / 100 : v;
49
49
  }
50
50
 
51
- // Hard ceiling: the toggle CANNOT suppress a block at or above this level.
51
+ // Hard ceiling: a higher informational level that changes the block wording only.
52
52
  // Default 0.80. Overridable via COMPANY_CONTEXT_HARD_CEILING (fraction or percent).
53
53
  // If the parsed value is below the soft threshold, it is clamped up to the threshold.
54
54
  function parseHardCeiling(threshold) {
@@ -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,52 +169,74 @@ 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
- // Per-session toggle: read .company/context-guard-config.json for this session.
175
- // Shape: { "sessions": { "<id>": { "enforceRestart": true|false } } }
176
- // Default (no file, no entry, parse error) = enforceRestart: true (safe default).
177
- // If enforceRestart === false for this session, and fill is BELOW the hard ceiling,
178
- // the toggle suppresses the soft-threshold block. At or above the hard ceiling the
179
- // toggle has no effect and we fall through to block unconditionally.
180
- if (sessionId && fill < hardCeiling) {
181
- const cfgPath = path.join(companyDir, 'context-guard-config.json');
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
182
205
  try {
183
- const cfgRaw = fs.readFileSync(cfgPath, 'utf8');
184
- const cfg = JSON.parse(cfgRaw);
185
- if (cfg && typeof cfg === 'object' &&
186
- cfg.sessions && typeof cfg.sessions === 'object' &&
187
- cfg.sessions[sessionId] && typeof cfg.sessions[sessionId] === 'object' &&
188
- cfg.sessions[sessionId].enforceRestart === false) {
189
- // Toggle is OFF and below the hard ceiling: allow through without blocking
190
- process.exit(0);
191
- }
192
- // Any other value (true, missing, or unknown) falls through to block
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);
193
211
  } catch (e) {
194
- // File missing or parse error: treat as enforceRestart=true (safe default)
212
+ return false; // missing or garbled pref = ON (fail toward the restart)
195
213
  }
196
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.
197
220
 
198
221
  // Fill is at or above threshold. Check the de-loop state file before blocking.
199
- // De-loop state is SESSION-SCOPED: stored as JSON { "sessionId": "<id>", "tokens": <n> }.
222
+ // De-loop state is SESSION-SCOPED JSON { "sessionId", "tokens", "releasedAtTokens"? }.
200
223
  // A state file from a different session (or a legacy bare-number file) is treated as
201
224
  // no prior fire for this session (lastFiredTokens = -1). This prevents a prior
202
225
  // session's high-water mark from suppressing a first-fire in a fresh session.
203
226
  // Within a session: first fire blocks + writes { sessionId, tokens }; subsequent stops
204
227
  // with grown tokens require the debate artifact; a token DROP re-arms unconditionally.
228
+ // Once a debate is honored the release is STICKY: releasedAtTokens records the fill at
229
+ // which the restart was released so re-stops at the same fill stay quiet (no re-demand
230
+ // loop), mirroring how CANCEL is persistent.
205
231
  const stateFile = path.join(companyDir, '.context-guard-state');
206
232
 
233
+ // Margin (5% of the context window) of new accumulation past the released high-water
234
+ // before a fresh restart demand is legitimate again. Below this, the session is treated
235
+ // as already released at this fill and the guard stays quiet.
236
+ const RELEASE_MARGIN_TOKENS = Math.round(contextWindow * 0.05);
237
+
207
238
  let lastFiredTokens = -1;
239
+ let releasedAtTokens = -1;
208
240
  try {
209
241
  const raw = fs.readFileSync(stateFile, 'utf8').trim();
210
242
  // Attempt JSON parse first (session-scoped format).
@@ -219,10 +251,30 @@ try {
219
251
  typeof parsed.tokens === 'number') {
220
252
  // State belongs to the current session: honor it.
221
253
  lastFiredTokens = parsed.tokens;
254
+ if (typeof parsed.releasedAtTokens === 'number') releasedAtTokens = parsed.releasedAtTokens;
222
255
  }
223
256
  // Null sessionId either side, legacy bare-number, or different session: treat as fresh.
224
257
  } catch (e) {}
225
258
 
259
+ // Sticky release: a restart already honored at releasedAtTokens silences the guard for
260
+ // re-stops at the same-or-similar fill. Only a meaningful rise past the margin re-arms.
261
+ if (releasedAtTokens >= 0) {
262
+ if (used <= releasedAtTokens + RELEASE_MARGIN_TOKENS) process.exit(0);
263
+ // Fill rose meaningfully above the released high-water: genuine new accumulation.
264
+ // Re-arm by recording a fresh first-fire at this fill and block once.
265
+ try { fs.writeFileSync(stateFile, JSON.stringify({ sessionId: sessionId, tokens: used })); } catch (e) {}
266
+ const sessionTagRearm = sessionId ? ' [session ' + sessionId + ']' : '';
267
+ const pctRearm = Math.round(fill * 100);
268
+ console.log(JSON.stringify({
269
+ decision: 'block',
270
+ reason: '[COMPANY] Context at ' + pctRearm + '% (grew past the prior restart point). ' +
271
+ 'Run /company restart NOW: quiesce in-flight agents, commit their work as draft PRs, ' +
272
+ 'refresh STATUS/NEXT, run the restart debate gate, and emit the verified continuation prompt. ' +
273
+ 'This is enforced, not advisory - do not continue new work.' + sessionTagRearm,
274
+ }));
275
+ process.exit(0);
276
+ }
277
+
226
278
  if (lastFiredTokens >= 0) {
227
279
  // Fired once already this session. Re-arm unconditionally when tokens dropped below
228
280
  // the fire count (a genuine context reset happened). Otherwise gate on the debate artifact.
@@ -253,8 +305,13 @@ if (lastFiredTokens >= 0) {
253
305
  } catch (e) {}
254
306
 
255
307
  if (artifactFresh) {
256
- // Debate recorded. Allow the stop and re-arm for the next cycle.
257
- try { fs.writeFileSync(stateFile, JSON.stringify({ sessionId: sessionId, tokens: -1 })); } catch (e) {}
308
+ // Debate recorded. Allow the stop and record a STICKY release at this fill so the
309
+ // next stop at the same-or-similar fill does not re-demand the debate in a loop.
310
+ try {
311
+ fs.writeFileSync(stateFile, JSON.stringify({
312
+ sessionId: sessionId, tokens: used, releasedAtTokens: used,
313
+ }));
314
+ } catch (e) {}
258
315
  // Best-effort: remove the consumed artifact so a stale one cannot release
259
316
  // a future fire. Failure is ignored (artifact absence is also safe).
260
317
  try { fs.unlinkSync(artifactPath); } catch (e) {}
@@ -287,7 +344,7 @@ const sessionTag = sessionId ? ' [session ' + sessionId + ']' : '';
287
344
  const atHardCeiling = fill >= hardCeiling;
288
345
  const reason = atHardCeiling
289
346
  ? '[COMPANY] Context at ' + pct + '% (>= ' + ceilPct + '% HARD CEILING). ' +
290
- 'The per-session enforceRestart toggle CANNOT suppress this restart. ' +
347
+ 'This restart is always enforced and cannot be suppressed per-session. ' +
291
348
  'Run /company restart NOW: quiesce in-flight agents, commit their work as draft PRs, ' +
292
349
  'refresh STATUS/NEXT, run the restart debate gate, and emit the verified continuation prompt.' +
293
350
  sessionTag