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.
- package/COMPANY.md.template +5 -1
- package/MODEL_POLICY.template +7 -0
- package/README.md +60 -17
- package/agents/company-critic.md +1 -0
- package/agents/company-digest.md +2 -0
- package/agents/company-lead.md +3 -1
- package/agents/company-worker.md +2 -1
- package/bin/install.js +43 -2
- package/hooks/company-prepush-guard.js +110 -0
- package/hooks/context-guard.js +83 -26
- package/hooks/intake-log.js +104 -0
- package/hooks/orchestrator-guard.js +141 -0
- package/hooks/owner-bootstrap.js +55 -0
- package/hooks/post-merge-cleanup.js +83 -0
- package/hooks/stop-guard.js +202 -11
- package/install.sh +17 -3
- package/package.json +13 -1
- package/scripts/check-criteria.js +52 -0
- package/scripts/check-isolation.js +158 -0
- package/scripts/check-playbook.js +29 -0
- package/scripts/check-roster.js +194 -0
- package/scripts/clean-stale-dashboards.js +205 -0
- package/scripts/cleanup.js +52 -0
- package/scripts/company-autoloop.js +30 -0
- package/scripts/dashboard.js +1716 -380
- package/scripts/migrate-state.js +126 -0
- package/scripts/reset-company-guard.js +15 -0
- package/scripts/restart-debate.js +11 -0
- package/scripts/secret-scan.js +51 -10
- package/scripts/statusline.js +292 -14
- package/scripts/workspace-guard.js +146 -0
- package/skill/SKILL.md +215 -22
package/COMPANY.md.template
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
# My Company
|
|
2
2
|
|
|
3
3
|
<!--
|
|
4
|
-
|
|
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
|
package/MODEL_POLICY.template
CHANGED
|
@@ -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
|
[](https://www.npmjs.com/package/company-skill) [](https://www.npmjs.com/package/company-skill) [](https://github.com/jagmarques/company-skill/actions/workflows/check.yml) [](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
|
|
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:
|
|
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
|
-
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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. [
|
|
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.
|
|
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. [
|
|
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
|
|
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
|
|
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
|
|
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)
|
|
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
|
package/agents/company-critic.md
CHANGED
|
@@ -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
|
|
package/agents/company-digest.md
CHANGED
|
@@ -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
|
|
package/agents/company-lead.md
CHANGED
|
@@ -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.
|
package/agents/company-worker.md
CHANGED
|
@@ -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
|
|
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') + ')');
|
package/hooks/context-guard.js
CHANGED
|
@@ -48,7 +48,7 @@ function parseThreshold() {
|
|
|
48
48
|
return v > 1 ? v / 100 : v;
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
// Hard ceiling:
|
|
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
|
-
|
|
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:
|
|
175
|
-
//
|
|
176
|
-
//
|
|
177
|
-
//
|
|
178
|
-
//
|
|
179
|
-
//
|
|
180
|
-
|
|
181
|
-
|
|
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
|
|
184
|
-
const
|
|
185
|
-
if (
|
|
186
|
-
|
|
187
|
-
|
|
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
|
-
//
|
|
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
|
|
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
|
|
257
|
-
|
|
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
|
-
'
|
|
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
|