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.
- package/.github/workflows/check.yml +16 -0
- package/.github/workflows/publish.yml +18 -0
- package/COMPANY.md.template +5 -2
- package/README.md +53 -156
- package/agents/company-critic.md +28 -4
- package/agents/company-digest.md +17 -2
- package/agents/company-lead.md +32 -3
- package/agents/company-reviewer.md +21 -3
- package/agents/company-worker.md +32 -2
- package/bin/install.js +7 -4
- package/commands/resume.md +6 -1
- package/commands/run.md +2 -1
- package/hooks/precompact.js +18 -4
- package/hooks/session-restore.js +42 -18
- package/hooks/stop-guard.js +129 -39
- package/install.sh +77 -12
- package/package.json +2 -2
- package/scripts/check-contracts.js +55 -0
- package/scripts/check-findings.js +25 -0
- package/scripts/check.sh +125 -0
- package/skill/SKILL.md +187 -50
- package/tests/check-contracts.test.js +31 -0
- package/tests/stop-guard.test.js +278 -0
- package/hooks/compiler-check.js +0 -39
package/hooks/session-restore.js
CHANGED
|
@@ -1,30 +1,54 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
// Restore company state after compaction
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
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
|
-
|
|
29
|
-
|
|
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
|
+
}));
|
package/hooks/stop-guard.js
CHANGED
|
@@ -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
|
|
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
|
|
17
|
+
const ownerPath = path.join(companyDir, 'OWNER');
|
|
12
18
|
|
|
13
|
-
//
|
|
14
|
-
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
//
|
|
24
|
-
|
|
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
|
|
27
|
-
if (
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
//
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
#
|
|
24
|
+
# Commands
|
|
13
25
|
mkdir -p "$HOME/.claude/commands/company"
|
|
14
26
|
for cmd in run status resume; do
|
|
15
|
-
|
|
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
|
-
#
|
|
30
|
+
# Agents
|
|
19
31
|
mkdir -p "$HOME/.claude/agents"
|
|
20
32
|
for agent in lead worker reviewer critic digest; do
|
|
21
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
+
"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');
|
package/scripts/check.sh
ADDED
|
@@ -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"
|