company-skill 4.3.0 → 4.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,30 +1,54 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- // Restore company state after compaction. Reads the checkpoint with reasoning + state.
3
+ // Restore company state after compaction and drive the /company restart handoff.
4
+ // PreCompact cannot make the model emit a prompt (shell-only, no model turn before
5
+ // compaction), so the reliable trigger is here: right after compaction the model is
6
+ // instructed to run /company restart (its mandatory verify and debate procedure)
7
+ // and emit the handoff.
8
+ //
9
+ // SessionStart context must go through hookSpecificOutput.additionalContext.
10
+ // That field reaches the model. systemMessage is a user-facing display only and
11
+ // never enters the model's context.
4
12
 
5
13
  const fs = require('fs');
6
14
  const path = require('path');
7
15
 
8
- const companyDir = path.join(process.cwd(), '.company');
16
+ const companyDir = process.env.COMPANY_DIR || path.join(process.cwd(), '.company');
9
17
  if (!fs.existsSync(companyDir)) process.exit(0);
10
18
 
11
- const checkpointMd = path.join(companyDir, '.checkpoint.md');
12
- const checkpointJson = path.join(companyDir, '.checkpoint.json');
13
-
14
- let msg = null;
19
+ // Only sessions that own the run are acted on. A foreign session that merely
20
+ // shares the directory must not be redirected or have state written on its
21
+ // behalf. Missing or empty OWNER is legacy state and keeps the old behavior.
22
+ try {
23
+ const hookInput = JSON.parse(fs.readFileSync(0, 'utf8'));
24
+ if (hookInput && typeof hookInput.session_id === 'string') {
25
+ const owners = fs.readFileSync(path.join(companyDir, 'OWNER'), 'utf8')
26
+ .split('\n').map(function (l) { return l.trim(); }).filter(Boolean);
27
+ if (owners.length > 0 && owners.indexOf(hookInput.session_id) === -1) process.exit(0);
28
+ }
29
+ } catch (e) {}
15
30
 
31
+ const checkpointMd = path.join(companyDir, '.checkpoint.md');
32
+ let state = '';
16
33
  if (fs.existsSync(checkpointMd)) {
17
- msg = fs.readFileSync(checkpointMd, 'utf8').substring(0, 2000);
18
- } else if (fs.existsSync(checkpointJson)) {
19
- try {
20
- const cp = JSON.parse(fs.readFileSync(checkpointJson, 'utf8'));
21
- msg = "[COMPANY RESTORED] Goal: " + (cp.goal || "unknown") +
22
- ", Cycle: " + (cp.cycle || 0) +
23
- ", Criteria: " + (cp.passing || 0) + "/" + (cp.total || 0) +
24
- ". Read .company/criteria.json and continue.";
25
- } catch (e) {}
34
+ state = fs.readFileSync(checkpointMd, 'utf8').substring(0, 2000);
26
35
  }
27
36
 
28
- if (msg) {
29
- console.log(JSON.stringify({ systemMessage: msg }));
30
- }
37
+ // The post-compaction directive: run the restart procedure, do not just "continue".
38
+ const directive =
39
+ '[COMPANY] Context was compacted, so prior turn-by-turn state is gone. Before doing ' +
40
+ 'anything else, run the /company restart procedure from the skill: refresh ' +
41
+ '.company/criteria.json, .company/STATUS.md and .company/NEXT.md, run the mandatory ' +
42
+ "Source-Verifier + Devil's-Advocate + Completeness debate to re-derive every claim " +
43
+ 'live (trust nothing the checkpoint asserts), then emit ONLY the single ' +
44
+ 'self-contained handoff prompt block with no trailing commentary. The ' +
45
+ 'pre-compaction checkpoint and the pending backlog are in .company/.checkpoint.md ' +
46
+ 'and .company/NEXT.md. Read them first.';
47
+
48
+ const msg = state ? directive + '\n\n--- pre-compaction checkpoint ---\n' + state : directive;
49
+ console.log(JSON.stringify({
50
+ hookSpecificOutput: {
51
+ hookEventName: 'SessionStart',
52
+ additionalContext: msg
53
+ }
54
+ }));
@@ -1,64 +1,154 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ // Stop gate for /company runs: blocks the stop while any criterion fails
4
+ // or lacks evidence. Fail closed on bad input (unparseable or wrong-shape
5
+ // criteria.json blocks). The cancel file is the HUMAN's exit and block
6
+ // reasons never name it. criteria.lock pins the id set: deleting a hard
7
+ // criterion blocks instead of unlocking. Staleness is surfaced, never a
8
+ // free pass. A harness force-stop leaves the run visibly failed.
9
+
3
10
  const fs = require('fs');
4
11
  const path = require('path');
5
12
 
6
- const cwd = process.cwd();
7
- const companyDir = path.join(cwd, '.company');
13
+ const companyDir = process.env.COMPANY_DIR || path.join(process.cwd(), '.company');
8
14
  const criteriaPath = path.join(companyDir, 'criteria.json');
9
15
  const goalPath = path.join(companyDir, 'GOAL.md');
10
16
  const cancelPath = path.join(companyDir, 'CANCEL');
11
- const lockPath = path.join(companyDir, '.stop-lock');
17
+ const ownerPath = path.join(companyDir, 'OWNER');
12
18
 
13
- // No company running
14
- if (!fs.existsSync(goalPath) && !fs.existsSync(criteriaPath)) process.exit(0);
19
+ // Session scoping: only sessions listed in .company/OWNER are gated.
20
+ // Missing or empty OWNER = legacy state, every session gated (fail closed).
21
+ // Manual escape for legacy: ~/.claude/hooks/company-guard-exempt.txt.
22
+ let sessionId = null;
23
+ try {
24
+ const input = JSON.parse(fs.readFileSync(0, 'utf8'));
25
+ if (input && typeof input.session_id === 'string') sessionId = input.session_id;
26
+ } catch (e) {}
27
+ if (sessionId) {
28
+ try {
29
+ const exempt = fs.readFileSync(path.join(
30
+ process.env.HOME || '', '.claude', 'hooks', 'company-guard-exempt.txt'), 'utf8');
31
+ if (exempt.split('\n').some(function (l) { return l.trim() === sessionId; })) process.exit(0);
32
+ } catch (e) {}
33
+ try {
34
+ const rawOwners = fs.readFileSync(ownerPath, 'utf8')
35
+ .split('\n').map(function (l) { return l.trim(); }).filter(Boolean);
36
+ // Garbled OWNER fails closed: only a clean id list frees a foreign session.
37
+ const valid = rawOwners.filter(function (l) { return /^[A-Za-z0-9][A-Za-z0-9._-]{7,}$/.test(l); });
38
+ if (rawOwners.length > 0 && valid.length === rawOwners.length &&
39
+ valid.indexOf(sessionId) === -1) process.exit(0);
40
+ } catch (e) {}
41
+ }
15
42
 
16
- // Cancel signal
17
- if (fs.existsSync(cancelPath)) {
18
- try { fs.unlinkSync(cancelPath); } catch (e) {}
19
- try { fs.unlinkSync(lockPath); } catch (e) {}
43
+ const STALE_MS = 24 * 60 * 60 * 1000;
44
+
45
+ function block(reason) {
46
+ // Tag the session id so a wrongly gated session can be exempted exactly.
47
+ const tag = sessionId ? ' [session ' + sessionId + ']' : '';
48
+ console.log(JSON.stringify({ decision: 'block', reason: '[COMPANY] ' + reason + tag }));
20
49
  process.exit(0);
21
50
  }
22
51
 
23
- // If lock file exists and is recent (< 60s), this is a repeat stop. Allow it.
24
- if (fs.existsSync(lockPath)) {
52
+ // Returns a warning suffix for the block reason when the file is stale,
53
+ // otherwise an empty string. Never used to allow a stop.
54
+ function staleNote(p) {
25
55
  try {
26
- const age = Date.now() - fs.statSync(lockPath).mtimeMs;
27
- if (age < 60000) {
28
- fs.unlinkSync(lockPath);
29
- process.exit(0);
56
+ const ms = Date.now() - fs.statSync(p).mtimeMs;
57
+ if (ms > STALE_MS) {
58
+ return ' NOTE: this state file has been untouched for ' +
59
+ Math.round(ms / 3600000) + ' hours. If it is a leftover from an old ' +
60
+ 'run, a human operator can cancel it (see the README)';
30
61
  }
31
62
  } catch (e) {}
63
+ return '';
64
+ }
65
+
66
+ // No company running
67
+ if (!fs.existsSync(goalPath) && !fs.existsSync(criteriaPath)) process.exit(0);
68
+
69
+ // Cancel signal: the only escape hatch
70
+ if (fs.existsSync(cancelPath)) {
71
+ try { fs.unlinkSync(cancelPath); } catch (e) {}
72
+ process.exit(0);
32
73
  }
33
74
 
34
- // Check criteria
35
75
  if (fs.existsSync(criteriaPath)) {
76
+ const stale = staleNote(criteriaPath);
77
+
78
+ let data;
36
79
  try {
37
- const data = JSON.parse(fs.readFileSync(criteriaPath, 'utf8'));
38
- const all = data.criteria || [];
39
- const failing = all.filter(c => !c.passes || !c.evidence);
80
+ data = JSON.parse(fs.readFileSync(criteriaPath, 'utf8'));
81
+ } catch (e) {
82
+ // Fail closed: broken JSON is not a free pass out of the gate.
83
+ block('criteria.json is unparseable. Repair the JSON so the criteria can be ' +
84
+ 'checked honestly.' + stale);
85
+ }
40
86
 
41
- if (all.length > 0 && failing.length === 0) {
42
- try { fs.unlinkSync(lockPath); } catch (e) {}
43
- process.exit(0);
44
- }
87
+ // Fail closed on the wrong shape too: a parseable file whose criteria
88
+ // field is not an array would otherwise throw below and let the stop slip.
89
+ if (!data || typeof data !== 'object' || !Array.isArray(data.criteria)) {
90
+ block('criteria.json has the wrong shape: "criteria" must be an array of ' +
91
+ '{id, description, passes, evidence} objects. Repair it so the criteria ' +
92
+ 'can be checked honestly.' + stale);
93
+ }
45
94
 
46
- // Write lock, block this stop
47
- fs.writeFileSync(lockPath, String(Date.now()));
48
- const failList = failing.map(c => c.description).join(', ');
49
- console.log(JSON.stringify({
50
- decision: "block",
51
- reason: "[COMPANY] " + failing.length + "/" + all.length + " criteria not met: " + failList + ". Continue THINK > EXECUTE > VERIFY."
52
- }));
53
- process.exit(0);
54
- } catch (e) {
55
- process.exit(0);
95
+ const all = data.criteria;
96
+
97
+ if (all.length === 0) {
98
+ block('criteria.json has zero criteria. Write real yes/no checkable criteria ' +
99
+ 'for the goal.' + stale);
100
+ }
101
+
102
+ // criteria.lock: first sight snapshots ids, removal blocks, additions extend.
103
+ const lockPath = path.join(companyDir, 'criteria.lock');
104
+ const currentIds = all
105
+ .filter(function (c) { return c && typeof c === 'object' && c.id !== undefined && c.id !== null; })
106
+ .map(function (c) { return String(c.id); });
107
+ let lockedIds = null;
108
+ try {
109
+ lockedIds = fs.readFileSync(lockPath, 'utf8')
110
+ .split('\n').map(function (l) { return l.trim(); }).filter(Boolean);
111
+ } catch (e) {}
112
+ if (lockedIds === null) {
113
+ try { fs.writeFileSync(lockPath, currentIds.join('\n') + '\n'); } catch (e) {}
114
+ } else {
115
+ const missing = lockedIds.filter(function (id) { return currentIds.indexOf(id) === -1; });
116
+ if (missing.length > 0) {
117
+ block('locked criterion id(s) removed from criteria.json: ' + missing.join(', ') +
118
+ '. Criteria are never deleted to satisfy the gate; restore them and meet them.' +
119
+ stale);
120
+ }
121
+ const added = currentIds.filter(function (id) { return lockedIds.indexOf(id) === -1; });
122
+ if (added.length > 0) {
123
+ try { fs.writeFileSync(lockPath, lockedIds.concat(added).join('\n') + '\n'); } catch (e) {}
124
+ }
56
125
  }
126
+
127
+ // passes:true requires non-null evidence. The VERIFY phase writes the
128
+ // reproduced evidence string when it flips a criterion to passing.
129
+ // A null or non-object entry counts as failing, never as a crash.
130
+ const failing = all.filter(c => !c || typeof c !== 'object' || !c.passes || !c.evidence);
131
+
132
+ if (failing.length === 0) process.exit(0);
133
+
134
+ // Surface the reviewer's note per failing criterion so the block reason is
135
+ // actionable feedback, not just a name list.
136
+ const failList = failing.map(c => {
137
+ if (!c || typeof c !== 'object') return '(malformed entry)';
138
+ const note = typeof c.note === 'string' && c.note.trim() ? ' [' + c.note.trim().slice(0, 120) + ']' : '';
139
+ return (c.description || '(no description)') + note;
140
+ }).join(', ');
141
+ let goalLine = '';
142
+ try {
143
+ goalLine = fs.readFileSync(goalPath, 'utf8').split('\n').find(function (l) { return l.trim(); }) || '';
144
+ if (goalLine) goalLine = 'GOAL: ' + goalLine.trim().slice(0, 100) + ' | ';
145
+ } catch (e) {}
146
+ block(goalLine + failing.length + '/' + all.length + ' criteria not met: ' + failList +
147
+ '. Continue THINK > EXECUTE > VERIFY. passes:true counts only with non-null ' +
148
+ 'evidence reproduced by the reviewer.' + stale);
57
149
  }
58
150
 
59
- // No criteria but goal exists block once
60
- fs.writeFileSync(lockPath, String(Date.now()));
61
- console.log(JSON.stringify({
62
- decision: "block",
63
- reason: "[COMPANY] Goal not achieved. Create criteria.json and start THINK > EXECUTE > VERIFY."
64
- }));
151
+ // Goal exists but criteria.json was never written
152
+ block('Goal not achieved. Create .company/criteria.json and start ' +
153
+ 'THINK > EXECUTE > VERIFY.' +
154
+ staleNote(goalPath));
package/install.sh CHANGED
@@ -1,37 +1,102 @@
1
1
  #!/bin/bash
2
+ # Installs the /company skill, commands, agents, AND hooks. Idempotent:
3
+ # re-running overwrites the copied files and never duplicates hook entries.
2
4
  set -e
3
5
 
4
6
  REPO="https://raw.githubusercontent.com/jagmarques/company-skill/main"
5
7
 
6
- # Install skill globally
8
+ fetch() {
9
+ curl -fsSL "$1" -o "$2" 2>/dev/null || wget -q "$1" -O "$2" 2>/dev/null
10
+ }
11
+
12
+ command -v curl >/dev/null 2>&1 || command -v wget >/dev/null 2>&1 || {
13
+ echo "Error: curl or wget required"
14
+ exit 1
15
+ }
16
+
17
+ # Skill
7
18
  mkdir -p "$HOME/.claude/skills/company"
8
- curl -sL "$REPO/skill/SKILL.md" -o "$HOME/.claude/skills/company/SKILL.md" 2>/dev/null || \
9
- wget -q "$REPO/skill/SKILL.md" -O "$HOME/.claude/skills/company/SKILL.md" 2>/dev/null || \
10
- { echo "Error: curl or wget required"; exit 1; }
19
+ fetch "$REPO/skill/SKILL.md" "$HOME/.claude/skills/company/SKILL.md" || {
20
+ echo "Error: could not download SKILL.md"
21
+ exit 1
22
+ }
11
23
 
12
- # Install commands globally
24
+ # Commands
13
25
  mkdir -p "$HOME/.claude/commands/company"
14
26
  for cmd in run status resume; do
15
- curl -sL "$REPO/commands/$cmd.md" -o "$HOME/.claude/commands/company/$cmd.md" 2>/dev/null || true
27
+ fetch "$REPO/commands/$cmd.md" "$HOME/.claude/commands/company/$cmd.md" || echo "Warning: failed to download command $cmd"
16
28
  done
17
29
 
18
- # Install agents globally
30
+ # Agents
19
31
  mkdir -p "$HOME/.claude/agents"
20
32
  for agent in lead worker reviewer critic digest; do
21
- curl -sL "$REPO/agents/company-$agent.md" -o "$HOME/.claude/agents/company-$agent.md" 2>/dev/null || true
33
+ fetch "$REPO/agents/company-$agent.md" "$HOME/.claude/agents/company-$agent.md" || echo "Warning: failed to download agent company-$agent"
22
34
  done
23
35
 
36
+ # Hooks (the stop gate and the compaction machinery live here)
37
+ mkdir -p "$HOME/.claude/hooks"
38
+ for pair in "stop-guard company-stop-guard" "precompact company-precompact" "session-restore company-session-restore"; do
39
+ src="${pair%% *}"
40
+ dest="${pair##* }"
41
+ fetch "$REPO/hooks/$src.js" "$HOME/.claude/hooks/$dest.js" || echo "Warning: failed to download hook $src"
42
+ done
43
+
44
+ # Register hooks in ~/.claude/settings.json. JSON editing from shell is not
45
+ # safe without a JSON tool, so use node when available and print exact manual
46
+ # steps otherwise. The merge is idempotent: existing entries are kept.
47
+ if command -v node >/dev/null 2>&1; then
48
+ node <<'EOF'
49
+ const fs = require('fs');
50
+ const path = require('path');
51
+ const os = require('os');
52
+ const home = os.homedir();
53
+ const hooksDir = path.join(home, '.claude', 'hooks');
54
+ const settingsPath = path.join(home, '.claude', 'settings.json');
55
+ let settings = {};
56
+ try { settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8')); } catch (e) {}
57
+ if (!settings.hooks) settings.hooks = {};
58
+ function ensure(event, marker, entry) {
59
+ if (!settings.hooks[event]) settings.hooks[event] = [];
60
+ const present = settings.hooks[event].some(h => (h.hooks || []).some(hh => (hh.command || '').includes(marker)));
61
+ if (!present) settings.hooks[event].push(entry);
62
+ }
63
+ ensure('Stop', 'company-stop-guard', { hooks: [{ type: 'command', command: `node "${path.join(hooksDir, 'company-stop-guard.js')}"`, timeout: 10 }] });
64
+ ensure('PreCompact', 'company-precompact', { hooks: [{ type: 'command', command: `node "${path.join(hooksDir, 'company-precompact.js')}"`, timeout: 10 }] });
65
+ ensure('SessionStart', 'company-session-restore', { matcher: 'compact', hooks: [{ type: 'command', command: `node "${path.join(hooksDir, 'company-session-restore.js')}"`, timeout: 10 }] });
66
+ fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
67
+ const tmp = settingsPath + '.tmp';
68
+ fs.writeFileSync(tmp, JSON.stringify(settings, null, 2));
69
+ fs.renameSync(tmp, settingsPath);
70
+ console.log('Hooks registered in ' + settingsPath);
71
+ EOF
72
+ else
73
+ cat <<EOF
74
+
75
+ node was not found, so the hook files were copied but NOT registered.
76
+ The hooks need node at runtime anyway, so install node, then add this to
77
+ the "hooks" section of ~/.claude/settings.json (merge with what is there):
78
+
79
+ "Stop": [{ "hooks": [{ "type": "command", "command": "node \"$HOME/.claude/hooks/company-stop-guard.js\"", "timeout": 10 }] }],
80
+ "PreCompact": [{ "hooks": [{ "type": "command", "command": "node \"$HOME/.claude/hooks/company-precompact.js\"", "timeout": 10 }] }],
81
+ "SessionStart": [{ "matcher": "compact", "hooks": [{ "type": "command", "command": "node \"$HOME/.claude/hooks/company-session-restore.js\"", "timeout": 10 }] }]
82
+
83
+ EOF
84
+ fi
85
+
24
86
  # Create COMPANY.md template in current directory if missing
25
87
  if [ ! -f "COMPANY.md" ]; then
26
- curl -sL "$REPO/COMPANY.md.template" -o "COMPANY.md" 2>/dev/null || true
88
+ fetch "$REPO/COMPANY.md.template" "COMPANY.md" || true
27
89
  [ -f "COMPANY.md" ] && echo "Created COMPANY.md template. Edit it with your team."
28
90
  fi
29
91
 
30
- # Gitignore .company/
31
- grep -q "^\.company/" .gitignore 2>/dev/null || echo ".company/" >> .gitignore 2>/dev/null || true
92
+ # Gitignore .company/ (only inside a git repo)
93
+ if [ -d ".git" ]; then
94
+ grep -q "^\.company/" .gitignore 2>/dev/null || echo ".company/" >> .gitignore
95
+ fi
32
96
 
33
- echo "Installed globally. Available commands:"
97
+ echo "Installed. Available commands:"
34
98
  echo " /company Main skill"
35
99
  echo " /company:run Run with a goal"
36
100
  echo " /company:status Check status"
37
101
  echo " /company:resume Continue from last session"
102
+ echo "Cancel a run: touch .company/CANCEL"
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "company-skill",
3
- "version": "4.3.0",
3
+ "version": "4.5.0",
4
4
  "description": "Goal-driven multi-employee company for Claude Code. Give it a goal, it runs until done.",
5
5
  "bin": {
6
6
  "company-skill": "./bin/install.js"
7
7
  },
8
- "keywords": ["claude-code", "skill", "multi-agent", "company", "orchestration"],
8
+ "keywords": ["claude-code", "skill", "multi-agent", "company", "orchestration", "model-agnostic", "autonomous-agents", "stop-hook", "agent-orchestration", "ai-agents", "fable", "claude-fable"],
9
9
  "author": "jagmarques",
10
10
  "license": "MIT",
11
11
  "repository": {
@@ -0,0 +1,55 @@
1
+ #!/usr/bin/env node
2
+ // Mechanical gate for delegation contracts: every TASK block must carry all
3
+ // seven fields and a non-empty VERIFY-WITH. Run before EXECUTE:
4
+ // node scripts/check-contracts.js .company/cycles/cycle-N-tasks.md
5
+ // Exit 1 lists each defective contract. No fields, no task.
6
+ const fs = require('fs');
7
+ const file = process.argv[2];
8
+ if (!file || !fs.existsSync(file)) { console.error('usage: check-contracts.js <tasks-file>'); process.exit(2); }
9
+ const text = fs.readFileSync(file, 'utf8');
10
+ const blocks = text.split(/\n(?=TASK:)/).filter(b => b.trim().startsWith('TASK:'));
11
+ if (blocks.length === 0) { console.error('no TASK blocks found'); process.exit(1); }
12
+ const FIELDS = ['TASK:', 'EMPLOYEE:', 'SKILL:', 'INPUTS:', 'OUTPUT:', 'DONE-WHEN:', 'VERIFY-WITH:', 'OUT-OF-SCOPE:'];
13
+ // DEPENDS-ON is optional. When present it must name existing task numbers
14
+ // and the dependency graph must be acyclic.
15
+ function checkDeps(blocks) {
16
+ const errs = [];
17
+ const deps = blocks.map((b, i) => {
18
+ const m = b.match(/DEPENDS-ON:\s*(.+)/);
19
+ if (!m) return [];
20
+ const v = m[1].trim();
21
+ if (/^none$/i.test(v)) return [];
22
+ if (!/^\d+(\s*(,|and)\s*\d+)*$/i.test(v)) {
23
+ errs.push('contract ' + (i + 1) + ': DEPENDS-ON must be "none" or task numbers, got: ' + v.slice(0, 40));
24
+ return [];
25
+ }
26
+ return v.match(/\d+/g).map(Number);
27
+ });
28
+ deps.forEach((ds, i) => ds.forEach(d => {
29
+ if (d < 1 || d > blocks.length) errs.push('contract ' + (i + 1) + ': DEPENDS-ON names missing task ' + d);
30
+ if (d === i + 1) errs.push('contract ' + (i + 1) + ': depends on itself');
31
+ }));
32
+ const state = new Array(blocks.length).fill(0);
33
+ function visit(i, trail) {
34
+ if (state[i] === 1) { errs.push('dependency cycle: ' + trail.concat(i + 1).join(' -> ')); return; }
35
+ if (state[i] === 2) return;
36
+ state[i] = 1;
37
+ deps[i].forEach(d => { if (d >= 1 && d <= blocks.length) visit(d - 1, trail.concat(i + 1)); });
38
+ state[i] = 2;
39
+ }
40
+ for (let i = 0; i < blocks.length; i++) visit(i, []);
41
+ return errs;
42
+ }
43
+ let bad = 0;
44
+ blocks.forEach((b, i) => {
45
+ const missing = FIELDS.filter(f => !b.includes(f));
46
+ const vw = (b.split('VERIFY-WITH:')[1] || '').split('\n')[0].trim();
47
+ const errs = [];
48
+ if (missing.length) errs.push('missing ' + missing.join(' '));
49
+ if (b.includes('VERIFY-WITH:') && vw.length < 8) errs.push('VERIFY-WITH is empty or vacuous');
50
+ if (errs.length) { bad += 1; console.error('contract ' + (i + 1) + ': ' + errs.join(', ')); }
51
+ });
52
+ const depErrs = checkDeps(blocks);
53
+ depErrs.forEach(e => console.error(e));
54
+ if (bad || depErrs.length) { console.error((bad + depErrs.length) + ' defects across ' + blocks.length + ' contracts'); process.exit(1); }
55
+ console.log(blocks.length + ' contracts well-formed');
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env node
2
+ // Mechanical gate for findings files: every FINDING line must be followed by
3
+ // a SOURCE line (or NOVEL marker) before the next FINDING. Run at VERIFY:
4
+ // node scripts/check-findings.js .company/<dept>/<employee>.md [...]
5
+ // Exit 1 names each unsourced finding. A claim without a source is unverifiable.
6
+ const fs = require('fs');
7
+ const files = process.argv.slice(2);
8
+ if (!files.length) { console.error('usage: check-findings.js <findings-file...>'); process.exit(2); }
9
+ let bad = 0, total = 0;
10
+ files.forEach(file => {
11
+ if (!fs.existsSync(file)) { console.error(file + ': missing'); bad += 1; return; }
12
+ const lines = fs.readFileSync(file, 'utf8').split('\n');
13
+ lines.forEach((l, i) => {
14
+ if (!/^FINDING:/.test(l)) return;
15
+ total += 1;
16
+ for (let j = i + 1; j < lines.length; j++) {
17
+ if (/^FINDING:/.test(lines[j])) break;
18
+ if (/^SOURCE:|NOVEL - needs validation/.test(lines[j])) return;
19
+ }
20
+ bad += 1;
21
+ console.error(file + ':' + (i + 1) + ' FINDING without SOURCE: ' + l.slice(0, 70));
22
+ });
23
+ });
24
+ if (bad) { console.error(bad + ' unsourced findings'); process.exit(1); }
25
+ console.log(total + ' findings all sourced');
@@ -0,0 +1,125 @@
1
+ #!/bin/bash
2
+ # Quality floor for this repo. Run from anywhere: bash scripts/check.sh
3
+ # Checks that every shipped JS file parses, the skill and agent files have
4
+ # frontmatter, and no private or banned content leaks into the public files.
5
+ set -u
6
+
7
+ ROOT="$(cd "$(dirname "$0")/.." && pwd)"
8
+ cd "$ROOT"
9
+ fail=0
10
+
11
+ note_fail() {
12
+ echo "FAIL: $1"
13
+ fail=1
14
+ }
15
+
16
+ # 1. Every hook and the installer must parse as valid JS
17
+ for f in hooks/*.js bin/install.js; do
18
+ if node --check "$f" 2>/dev/null; then
19
+ echo "ok: node --check $f"
20
+ else
21
+ node --check "$f" || true
22
+ note_fail "$f does not parse"
23
+ fi
24
+ done
25
+
26
+ # 2. install.sh must parse as valid shell
27
+ if bash -n install.sh 2>/dev/null; then
28
+ echo "ok: bash -n install.sh"
29
+ else
30
+ note_fail "install.sh does not parse"
31
+ fi
32
+
33
+ # 3. SKILL.md frontmatter exists and names the skill
34
+ if [ "$(head -1 skill/SKILL.md)" = "---" ] && grep -q '^name: company$' skill/SKILL.md; then
35
+ echo "ok: SKILL.md frontmatter"
36
+ else
37
+ note_fail "skill/SKILL.md frontmatter missing or name field absent"
38
+ fi
39
+
40
+ # 4. Every agent file has frontmatter with name and model fields
41
+ for f in agents/*.md; do
42
+ if [ "$(head -1 "$f")" = "---" ] && grep -q '^name: ' "$f" && grep -q '^model: ' "$f"; then
43
+ echo "ok: frontmatter $f"
44
+ else
45
+ note_fail "$f missing frontmatter, name, or model field"
46
+ fi
47
+ done
48
+
49
+ # 5. package.json parses
50
+ if node -e "JSON.parse(require('fs').readFileSync('package.json','utf8'))" 2>/dev/null; then
51
+ echo "ok: package.json parses"
52
+ else
53
+ note_fail "package.json is invalid JSON"
54
+ fi
55
+
56
+ # 6. No private rule-number references (e.g. references to a numbered rule
57
+ # in someone's personal CLAUDE.md) in shipped files
58
+ if grep -rnE 'CLAUDE\.md +(rule +)?[0-9]+\.[0-9]+|\(CLAUDE\.md +[0-9]' \
59
+ --include='*.md' --include='*.js' --include='*.sh' --include='*.template' \
60
+ --exclude-dir=.git --exclude-dir=node_modules --exclude=check.sh .; then
61
+ note_fail "private rule-number reference found"
62
+ else
63
+ echo "ok: no private rule references"
64
+ fi
65
+
66
+ # 7. No bare rule-number references either ("rule 1.2" without naming a
67
+ # file is still a leak from someone's private rule set)
68
+ if grep -rinE 'rule [0-9]+\.[0-9]+' \
69
+ --include='*.md' --include='*.js' --include='*.sh' --include='*.template' \
70
+ --exclude-dir=.git --exclude-dir=node_modules --exclude=check.sh .; then
71
+ note_fail "bare rule-number reference found"
72
+ else
73
+ echo "ok: no bare rule references"
74
+ fi
75
+
76
+ # 8. No hardcoded IP addresses (loopback and wildcard excepted)
77
+ if grep -rnE '\b([0-9]{1,3}\.){3}[0-9]{1,3}\b' \
78
+ --include='*.md' --include='*.js' --include='*.sh' --include='*.template' \
79
+ --exclude-dir=.git --exclude-dir=node_modules --exclude=check.sh . \
80
+ | grep -vE '127\.0\.0\.1|0\.0\.0\.0'; then
81
+ note_fail "hardcoded IP address found"
82
+ else
83
+ echo "ok: no hardcoded IPs"
84
+ fi
85
+
86
+ # 9. No em dashes in shipped files
87
+ EMDASH=$(printf '\342\200\224')
88
+ if grep -rn "$EMDASH" \
89
+ --include='*.md' --include='*.js' --include='*.sh' --include='*.template' \
90
+ --exclude-dir=.git --exclude-dir=node_modules --exclude=check.sh .; then
91
+ note_fail "em dash found"
92
+ else
93
+ echo "ok: no em dashes"
94
+ fi
95
+
96
+ # 10. No leaked operator brand names anywhere in the tree. This is a generic
97
+ # public tool and must never name the company of whoever maintains it.
98
+ # check.sh is excluded only because the banned word list lives here.
99
+ if grep -rin 'asqav' --exclude-dir=.git --exclude-dir=node_modules \
100
+ --exclude=check.sh .; then
101
+ note_fail "leaked brand name found"
102
+ else
103
+ echo "ok: no leaked brand names"
104
+ fi
105
+
106
+ # 11. Stop-guard decision-logic matrix: the fail-closed behavior is load-bearing
107
+ # and must not regress silently behind a green parse check.
108
+ if node tests/stop-guard.test.js; then
109
+ echo "ok: stop-guard decision matrix"
110
+ else
111
+ note_fail "stop-guard decision matrix failed"
112
+ fi
113
+
114
+ # 12. Contract checker matrix: field and DEPENDS-ON validation must hold.
115
+ if node tests/check-contracts.test.js; then
116
+ echo "ok: contract checker matrix"
117
+ else
118
+ note_fail "contract checker matrix failed"
119
+ fi
120
+
121
+ if [ "$fail" -ne 0 ]; then
122
+ echo "CHECKS FAILED"
123
+ exit 1
124
+ fi
125
+ echo "ALL CHECKS PASSED"