company-skill 4.6.8 → 4.6.10
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 +38 -7
- package/agents/company-critic.md +1 -0
- package/agents/company-digest.md +2 -0
- package/agents/company-lead.md +3 -1
- package/agents/company-reviewer.md +13 -1
- package/agents/company-worker.md +2 -1
- package/bin/install.js +46 -3
- package/hooks/company-prepush-guard.js +110 -0
- package/hooks/context-guard.js +48 -6
- 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 +18 -4
- package/package.json +14 -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/dashboard.js +1162 -219
- package/scripts/diagnosis-block.js +75 -0
- package/scripts/migrate-state.js +126 -0
- package/scripts/reset-company-guard.js +133 -0
- package/scripts/restart-debate.js +11 -0
- package/scripts/secret-scan.js +51 -10
- package/scripts/statusline.js +439 -34
- package/scripts/workspace-guard.js +146 -0
- package/skill/SKILL.md +216 -23
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
|
@@ -24,9 +24,9 @@ Reach for it when "looks done" is not good enough and you want the loop to finis
|
|
|
24
24
|
Compared to a single agent or a plain prompt loop, the difference is the gate. A single agent stops when it decides it is finished. A company of agents keeps running, re-checking its own claims each cycle, until the evidence is there. It does not invent work outside the goal, and it tells you plainly when it is blocked.
|
|
25
25
|
|
|
26
26
|
<p align="center">
|
|
27
|
-
<img src="assets/dashboard.png" alt="Company dashboard showing
|
|
27
|
+
<img src="assets/dashboard.png" alt="Company dashboard showing a tokens box and a separate cost box with a model-policy toggle, a horizontal cycles-and-memory row, context gauge, active agents, the delegation tree, and criteria" width="900">
|
|
28
28
|
<br>
|
|
29
|
-
<em>Live dashboard:
|
|
29
|
+
<em>Live dashboard: a tokens box and a separate cost box (cache savings, hit rate and cycles live with the dollars), a horizontal cycles-and-memory row, context gauge, agent table, delegation tree, and criteria checklist - auto-starts with every /company run.</em>
|
|
30
30
|
</p>
|
|
31
31
|
|
|
32
32
|
```bash
|
|
@@ -72,21 +72,23 @@ The dashboard starts automatically when you run `/company` and prints its URL in
|
|
|
72
72
|
http://127.0.0.1:7421 <- your session's link, printed at startup and in the status bar
|
|
73
73
|
```
|
|
74
74
|
|
|
75
|
-
A per-session identity header sits at the top: the project, the session id, the active model, and an "All projects" link to the cross-project roll-up.
|
|
75
|
+
A per-session identity header sits at the top: the project, the session id, the active model, and an "All projects" link to the cross-project roll-up. The project name is read from your `.company` directory, so two companies launched from the same folder stay labelled apart.
|
|
76
76
|
|
|
77
|
-
What you see, panel by panel:
|
|
77
|
+
Every block is a card with the same tile look, so cost, cycles, agents, and criteria all read the same way. What you see, panel by panel:
|
|
78
|
+
|
|
79
|
+
**Cost and usage** - two stacked bands so dollars never interleave with token counts. The Tokens band shows the volumes (input, output, cache-write, this session) as plain counts. The Cost band shows every dollar figure together (today, this session, and "Saved by cheaper models vs all-opus"), with the list-price "not billed" note attached only to the dollars. The dollars come from ccusage at public list prices and are notional on a subscription plan, which the card says plainly. A model-policy toggle sits on the savings tile, reusing the same Apple-style pill as the auto-restart control: off is adaptive (cheaper models run sub-tasks for more savings), on forces the best model everywhere. It writes `.company/MODEL_POLICY` (`TIERED` or `FORCE_BEST`), the file the orchestrator reads at the start of each cycle, so the change applies next cycle.
|
|
78
80
|
|
|
79
81
|
**Context fill** - the live fill percentage, computed with the same formula the context-guard uses. When the session hits the restart threshold (default 50%), the gauge marks the gate before it fires. Next to it sits an Apple-style auto-restart pill, locked on - the restart block is always enforced and the toggle cannot turn it off.
|
|
80
82
|
|
|
81
|
-
**Delegation tree** - SVG tree of orchestrator, department leads, and workers,
|
|
83
|
+
**Delegation tree** - SVG tree of orchestrator, department leads, and workers, showing only the agents running right now. COMPANY.md is the source pool and the activated roster decides which departments are eligible, but a role node paints only when a currently-active agent maps to it, and a department appears only when at least one of its roles is live, so a finished or stale agent leaves no node behind. The orchestrator (CEO) root always shows. Long role names wrap inside their node instead of spilling out. Click any node to expand its current task and status. Zoom only with the pill-shaped +/- buttons, or use the expand button to blow the tree up to fill the screen and the contract button to bring it back. Drag to pan. A refresh resets the tree to its default view. Zero external JS libraries.
|
|
82
84
|
|
|
83
|
-
**Cycles and memory** - a savings card that shows
|
|
85
|
+
**Cycles and memory** - a savings card that shows cycle and memory counts plus the cache-read volume and the dollars saved by prompt caching, marked approximate. The model-tiering saving lives in the Cost band above, next to the policy toggle.
|
|
84
86
|
|
|
85
87
|
**Active agents** - centered live table of every agent the orchestrator has spawned this session, with model, status, and token count.
|
|
86
88
|
|
|
87
89
|
**Criteria** - compact progress view with a click-to-expand toggle for the full pass/fail list and reproduced evidence.
|
|
88
90
|
|
|
89
|
-
**All projects** - the "All projects" link near the session header opens `/all`, a cross-project roll-up. It reads `~/.claude/company-dashboards.json`, a small index every dashboard writes itself into, and shows aggregate cost, tokens, and cache reuse grouped by project. It lists only dashboards seen in the last 5 minutes, costs come from ccusage and are notional on a subscription plan, and a session whose usage row is missing shows `?` rather than zero. Each row links back to that session's own dashboard.
|
|
91
|
+
**All projects** - the "All projects" link near the session header opens `/all`, a cross-project roll-up. It reads `~/.claude/company-dashboards.json`, a small index every dashboard writes itself into, and shows aggregate cost, tokens, and cache reuse grouped by project. It lists only dashboards seen in the last 5 minutes, costs come from ccusage and are notional on a subscription plan, and a session whose usage row is missing shows `?` rather than zero. Each row links back to that session's own dashboard. A dashboard whose owning session has ended drops itself from the index, so `/all` never lists a stale one.
|
|
90
92
|
|
|
91
93
|
The dashboard binds 127.0.0.1 only, reads local files, and sends nothing anywhere. Override the port with `COMPANY_DASHBOARD_PORT`.
|
|
92
94
|
|
|
@@ -193,6 +195,35 @@ State lives in `./.company/` (relocate with `COMPANY_DIR`):
|
|
|
193
195
|
```
|
|
194
196
|
|
|
195
197
|
|
|
198
|
+
## State is per-project, code is global
|
|
199
|
+
|
|
200
|
+
Run state is per-project. Every `.company/` lives inside the project it belongs to
|
|
201
|
+
(or wherever `COMPANY_DIR` points), so two projects never share or leak state.
|
|
202
|
+
|
|
203
|
+
The skill code is global on purpose. One install lives under `~/.claude/skills/company`
|
|
204
|
+
and serves every project. There are no per-project copies, which avoids version drift
|
|
205
|
+
across projects, a separate update path per project, and duplicated disk. Isolation
|
|
206
|
+
comes from per-project state, not per-project code.
|
|
207
|
+
|
|
208
|
+
Per-session resources are keyed on the session id. The dashboard port and the
|
|
209
|
+
statusline render cache derive from `$CLAUDE_CODE_SESSION_ID`, so concurrent sessions
|
|
210
|
+
do not collide and dead sessions release their slot.
|
|
211
|
+
|
|
212
|
+
Stop-guard anchors live outside the project, under `~/.claude/company-guard-state/`,
|
|
213
|
+
keyed by `sha256(realpath(COMPANY_DIR))`. They sit outside the project so an in-run
|
|
214
|
+
actor cannot forge them. The key set grows by one for every distinct `.company` path
|
|
215
|
+
ever used and is never pruned by the default reset. Reclaim old ones by age with:
|
|
216
|
+
|
|
217
|
+
```bash
|
|
218
|
+
node ~/.claude/skills/company/scripts/reset-company-guard.js --gc --dry-run
|
|
219
|
+
node ~/.claude/skills/company/scripts/reset-company-guard.js --gc --max-age-days 30
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
`--gc` is opt-in and never auto-run. It removes only anchors older than the cutoff
|
|
223
|
+
(default 30 days), `--dry-run` lists what would go without removing anything, and it
|
|
224
|
+
only ever touches dirs under `~/.claude/company-guard-state/`.
|
|
225
|
+
|
|
226
|
+
|
|
196
227
|
## Examples
|
|
197
228
|
|
|
198
229
|
[`startup.md`](examples/startup.md), [`research-lab.md`](examples/research-lab.md), [`dev-team.md`](examples/dev-team.md).
|
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.
|
|
@@ -22,7 +22,19 @@ Additional duties:
|
|
|
22
22
|
- **Stall counter.** When you keep a criterion failing, increment (or create) an `attempts` field on its criteria.json entry. At 2+ state in your verdict that the approach is stalled and the next cycle must re-plan, not re-try. Writing `attempts` is required: the stall detector and the high-stakes 3-lens gate both read it from criteria.json.
|
|
23
23
|
- **Anti-vacuous test check (SKILL.md ANTI-VACUOUS TEST).** For every new test introduced this cycle, confirm it FAILS against the pre-change code. A test that passes before the fix is vacuous and must be rejected.
|
|
24
24
|
- **Feature reachability check (SKILL.md FEATURE REACHABILITY).** For every new feature that gates on a field in criteria.json (e.g. `stakes: "high"`), confirm the authoring path that sets that field exists in the skill or agent instructions. A gate that is never set is a dead feature; reject it until the wiring is present.
|
|
25
|
-
- **Respawn
|
|
25
|
+
- **Respawn diagnosis.** For any task that will be respawned, author a structured FAILURE-DIAGNOSIS block into your verdict for the orchestrator to paste into the fresh contract. You author it by reading the shaped FINDINGS file (and re-running the cited command), NEVER the raw transcript and never the failed worker's self-report. The block:
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
FAILURE-DIAGNOSIS (task {id}, attempt {n})
|
|
29
|
+
ROOT-CAUSE-CLASS: one of [wrong-approach, missing-context, environment/tooling, spec-ambiguity, flaky/transient, scope-too-large, vacuous-or-misdirected-test]
|
|
30
|
+
ROOT-CAUSE: {one line}
|
|
31
|
+
EVIDENCE: {a findings-file:line you read, or a command you re-ran + its output, verbatim}
|
|
32
|
+
WHAT-TO-CHANGE: {concrete, contract-shaped: the next contract's TASK/INPUTS/surfaces}
|
|
33
|
+
KEEP: {what worked and must not be thrown away}
|
|
34
|
+
CONFIDENCE: high|low
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
EVIDENCE must cite a real findings line or a re-run command and its output, never your memory of the failure. A diagnosis with an empty ROOT-CAUSE, EVIDENCE, or WHAT-TO-CHANGE, or a ROOT-CAUSE-CLASS outside the enum, is not actionable (the anti "try-harder" guard) and the orchestrator falls back to the plain 3-line reflection (WHAT-WAS-TRIED / WHY-IT-FAILED cited to the findings file / DO-DIFFERENTLY). When CONFIDENCE is low, say so in the block so the next THINK varies the decomposition rather than trusting one read.
|
|
26
38
|
|
|
27
39
|
Audit each verdict against a tool result from THIS session. Only mark a criterion MET when you can cite the command you ran and its output from this run.
|
|
28
40
|
|
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
|
@@ -19,17 +19,26 @@ function copyFile(src, dest) {
|
|
|
19
19
|
copyFile(path.join(srcDir, 'skill', 'SKILL.md'), path.join(skillDir, 'SKILL.md'));
|
|
20
20
|
|
|
21
21
|
// Scripts are runtime dependencies referenced from SKILL.md. check.sh, lint-*,
|
|
22
|
-
// check-doc-commands.js, check-version.js, and test files
|
|
22
|
+
// check-doc-commands.js, check-version.js, check-regression.js, and test files
|
|
23
|
+
// stay in the repo only.
|
|
23
24
|
const INSTALL_SCRIPTS = [
|
|
24
25
|
'codegraph.js',
|
|
25
26
|
'check-contracts.js',
|
|
26
27
|
'check-findings.js',
|
|
28
|
+
'check-criteria.js',
|
|
29
|
+
'diagnosis-block.js',
|
|
30
|
+
'check-playbook.js',
|
|
27
31
|
'restart-debate.js',
|
|
28
32
|
'dashboard.js',
|
|
29
33
|
'secret-scan.js',
|
|
30
34
|
'reset-company-guard.js',
|
|
31
35
|
'cleanup.js',
|
|
32
36
|
'statusline.js',
|
|
37
|
+
'workspace-guard.js',
|
|
38
|
+
'migrate-state.js',
|
|
39
|
+
'check-isolation.js',
|
|
40
|
+
'check-roster.js',
|
|
41
|
+
'clean-stale-dashboards.js',
|
|
33
42
|
];
|
|
34
43
|
const scriptsDestDir = path.join(skillDir, 'scripts');
|
|
35
44
|
for (const script of INSTALL_SCRIPTS) {
|
|
@@ -51,7 +60,12 @@ const hookFiles = {
|
|
|
51
60
|
'stop-guard.js': 'company-stop-guard.js',
|
|
52
61
|
'context-guard.js': 'company-context-guard.js',
|
|
53
62
|
'precompact.js': 'company-precompact.js',
|
|
54
|
-
'session-restore.js': 'company-session-restore.js'
|
|
63
|
+
'session-restore.js': 'company-session-restore.js',
|
|
64
|
+
'intake-log.js': 'company-intake-log.js',
|
|
65
|
+
'company-prepush-guard.js': 'company-prepush-guard.js',
|
|
66
|
+
'orchestrator-guard.js': 'company-orchestrator-guard.js',
|
|
67
|
+
'post-merge-cleanup.js': 'company-post-merge-cleanup.js',
|
|
68
|
+
'owner-bootstrap.js': 'company-owner-bootstrap.js'
|
|
55
69
|
};
|
|
56
70
|
|
|
57
71
|
for (const [src, dest] of Object.entries(hookFiles)) {
|
|
@@ -85,12 +99,41 @@ try {
|
|
|
85
99
|
if (!settings.hooks.SessionStart.some(h => h.hooks?.some(hh => hh.command?.includes('company-session-restore')))) {
|
|
86
100
|
settings.hooks.SessionStart.push({ matcher: 'compact', hooks: [{ type: 'command', command: `node "${path.join(hooksDir, 'company-session-restore.js')}"`, timeout: 10 }] });
|
|
87
101
|
}
|
|
102
|
+
// SessionStart on a fresh start or resume: bootstrap OWNER so the owning session is
|
|
103
|
+
// recorded mechanically. Fail-open, idempotent, only writes inside an active run.
|
|
104
|
+
if (!settings.hooks.SessionStart.some(h => h.hooks?.some(hh => hh.command?.includes('company-owner-bootstrap')))) {
|
|
105
|
+
settings.hooks.SessionStart.push({ matcher: 'startup|resume', hooks: [{ type: 'command', command: `node "${path.join(hooksDir, 'company-owner-bootstrap.js')}"`, timeout: 10 }] });
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (!settings.hooks.UserPromptSubmit) settings.hooks.UserPromptSubmit = [];
|
|
109
|
+
if (!settings.hooks.UserPromptSubmit.some(h => h.hooks?.some(hh => hh.command?.includes('company-intake-log')))) {
|
|
110
|
+
settings.hooks.UserPromptSubmit.push({ hooks: [{ type: 'command', command: `node "${path.join(hooksDir, 'company-intake-log.js')}"`, timeout: 10 }] });
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// PreToolUse: mechanical secret-scan-before-push gate. Matcher 'Bash' so it only
|
|
114
|
+
// fires on shell commands; the hook itself filters to git push / gh pr create|ready.
|
|
115
|
+
if (!settings.hooks.PreToolUse) settings.hooks.PreToolUse = [];
|
|
116
|
+
if (!settings.hooks.PreToolUse.some(h => h.hooks?.some(hh => hh.command?.includes('company-prepush-guard')))) {
|
|
117
|
+
settings.hooks.PreToolUse.push({ matcher: 'Bash', hooks: [{ type: 'command', command: `node "${path.join(hooksDir, 'company-prepush-guard.js')}"`, timeout: 15 }] });
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// PostToolUse: lean-orchestrator advisory guard. Warns (never blocks) when the owner
|
|
121
|
+
// session loads oversized content inline instead of delegating it to a worker.
|
|
122
|
+
if (!settings.hooks.PostToolUse) settings.hooks.PostToolUse = [];
|
|
123
|
+
if (!settings.hooks.PostToolUse.some(h => h.hooks?.some(hh => hh.command?.includes('company-orchestrator-guard')))) {
|
|
124
|
+
settings.hooks.PostToolUse.push({ hooks: [{ type: 'command', command: `node "${path.join(hooksDir, 'company-orchestrator-guard.js')}"`, timeout: 10 }] });
|
|
125
|
+
}
|
|
126
|
+
// PostToolUse: post-merge cleanup reminder. Fires after a Bash gh pr merge to nudge
|
|
127
|
+
// the orchestrator to prune the merged branch + worktree. Advisory, fail-open.
|
|
128
|
+
if (!settings.hooks.PostToolUse.some(h => h.hooks?.some(hh => hh.command?.includes('company-post-merge-cleanup')))) {
|
|
129
|
+
settings.hooks.PostToolUse.push({ matcher: 'Bash', hooks: [{ type: 'command', command: `node "${path.join(hooksDir, 'company-post-merge-cleanup.js')}"`, timeout: 10 }] });
|
|
130
|
+
}
|
|
88
131
|
|
|
89
132
|
// Atomic write: a crash mid-write must not corrupt the user's settings.
|
|
90
133
|
const tmpPath = settingsPath + '.tmp';
|
|
91
134
|
fs.writeFileSync(tmpPath, JSON.stringify(settings, null, 2));
|
|
92
135
|
fs.renameSync(tmpPath, settingsPath);
|
|
93
|
-
console.log('Hooks installed: Stop guard + Context guard + PreCompact + SessionStart restore');
|
|
136
|
+
console.log('Hooks installed: Stop guard + Context guard + PreCompact + SessionStart restore + intake log + PreToolUse prepush guard');
|
|
94
137
|
} catch (e) {
|
|
95
138
|
console.log('Could not register hooks. Add manually to settings.json.');
|
|
96
139
|
}
|
|
@@ -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
|
@@ -103,6 +103,7 @@ function resolveCompanyDir() {
|
|
|
103
103
|
const companyDir = resolveCompanyDir();
|
|
104
104
|
const ownerPath = path.join(companyDir, 'OWNER');
|
|
105
105
|
const cancelPath = path.join(companyDir, 'CANCEL');
|
|
106
|
+
const handoffPath = path.join(companyDir, 'HANDOFF');
|
|
106
107
|
|
|
107
108
|
// Respect CANCEL: a cancelled run must not be force-restarted.
|
|
108
109
|
try {
|
|
@@ -111,6 +112,15 @@ try {
|
|
|
111
112
|
process.exit(0);
|
|
112
113
|
}
|
|
113
114
|
|
|
115
|
+
// A handed-off session is never re-demanded a restart (symmetric with stop-guard).
|
|
116
|
+
try {
|
|
117
|
+
if (sessionId) {
|
|
118
|
+
const handed = fs.readFileSync(handoffPath, 'utf8')
|
|
119
|
+
.split('\n').map(function (l) { return l.trim(); }).filter(Boolean);
|
|
120
|
+
if (handed.indexOf(sessionId) !== -1) process.exit(0);
|
|
121
|
+
}
|
|
122
|
+
} catch (e) {}
|
|
123
|
+
|
|
114
124
|
// Check session ownership before doing any work.
|
|
115
125
|
// Null sessionId with a clean OWNER list = unidentifiable session, fail-open (allow).
|
|
116
126
|
// Null sessionId with no OWNER / garbled OWNER = legacy gate-all mode, fall through.
|
|
@@ -159,22 +169,54 @@ try {
|
|
|
159
169
|
// No usage found: fail-open.
|
|
160
170
|
if (!lastUsage) process.exit(0);
|
|
161
171
|
|
|
172
|
+
// Token-sum fill must match the dashboard gauge + statusline EXACTLY:
|
|
173
|
+
// input + cache_read + cache_creation + output. Omitting output_tokens made the
|
|
174
|
+
// guard compute a LOWER % than the number the user sees, so a session displayed at
|
|
175
|
+
// >= threshold could sit just under the guard and never fire (criterion 38).
|
|
162
176
|
const used =
|
|
163
177
|
(lastUsage.input_tokens || 0) +
|
|
164
178
|
(lastUsage.cache_read_input_tokens || 0) +
|
|
165
|
-
(lastUsage.cache_creation_input_tokens || 0)
|
|
179
|
+
(lastUsage.cache_creation_input_tokens || 0) +
|
|
180
|
+
(lastUsage.output_tokens || 0);
|
|
166
181
|
|
|
167
182
|
const contextWindow = detectWindow(lastModelId);
|
|
168
183
|
const threshold = parseThreshold();
|
|
169
184
|
const hardCeiling = parseHardCeiling(threshold);
|
|
170
|
-
|
|
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
|
-
//
|
|
175
|
-
//
|
|
176
|
-
//
|
|
177
|
-
// The
|
|
197
|
+
// Per-session SOFT toggle (founder decision): the dashboard pill can turn OFF the early
|
|
198
|
+
// 50% soft restart for THIS session only. Pref lives in <companyDir>/restart-pref.json as
|
|
199
|
+
// { sessions: { "<sid>": { soft: false } } }; default (absent/true/garbled) = ON.
|
|
200
|
+
// The toggle suppresses ONLY the band between the soft threshold and the hard ceiling.
|
|
201
|
+
// The 80% hard ceiling (parseHardCeiling) stays ALWAYS active even when soft is off, so a
|
|
202
|
+
// runaway session still hard-blocks. Below the hard ceiling, soft:false allows the stop.
|
|
203
|
+
function softRestartOff(dir, sid) {
|
|
204
|
+
if (!sid) return false; // unidentifiable session never reads a per-session pref
|
|
205
|
+
try {
|
|
206
|
+
const raw = fs.readFileSync(path.join(dir, 'restart-pref.json'), 'utf8');
|
|
207
|
+
const parsed = JSON.parse(raw);
|
|
208
|
+
if (!parsed || typeof parsed !== 'object' || !parsed.sessions) return false;
|
|
209
|
+
const entry = parsed.sessions[sid];
|
|
210
|
+
return !!(entry && entry.soft === false);
|
|
211
|
+
} catch (e) {
|
|
212
|
+
return false; // missing or garbled pref = ON (fail toward the restart)
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
if (fill < hardCeiling && softRestartOff(companyDir, sessionId)) process.exit(0);
|
|
216
|
+
|
|
217
|
+
// At or above the hard ceiling the restart block is UNCONDITIONAL: the per-session soft
|
|
218
|
+
// toggle above can never reach here. Between soft and hard, an ON session falls through and
|
|
219
|
+
// blocks as before. The hard ceiling only changes the wording of the block, not whether it fires.
|
|
178
220
|
|
|
179
221
|
// Fill is at or above threshold. Check the de-loop state file before blocking.
|
|
180
222
|
// De-loop state is SESSION-SCOPED JSON { "sessionId", "tokens", "releasedAtTokens"? }.
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// UserPromptSubmit hook for /company runs: durably captures every operator request
|
|
4
|
+
// into .company/intake-log.md so an ask can never be lost to model memory. This is
|
|
5
|
+
// the mechanical complement to the criteria-gate (stop-guard): the stop-guard keeps
|
|
6
|
+
// a run open until every captured ask is triaged, this hook makes sure the ask is
|
|
7
|
+
// captured in the first place by a hook rather than by the model remembering.
|
|
8
|
+
//
|
|
9
|
+
// CONTRACT: ALWAYS exit 0, never print a decision, fail-OPEN on every error. A
|
|
10
|
+
// UserPromptSubmit hook that blocked or crashed would wedge the operator's own
|
|
11
|
+
// prompt; logging is best-effort and never gates input. The stop-guard does the
|
|
12
|
+
// gating; this hook only records.
|
|
13
|
+
//
|
|
14
|
+
// Owner-scoping mirrors stop-guard.js: if OWNER is clean and non-empty and this
|
|
15
|
+
// session is NOT listed, do not log (a foreign session sharing the dir must not
|
|
16
|
+
// pollute this run's intake log). A garbled or missing OWNER falls through and logs
|
|
17
|
+
// (fail-open: better a spurious line than a dropped ask).
|
|
18
|
+
|
|
19
|
+
const fs = require('fs');
|
|
20
|
+
const path = require('path');
|
|
21
|
+
const crypto = require('crypto');
|
|
22
|
+
|
|
23
|
+
// Resolve companyDir robustly: COMPANY_DIR env wins; else prefer the dir that holds
|
|
24
|
+
// a clean OWNER (at least one valid session-id line); fall back to cwd/.company.
|
|
25
|
+
// A blank/garbled OWNER does NOT qualify a dir as the active run (BLOCKER-1 fix).
|
|
26
|
+
function hasCleanOwner(ownerPath) {
|
|
27
|
+
try {
|
|
28
|
+
const lines = fs.readFileSync(ownerPath, 'utf8')
|
|
29
|
+
.split('\n').map(function (l) { return l.trim(); }).filter(Boolean);
|
|
30
|
+
return lines.length > 0 &&
|
|
31
|
+
lines.every(function (l) { return /^[A-Za-z0-9][A-Za-z0-9._-]{7,}$/.test(l); });
|
|
32
|
+
} catch (e) { return false; }
|
|
33
|
+
}
|
|
34
|
+
function resolveCompanyDir() {
|
|
35
|
+
if (process.env.COMPANY_DIR) return process.env.COMPANY_DIR;
|
|
36
|
+
const home = process.env.HOME || '';
|
|
37
|
+
const cwdDir = path.join(process.cwd(), '.company');
|
|
38
|
+
const homeDir = path.join(home, '.company');
|
|
39
|
+
const cwdHasOwner = hasCleanOwner(path.join(cwdDir, 'OWNER'));
|
|
40
|
+
const homeHasOwner = home && hasCleanOwner(path.join(homeDir, 'OWNER'));
|
|
41
|
+
// cwd/.company wins when it has a clean OWNER (project-local run, or both have OWNER).
|
|
42
|
+
if (cwdHasOwner) return cwdDir;
|
|
43
|
+
if (homeHasOwner) return homeDir;
|
|
44
|
+
return cwdDir; // new-run default: preserves original single-project behavior
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function main() {
|
|
48
|
+
const companyDir = resolveCompanyDir();
|
|
49
|
+
// No company dir: nothing to log against.
|
|
50
|
+
if (!companyDir || !fs.existsSync(companyDir)) return;
|
|
51
|
+
|
|
52
|
+
let input = null;
|
|
53
|
+
try {
|
|
54
|
+
input = JSON.parse(fs.readFileSync(0, 'utf8'));
|
|
55
|
+
} catch (e) { return; } // malformed stdin: fail-open, no line, no crash.
|
|
56
|
+
if (!input || typeof input !== 'object') return;
|
|
57
|
+
|
|
58
|
+
const sessionId = typeof input.session_id === 'string' ? input.session_id : '';
|
|
59
|
+
const prompt = input.prompt;
|
|
60
|
+
// Missing/empty/non-string prompt: nothing to capture.
|
|
61
|
+
if (typeof prompt !== 'string' || prompt.trim() === '') return;
|
|
62
|
+
|
|
63
|
+
// Owner-scope: clean+non-empty OWNER that does NOT list this session -> do not log.
|
|
64
|
+
// Garbled/missing OWNER -> fall through (log). Mirrors stop-guard's regex guard.
|
|
65
|
+
try {
|
|
66
|
+
const ownerPath = path.join(companyDir, 'OWNER');
|
|
67
|
+
const rawOwners = fs.readFileSync(ownerPath, 'utf8')
|
|
68
|
+
.split('\n').map(function (l) { return l.trim(); }).filter(Boolean);
|
|
69
|
+
const valid = rawOwners.filter(function (l) { return /^[A-Za-z0-9][A-Za-z0-9._-]{7,}$/.test(l); });
|
|
70
|
+
// Clean OWNER (every line valid, at least one line): scope strictly.
|
|
71
|
+
if (rawOwners.length > 0 && valid.length === rawOwners.length) {
|
|
72
|
+
if (valid.indexOf(sessionId) === -1) return; // foreign session, do not log.
|
|
73
|
+
}
|
|
74
|
+
// Garbled OWNER (some invalid line) or missing OWNER (read threw): fall through.
|
|
75
|
+
} catch (e) { /* no OWNER file: fall through and log */ }
|
|
76
|
+
|
|
77
|
+
// Stable id: first 8 hex of sha256(timestamp + sessionId + prompt).
|
|
78
|
+
const ts = new Date().toISOString();
|
|
79
|
+
const id8 = crypto.createHash('sha256')
|
|
80
|
+
.update(ts + sessionId + prompt).digest('hex').slice(0, 8);
|
|
81
|
+
|
|
82
|
+
// One-line the prompt: newlines -> space, pipe -> '/', collapse whitespace, truncate 500.
|
|
83
|
+
let oneLine = prompt.replace(/[\r\n]+/g, ' ').replace(/\|/g, '/').replace(/\s+/g, ' ').trim();
|
|
84
|
+
if (oneLine.length > 500) oneLine = oneLine.slice(0, 500);
|
|
85
|
+
|
|
86
|
+
const logPath = path.join(companyDir, 'intake-log.md');
|
|
87
|
+
const header =
|
|
88
|
+
'<!-- company intake log: append-only operator-request capture. Each line is one ' +
|
|
89
|
+
'UserPromptSubmit event. `- [ ]` = un-triaged, the THINK phase folds it into a ' +
|
|
90
|
+
'criterion and rewrites the marker: `- [x] captured: <criterion-id>`, ' +
|
|
91
|
+
'`- [~] declined: <reason>`, or `- [>] deferred: <target>`. The stop-guard blocks ' +
|
|
92
|
+
'a stop while any stale `- [ ]` line remains. Do not hand-edit the id or timestamp. -->\n';
|
|
93
|
+
const line = '- [ ] ' + ts + ' ' + id8 + ' | ' + sessionId + ' | ' + oneLine + '\n';
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
if (!fs.existsSync(logPath)) {
|
|
97
|
+
fs.writeFileSync(logPath, header);
|
|
98
|
+
}
|
|
99
|
+
fs.appendFileSync(logPath, line);
|
|
100
|
+
} catch (e) { /* fail-open: a write error must never gate the operator's prompt */ }
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
try { main(); } catch (e) { /* fail-open on anything unexpected */ }
|
|
104
|
+
process.exit(0);
|