@cohortapp/agent-sdk 2.11.15 → 2.13.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/.env.example +37 -22
- package/README.md +2 -0
- package/bin/maestro.mjs +117 -39
- package/bin/maestro.test.mjs +175 -5
- package/docs/guides/front-door-session.md +313 -0
- package/docs/guides/mac-mini.md +100 -28
- package/docs/guides/org-onboarding.md +1 -1
- package/docs/guides/setup-wizard.md +9 -5
- package/docs/runbooks/cohort-cutover.md +11 -1
- package/docs/runbooks/mac-mini-bootstrap.md +38 -63
- package/lib/cadence-bus-requeue.test.mjs +83 -0
- package/lib/cadence-bus.mjs +43 -7
- package/lib/channels/inbox-item.mjs +59 -2
- package/lib/cli/board.mjs +285 -0
- package/lib/cli/board.test.mjs +227 -0
- package/lib/cli/design.mjs +185 -0
- package/lib/cli/design.test.mjs +270 -0
- package/lib/cli/doctor-checks.mjs +441 -0
- package/lib/cli/doctor-checks.test.mjs +336 -0
- package/lib/cli/global-setup-extras.mjs +454 -0
- package/lib/cli/global-setup-extras.test.mjs +462 -0
- package/lib/cli/inbox.mjs +304 -0
- package/lib/cli/inbox.test.mjs +230 -0
- package/lib/cli/session-ack.mjs +63 -0
- package/lib/cli/session-ack.test.mjs +63 -0
- package/lib/cli/session.mjs +760 -0
- package/lib/cli/session.test.mjs +613 -0
- package/lib/collective/global-config.mjs +209 -6
- package/lib/collective/global-config.test.mjs +145 -0
- package/lib/collective/global-skills.mjs +145 -0
- package/lib/collective/global-skills.test.mjs +126 -0
- package/lib/collective/presence.mjs +4 -3
- package/lib/collective/vendor-skills.mjs +305 -0
- package/lib/collective/vendor-skills.test.mjs +306 -0
- package/lib/comms/send-gate.mjs +115 -0
- package/lib/comms/send-gate.test.mjs +113 -0
- package/lib/design/design-md.mjs +793 -0
- package/lib/design/design-md.test.mjs +318 -0
- package/lib/design/fixtures/DESIGN.golden.md +238 -0
- package/lib/design/fixtures/PRODUCT.golden.md +67 -0
- package/lib/design/fixtures/foundation.json +133 -0
- package/lib/design/refresh-gate.mjs +154 -0
- package/lib/design/refresh-gate.test.mjs +144 -0
- package/lib/design/write.mjs +275 -0
- package/lib/design/write.test.mjs +241 -0
- package/lib/feature-init.mjs +2 -2
- package/lib/mcp/server.test.mjs +9 -4
- package/lib/model-router/spawn.test.mjs +21 -0
- package/lib/org/board-mine-cache.mjs +99 -0
- package/lib/org/board-mine-cache.test.mjs +53 -0
- package/lib/org/board.mjs +11 -0
- package/lib/org/board.test.mjs +11 -1
- package/lib/org/client.mjs +36 -0
- package/lib/org/client.test.mjs +46 -0
- package/lib/org/inbound/directedness.mjs +18 -2
- package/lib/org/inbound/directedness.test.mjs +58 -0
- package/lib/org/inbound/index.mjs +8 -1
- package/lib/org/inbound/index.test.mjs +22 -0
- package/lib/org/mesh-directives.test.mjs +110 -0
- package/lib/org/mesh.mjs +61 -1
- package/lib/org/protocol.checksum +1 -1
- package/lib/org/protocol.mjs +52 -0
- package/lib/org/protocol.test.mjs +12 -1
- package/lib/org/registry.mjs +3 -2
- package/lib/org/tool-surface.mjs +120 -0
- package/lib/org/tool-surface.test.mjs +118 -5
- package/lib/prompts/parallelism.mjs +79 -0
- package/lib/prompts/parallelism.test.mjs +177 -0
- package/lib/security/external-content.mjs +1 -1
- package/lib/security/external-content.test.mjs +17 -0
- package/lib/session/config.mjs +137 -0
- package/lib/session/config.test.mjs +92 -0
- package/lib/session/feed-core.mjs +229 -0
- package/lib/session/feed-core.test.mjs +198 -0
- package/lib/session/first-run.mjs +126 -0
- package/lib/session/first-run.test.mjs +121 -0
- package/lib/session/frontdoor.mjs +266 -0
- package/lib/session/frontdoor.test.mjs +205 -0
- package/lib/session/handoffs.mjs +295 -0
- package/lib/session/handoffs.test.mjs +183 -0
- package/lib/session/identity.mjs +220 -0
- package/lib/session/identity.test.mjs +180 -0
- package/lib/session/inbox-claims.mjs +434 -0
- package/lib/session/inbox-claims.test.mjs +286 -0
- package/lib/session/launch-args.mjs +161 -0
- package/lib/session/launch-args.test.mjs +157 -0
- package/lib/session/liveness.mjs +174 -0
- package/lib/session/liveness.test.mjs +100 -0
- package/lib/session/status-summary.mjs +172 -0
- package/lib/session/status-summary.test.mjs +118 -0
- package/lib/session-permissions.mjs +39 -3
- package/lib/session-permissions.test.mjs +20 -0
- package/lib/setup/claude-probe.mjs +161 -24
- package/lib/setup/claude-probe.test.mjs +187 -0
- package/lib/setup/sections/learning.mjs +2 -1
- package/lib/setup/sections/model.mjs +104 -24
- package/lib/setup/sections/model.test.mjs +240 -0
- package/lib/setup/sections/org.mjs +27 -2
- package/lib/setup/sections/org.test.mjs +35 -2
- package/lib/setup/sections/verify.mjs +5 -0
- package/lib/setup/state.mjs +30 -10
- package/lib/setup/state.test.mjs +24 -1
- package/lib/singleton.js +11 -3
- package/lib/singleton.test.mjs +16 -0
- package/lib/subagents/lock.mjs +1 -1
- package/lib/telemetry/collect.mjs +270 -6
- package/lib/telemetry/collect.test.mjs +196 -1
- package/lib/upgrade/global-refresh.mjs +108 -0
- package/lib/upgrade/global-refresh.test.mjs +65 -0
- package/lib/upgrade/launchd-reconcile.mjs +327 -0
- package/lib/upgrade/launchd-reconcile.test.mjs +272 -0
- package/lib/upgrade/post-steps.mjs +151 -0
- package/lib/upgrade/post-steps.test.mjs +200 -0
- package/lib/upgrade/verify.mjs +215 -0
- package/lib/upgrade/verify.test.mjs +164 -0
- package/lib/voice/outbound.mjs +3 -2
- package/lib/voice/post-call-brief.mjs +2 -1
- package/lib/voice/session-rotation.mjs +6 -1
- package/lib/voice/session-rotation.test.mjs +114 -0
- package/package.json +3 -3
- package/plugins/maestro-skills/plugin.json +25 -1
- package/plugins/maestro-skills/skills/board-work.md +63 -0
- package/plugins/maestro-skills/skills/cohort-design.md +153 -0
- package/plugins/maestro-skills/skills/inbound-triage.md +80 -0
- package/plugins/maestro-skills/skills/main-session.md +102 -0
- package/plugins/maestro-skills/skills/peer-sessions.md +65 -0
- package/plugins/maestro-skills/skills/persona-discipline.md +75 -0
- package/plugins/maestro-skills/vendor/emilkowalski/LICENSE +21 -0
- package/plugins/maestro-skills/vendor/emilkowalski/UPSTREAM.json +70 -0
- package/plugins/maestro-skills/vendor/emilkowalski/skills/animate/RECIPES.md +324 -0
- package/plugins/maestro-skills/vendor/emilkowalski/skills/animate/SKILL.md +199 -0
- package/plugins/maestro-skills/vendor/emilkowalski/skills/animation-vocabulary/SKILL.md +173 -0
- package/plugins/maestro-skills/vendor/emilkowalski/skills/apple-design/SKILL.md +282 -0
- package/plugins/maestro-skills/vendor/emilkowalski/skills/emil-design-eng/SKILL.md +674 -0
- package/plugins/maestro-skills/vendor/emilkowalski/skills/find-animation-opportunities/SKILL.md +132 -0
- package/plugins/maestro-skills/vendor/emilkowalski/skills/improve-animations/AUDIT.md +115 -0
- package/plugins/maestro-skills/vendor/emilkowalski/skills/improve-animations/PLAN-TEMPLATE.md +73 -0
- package/plugins/maestro-skills/vendor/emilkowalski/skills/improve-animations/SKILL.md +101 -0
- package/plugins/maestro-skills/vendor/emilkowalski/skills/prototype/PICKER.md +197 -0
- package/plugins/maestro-skills/vendor/emilkowalski/skills/prototype/SKILL.md +90 -0
- package/plugins/maestro-skills/vendor/emilkowalski/skills/review-animations/SKILL.md +112 -0
- package/plugins/maestro-skills/vendor/emilkowalski/skills/review-animations/STANDARDS.md +187 -0
- package/plugins/maestro-skills/vendor/impeccable/LICENSE +191 -0
- package/plugins/maestro-skills/vendor/impeccable/NOTICE.md +11 -0
- package/plugins/maestro-skills/vendor/impeccable/SKILL.md +86 -0
- package/plugins/maestro-skills/vendor/impeccable/UPSTREAM.json +201 -0
- package/plugins/maestro-skills/vendor/impeccable/agents/impeccable-asset-producer.md +42 -0
- package/plugins/maestro-skills/vendor/impeccable/agents/impeccable-documenter.md +29 -0
- package/plugins/maestro-skills/vendor/impeccable/agents/impeccable-finish-reviewer.md +43 -0
- package/plugins/maestro-skills/vendor/impeccable/agents/impeccable-manual-edit-applier.md +97 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/adapt.md +312 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/adapt.native.md +58 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/android.md +46 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/animate.md +89 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/audit.md +136 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/audit.native.md +139 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/bolder.md +33 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/clarify.md +94 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/colorize.md +86 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/craft-floor.md +44 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/craft.md +5 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/critique.md +806 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/degraded/asset-producer.md +37 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/degraded/documenter.md +24 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/degraded/finish-reviewer.md +38 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/degraded/manual-edit-applier.md +92 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/delight.md +70 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/distill.md +111 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/doctor.md +54 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/document.md +416 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/extract.md +69 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/harden.md +336 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/hooks.md +111 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/init.md +131 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/ios.md +51 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/layout.md +84 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/live-setup.md +104 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/live.md +325 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/new-work.md +147 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/onboard.md +234 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/operate.md +61 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/optimize.md +258 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/overdrive.md +127 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/polish.md +105 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/quieter.md +99 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/routing.md +24 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/shape.md +59 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/typeset.md +80 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/visualize.md +46 -0
- package/plugins/maestro-skills/vendor/taste-skill/LICENSE +21 -0
- package/plugins/maestro-skills/vendor/taste-skill/UPSTREAM.json +37 -0
- package/plugins/maestro-skills/vendor/taste-skill/skills/minimalist-skill/SKILL.md +85 -0
- package/plugins/maestro-skills/vendor/taste-skill/skills/redesign-skill/SKILL.md +178 -0
- package/plugins/maestro-skills/vendor/taste-skill/skills/soft-skill/SKILL.md +98 -0
- package/plugins/maestro-skills/vendor/taste-skill/skills/taste-skill/SKILL.md +1206 -0
- package/plugins/maestro-skills/vendor/unlazy/LICENSE +21 -0
- package/plugins/maestro-skills/vendor/unlazy/SECURITY.md +72 -0
- package/plugins/maestro-skills/vendor/unlazy/SKILL.md +104 -0
- package/plugins/maestro-skills/vendor/unlazy/UPSTREAM.json +94 -0
- package/plugins/maestro-skills/vendor/unlazy/references/dispatch.md +82 -0
- package/plugins/maestro-skills/vendor/unlazy/references/gates.md +149 -0
- package/plugins/maestro-skills/vendor/unlazy/references/method.md +49 -0
- package/plugins/maestro-skills/vendor/unlazy/references/orchestration.md +107 -0
- package/plugins/maestro-skills/vendor/unlazy/references/parallel.md +133 -0
- package/plugins/maestro-skills/vendor/unlazy/references/token-economy.md +48 -0
- package/plugins/maestro-skills/vendor/unlazy/scripts/dispatch-check.mjs +139 -0
- package/plugins/maestro-skills/vendor/unlazy/scripts/gate-check.mjs +960 -0
- package/plugins/maestro-skills/vendor/unlazy/scripts/gate-lint.mjs +245 -0
- package/plugins/maestro-skills/vendor/unlazy/scripts/lib/check-supervisor.mjs +46 -0
- package/plugins/maestro-skills/vendor/unlazy/scripts/lib/dispatch.mjs +293 -0
- package/plugins/maestro-skills/vendor/unlazy/scripts/lib/gates.mjs +953 -0
- package/plugins/maestro-skills/vendor/unlazy/scripts/lib/process-tree.mjs +161 -0
- package/plugins/maestro-skills/vendor/unlazy/scripts/lib/regex-worker.mjs +9 -0
- package/plugins/maestro-skills/vendor/unlazy/templates/PLAN.md +116 -0
- package/plugins/maestro-skills/vendor/unlazy/templates/gates-leaf.md +51 -0
- package/plugins/maestro-skills/vendor/unlazy/templates/gates-node.md +51 -0
- package/scaffold/CLAUDE.md +24 -0
- package/scripts/ci/check-durable-write-seam.mjs +147 -0
- package/scripts/ci/check-durable-write-seam.test.mjs +90 -0
- package/scripts/ci/check-skill-packs.mjs +388 -0
- package/scripts/ci/check-skill-packs.test.mjs +495 -0
- package/scripts/ci/check.mjs +6 -0
- package/scripts/collective/hook-runner.mjs +39 -4
- package/scripts/collective/hook-runner.test.mjs +85 -2
- package/scripts/daemon/agent-daemon-board-mine.test.mjs +96 -0
- package/scripts/daemon/agent-daemon-design.test.mjs +238 -0
- package/scripts/daemon/agent-daemon-frontdoor.test.mjs +60 -0
- package/scripts/daemon/agent-daemon.mjs +249 -10
- package/scripts/daemon/agent-daemon.test.mjs +73 -0
- package/scripts/daemon/assurance-e2e.test.mjs +141 -6
- package/scripts/daemon/assurance.mjs +461 -37
- package/scripts/daemon/assurance.test.mjs +408 -43
- package/scripts/daemon/cadence-consumer-frontdoor.test.mjs +393 -0
- package/scripts/daemon/cadence-consumer.mjs +289 -89
- package/scripts/daemon/cadence-handlers.mjs +53 -0
- package/scripts/daemon/classifier.mjs +1 -1
- package/scripts/daemon/dispatcher-resume.test.mjs +166 -0
- package/scripts/daemon/dispatcher.mjs +127 -19
- package/scripts/daemon/health.mjs +12 -1
- package/scripts/daemon/inbox-deferral-session.test.mjs +49 -0
- package/scripts/daemon/inbox-deferral.mjs +6 -0
- package/scripts/daemon/lib/self-echo.mjs +201 -0
- package/scripts/daemon/lib/self-echo.test.mjs +153 -0
- package/scripts/daemon/maestro-daemon.mjs +3 -0
- package/scripts/daemon/prompt-builder.mjs +19 -3
- package/scripts/daemon/responder.mjs +51 -40
- package/scripts/daemon/sdk-version.mjs +51 -0
- package/scripts/daemon/sdk-version.test.mjs +31 -0
- package/scripts/hooks/pre-send-audit.sh +97 -4
- package/scripts/hooks/pre-send-audit.test.mjs +140 -1
- package/scripts/local-triggers/autoupdate.sh +243 -19
- package/scripts/local-triggers/autoupdate.test.mjs +518 -0
- package/scripts/local-triggers/generate-plists.sh +24 -1
- package/scripts/local-triggers/generate-plists.test.mjs +49 -11
- package/scripts/org/send-orgmail.first-contact.test.mjs +102 -0
- package/scripts/org/send-orgmail.mjs +27 -3
- package/scripts/poller/inbox-privilege-injection.test.mjs +167 -0
- package/scripts/poller/slack-poller.mjs +13 -1
- package/scripts/poller/utils.mjs +46 -1
- package/scripts/poller-launchd/install.sh +19 -11
- package/scripts/poller-launchd/install.test.mjs +243 -0
- package/scripts/poller-launchd/launchd-poller-wrapper.sh +92 -0
- package/scripts/poller-launchd/migrate.sh +66 -0
- package/scripts/poller-launchd/poller.plist.template +4 -2
- package/scripts/session/feed.mjs +237 -0
- package/scripts/session/feed.test.mjs +196 -0
- package/scripts/session/supervisor-sh.test.mjs +218 -0
- package/scripts/session/supervisor.mjs +328 -0
- package/scripts/session/supervisor.sh +141 -0
- package/scripts/session/supervisor.test.mjs +482 -0
- package/scripts/setup/configure-macos.sh +250 -55
- package/scripts/setup/configure-macos.test.mjs +306 -0
- package/scripts/setup/init-agent.sh +112 -7
- package/scripts/setup/init-agent.test.mjs +220 -1
- package/scripts/vendor/skill-packs.mjs +354 -0
- package/scripts/vendor/sync-skill-packs.mjs +242 -0
- package/scripts/vendor/sync-skill-packs.test.mjs +103 -0
- package/scripts/watchdog/memory-watchdog.sh +37 -1
- package/scripts/watchdog/memory-watchdog.test.mjs +64 -0
- package/scripts/setup/boot-claude-session.sh +0 -94
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* sync-skill-packs.mjs — re-vendor a pinned upstream design skill pack.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* node scripts/vendor/sync-skill-packs.mjs <pack>[ <pack> …] re-fetch and rewrite
|
|
7
|
+
* node scripts/vendor/sync-skill-packs.mjs --all
|
|
8
|
+
* node scripts/vendor/sync-skill-packs.mjs --list
|
|
9
|
+
* node scripts/vendor/sync-skill-packs.mjs <pack> --dry-run report, write nothing
|
|
10
|
+
* node scripts/vendor/sync-skill-packs.mjs --all --dry-run RE-VERIFY every
|
|
11
|
+
* pack against its pinned upstream commit. This is the only thing that
|
|
12
|
+
* establishes provenance: `check-skill-packs.mjs` runs offline and can
|
|
13
|
+
* only prove the vendored trees have not moved since review, never that
|
|
14
|
+
* they came from upstream. Run this when auditing, not on every commit.
|
|
15
|
+
*
|
|
16
|
+
* What it does, for each pack in `scripts/vendor/skill-packs.mjs`:
|
|
17
|
+
* 1. downloads `https://codeload.github.com/<repo>/tar.gz/<sha>` — the PINNED
|
|
18
|
+
* commit, never a branch — into a temp dir and extracts it with `tar`;
|
|
19
|
+
* 2. copies ONLY the paths the pack's allow-list admits into
|
|
20
|
+
* `plugins/maestro-skills/vendor/<pack>/`, deleting anything already there
|
|
21
|
+
* that the allow-list no longer admits;
|
|
22
|
+
* 3. rewrites `UPSTREAM.json` with `{repo, sha, date, license, files:[{path,
|
|
23
|
+
* sha256}], omitted, notes}`, sorted, so the diff of a re-sync is readable.
|
|
24
|
+
*
|
|
25
|
+
* This is a MAINTAINER tool. It is the only place in the SDK that fetches skill
|
|
26
|
+
* content, it runs on a developer's machine before a commit, and nothing on a
|
|
27
|
+
* seat ever calls it: seats get the reviewed bytes from the npm tarball. It
|
|
28
|
+
* never executes anything out of the downloaded tree.
|
|
29
|
+
*
|
|
30
|
+
* Node builtins only (plus the system `tar`, which macOS and Linux both ship).
|
|
31
|
+
*
|
|
32
|
+
* @module scripts/vendor/sync-skill-packs
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
"use strict";
|
|
36
|
+
|
|
37
|
+
import { createHash } from "node:crypto";
|
|
38
|
+
import { execFileSync } from "node:child_process";
|
|
39
|
+
import {
|
|
40
|
+
mkdtempSync, mkdirSync, rmSync, readdirSync, readFileSync, writeFileSync, statSync, existsSync, copyFileSync,
|
|
41
|
+
} from "node:fs";
|
|
42
|
+
import { tmpdir } from "node:os";
|
|
43
|
+
import path from "node:path";
|
|
44
|
+
import { fileURLToPath } from "node:url";
|
|
45
|
+
import { PACKS, findPack, posix, isAllowedDest, manifestDigest } from "./skill-packs.mjs";
|
|
46
|
+
|
|
47
|
+
const REPO_ROOT = path.resolve(fileURLToPath(new URL(".", import.meta.url)), "..", "..");
|
|
48
|
+
|
|
49
|
+
/** Where vendored packs live, relative to the repo root. */
|
|
50
|
+
export const VENDOR_REL = path.join("plugins", "maestro-skills", "vendor");
|
|
51
|
+
|
|
52
|
+
/** @param {string} p @returns {string} */
|
|
53
|
+
export function sha256File(p) {
|
|
54
|
+
return createHash("sha256").update(readFileSync(p)).digest("hex");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Recursively list files under `dir`, as paths relative to it (POSIX). */
|
|
58
|
+
export function listFiles(dir, prefix = "") {
|
|
59
|
+
let out = [];
|
|
60
|
+
let entries;
|
|
61
|
+
try {
|
|
62
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
63
|
+
} catch {
|
|
64
|
+
return out; // an absent directory is an empty one for our purposes
|
|
65
|
+
}
|
|
66
|
+
for (const e of entries.sort((a, b) => (a.name < b.name ? -1 : 1))) {
|
|
67
|
+
const rel = prefix ? `${prefix}/${e.name}` : e.name;
|
|
68
|
+
if (e.isDirectory()) out = out.concat(listFiles(path.join(dir, e.name), rel));
|
|
69
|
+
else if (e.isFile()) out.push(rel);
|
|
70
|
+
}
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Expand a pack's rules against a real upstream tree into {from, to} pairs.
|
|
76
|
+
* Pure over the injected lister so the planner is testable without a download.
|
|
77
|
+
*
|
|
78
|
+
* @param {import("./skill-packs.mjs").SkillPack} pack
|
|
79
|
+
* @param {string} srcRoot
|
|
80
|
+
* @param {{list?:(dir:string)=>string[], exists?:(p:string)=>boolean}} [deps]
|
|
81
|
+
* @returns {{ pairs: Array<{from:string,to:string}>, missing: string[] }}
|
|
82
|
+
*/
|
|
83
|
+
export function planPack(pack, srcRoot, deps = {}) {
|
|
84
|
+
const list = deps.list || ((d) => listFiles(d));
|
|
85
|
+
const exists = deps.exists || existsSync;
|
|
86
|
+
const pairs = [];
|
|
87
|
+
const missing = [];
|
|
88
|
+
for (const rule of pack.rules) {
|
|
89
|
+
if (rule.type === "file") {
|
|
90
|
+
const abs = path.join(srcRoot, rule.from);
|
|
91
|
+
if (!exists(abs)) { missing.push(rule.from); continue; }
|
|
92
|
+
pairs.push({ from: rule.from, to: posix(rule.to) });
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
const absDir = path.join(srcRoot, rule.from);
|
|
96
|
+
if (!exists(absDir)) { missing.push(`${rule.from}/`); continue; }
|
|
97
|
+
let taken = 0;
|
|
98
|
+
for (const rel of list(absDir)) {
|
|
99
|
+
if (!rule.recursive && rel.includes("/")) continue;
|
|
100
|
+
const name = rel.slice(rel.lastIndexOf("/") + 1);
|
|
101
|
+
if (rule.match && !rule.match.test(name)) continue;
|
|
102
|
+
pairs.push({ from: `${rule.from}/${rel}`, to: posix(rule.to ? `${rule.to}/${rel}` : rel) });
|
|
103
|
+
taken += 1;
|
|
104
|
+
}
|
|
105
|
+
if (taken === 0) missing.push(`${rule.from}/ (matched nothing)`);
|
|
106
|
+
}
|
|
107
|
+
// A rule that produced a path the guard would reject is a registry bug, not a
|
|
108
|
+
// vendoring accident — say so loudly rather than writing an unverifiable tree.
|
|
109
|
+
for (const p of pairs) {
|
|
110
|
+
if (!isAllowedDest(pack, p.to)) throw new Error(`rule produced a path its own allow-list rejects: ${p.to}`);
|
|
111
|
+
}
|
|
112
|
+
pairs.sort((a, b) => (a.to < b.to ? -1 : a.to > b.to ? 1 : 0));
|
|
113
|
+
return { pairs, missing };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Download the pinned tarball and extract it; returns the extracted repo root. */
|
|
117
|
+
function fetchTree(pack, workDir) {
|
|
118
|
+
const url = `https://codeload.github.com/${pack.repo}/tar.gz/${pack.sha}`;
|
|
119
|
+
const tgz = path.join(workDir, "pack.tar.gz");
|
|
120
|
+
execFileSync("curl", ["-sSLf", "--max-time", "300", "-o", tgz, url], { stdio: ["ignore", "ignore", "pipe"] });
|
|
121
|
+
const outDir = path.join(workDir, "tree");
|
|
122
|
+
mkdirSync(outDir, { recursive: true });
|
|
123
|
+
execFileSync("tar", ["-xzf", tgz, "-C", outDir], { stdio: ["ignore", "ignore", "pipe"] });
|
|
124
|
+
const roots = readdirSync(outDir, { withFileTypes: true }).filter((e) => e.isDirectory());
|
|
125
|
+
if (roots.length !== 1) throw new Error(`unexpected tarball layout for ${pack.repo}: ${roots.length} top-level dirs`);
|
|
126
|
+
return path.join(outDir, roots[0].name);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Build the UPSTREAM.json body for a vendored pack. Pure. */
|
|
130
|
+
export function buildManifest(pack, files) {
|
|
131
|
+
return {
|
|
132
|
+
repo: pack.repo,
|
|
133
|
+
sha: pack.sha,
|
|
134
|
+
date: pack.date,
|
|
135
|
+
license: pack.license,
|
|
136
|
+
files: files.slice().sort((a, b) => (a.path < b.path ? -1 : 1)),
|
|
137
|
+
omitted: pack.omitted.slice(),
|
|
138
|
+
notes: pack.notes,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Re-vendor one pack. Returns a summary; throws only on a defect (no network,
|
|
144
|
+
* a moved upstream path, a rule that contradicts the allow-list).
|
|
145
|
+
* @param {string} id @param {{repoRoot?:string, dryRun?:boolean, log?:Function}} [o]
|
|
146
|
+
*/
|
|
147
|
+
export function syncPack(id, o = {}) {
|
|
148
|
+
const pack = findPack(id);
|
|
149
|
+
if (!pack) throw new Error(`unknown pack "${id}" (known: ${PACKS.map((p) => p.id).join(", ")})`);
|
|
150
|
+
const repoRoot = o.repoRoot || REPO_ROOT;
|
|
151
|
+
const log = o.log || (() => {});
|
|
152
|
+
const destRoot = path.join(repoRoot, VENDOR_REL, pack.id);
|
|
153
|
+
const work = mkdtempSync(path.join(tmpdir(), `maestro-vendor-${pack.id}-`));
|
|
154
|
+
try {
|
|
155
|
+
const srcRoot = fetchTree(pack, work);
|
|
156
|
+
const { pairs, missing } = planPack(pack, srcRoot);
|
|
157
|
+
if (missing.length) throw new Error(`${pack.id}: upstream paths absent at ${pack.sha}: ${missing.join(", ")}`);
|
|
158
|
+
const wanted = new Set(pairs.map((p) => p.to));
|
|
159
|
+
const existingFiles = listFiles(destRoot);
|
|
160
|
+
const removed = existingFiles.filter((f) => f !== "UPSTREAM.json" && !wanted.has(f));
|
|
161
|
+
if (!o.dryRun) {
|
|
162
|
+
for (const rel of removed) rmSync(path.join(destRoot, rel), { force: true });
|
|
163
|
+
for (const p of pairs) {
|
|
164
|
+
const dest = path.join(destRoot, p.to);
|
|
165
|
+
mkdirSync(path.dirname(dest), { recursive: true });
|
|
166
|
+
copyFileSync(path.join(srcRoot, p.from), dest);
|
|
167
|
+
// Vendored content is data. Strip any executable bit the upstream tree
|
|
168
|
+
// carried unless the pack explicitly allows one there.
|
|
169
|
+
const mode = statSync(dest).mode & 0o777;
|
|
170
|
+
const want = 0o644;
|
|
171
|
+
if (mode !== want) execFileSync("chmod", ["644", dest]);
|
|
172
|
+
}
|
|
173
|
+
pruneEmptyDirs(destRoot);
|
|
174
|
+
}
|
|
175
|
+
const files = pairs.map((p) => ({
|
|
176
|
+
path: p.to,
|
|
177
|
+
sha256: o.dryRun ? sha256File(path.join(srcRoot, p.from)) : sha256File(path.join(destRoot, p.to)),
|
|
178
|
+
}));
|
|
179
|
+
const manifest = buildManifest(pack, files);
|
|
180
|
+
const manifestPath = path.join(destRoot, "UPSTREAM.json");
|
|
181
|
+
if (!o.dryRun) {
|
|
182
|
+
mkdirSync(destRoot, { recursive: true });
|
|
183
|
+
writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
184
|
+
}
|
|
185
|
+
// The content pin. `UPSTREAM.json` is written from the bytes it records, so
|
|
186
|
+
// it can only ever agree with itself; the digest below lives in
|
|
187
|
+
// skill-packs.mjs, a reviewed source file no vendored tree can reach, and is
|
|
188
|
+
// what makes a change to the vendored file list show up in a diff as one
|
|
189
|
+
// line rather than as a data blob nobody reads. A re-sync that shifts it is
|
|
190
|
+
// a real upstream change and wants a real look — hence the loud line.
|
|
191
|
+
const contentSha256 = manifestDigest(files);
|
|
192
|
+
const pinned = pack.contentSha256 || null;
|
|
193
|
+
if (pinned && pinned !== contentSha256) {
|
|
194
|
+
log(`${pack.id}: CONTENT CHANGED — review the diff, then set contentSha256: "${contentSha256}" in scripts/vendor/skill-packs.mjs (was ${pinned})`);
|
|
195
|
+
} else if (!pinned) {
|
|
196
|
+
log(`${pack.id}: set contentSha256: "${contentSha256}" in scripts/vendor/skill-packs.mjs`);
|
|
197
|
+
}
|
|
198
|
+
log(`${pack.id}: ${files.length} file(s) from ${pack.repo}@${pack.sha.slice(0, 12)}${removed.length ? `, ${removed.length} removed` : ""}${o.dryRun ? " (dry-run)" : ""}`);
|
|
199
|
+
return { pack: pack.id, files, removed, contentSha256, pinned, dryRun: !!o.dryRun };
|
|
200
|
+
} finally {
|
|
201
|
+
rmSync(work, { recursive: true, force: true });
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** Remove directories left empty after a prune. */
|
|
206
|
+
function pruneEmptyDirs(root) {
|
|
207
|
+
let entries;
|
|
208
|
+
try { entries = readdirSync(root, { withFileTypes: true }); } catch { return; }
|
|
209
|
+
for (const e of entries) {
|
|
210
|
+
if (!e.isDirectory()) continue;
|
|
211
|
+
const child = path.join(root, e.name);
|
|
212
|
+
pruneEmptyDirs(child);
|
|
213
|
+
try { if (readdirSync(child).length === 0) rmSync(child, { recursive: true, force: true }); } catch { /* a racing reader is not our problem */ }
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async function main(argv) {
|
|
218
|
+
const args = argv.slice(2);
|
|
219
|
+
if (args.includes("--list") || args.length === 0) {
|
|
220
|
+
for (const p of PACKS) console.log(`${p.id.padEnd(14)} ${p.repo}@${p.sha.slice(0, 12)} ${p.license}`);
|
|
221
|
+
if (args.length === 0) console.log("\nusage: node scripts/vendor/sync-skill-packs.mjs <pack>|--all [--dry-run]");
|
|
222
|
+
return 0;
|
|
223
|
+
}
|
|
224
|
+
const dryRun = args.includes("--dry-run");
|
|
225
|
+
const ids = args.includes("--all") ? PACKS.map((p) => p.id) : args.filter((a) => !a.startsWith("--"));
|
|
226
|
+
let code = 0;
|
|
227
|
+
for (const id of ids) {
|
|
228
|
+
try {
|
|
229
|
+
syncPack(id, { dryRun, log: (m) => console.log(`sync-skill-packs: ${m}`) });
|
|
230
|
+
} catch (err) {
|
|
231
|
+
console.error(`sync-skill-packs: ${id} FAILED — ${err && err.message ? err.message : err}`);
|
|
232
|
+
code = 1;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return code;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
239
|
+
main(process.argv).then((c) => process.exit(c)).catch((e) => { console.error("sync-skill-packs: ERROR", e && e.message ? e.message : e); process.exit(2); });
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export default { syncPack, planPack, buildManifest, listFiles, sha256File, VENDOR_REL };
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the vendoring maintainer tool — the half that can run with no
|
|
3
|
+
* network. `syncPack` itself downloads a pinned tarball and is not exercised
|
|
4
|
+
* here; what IS exercised is the planner and the content pin, because those are
|
|
5
|
+
* the two places a re-sync can quietly change what the fleet reads.
|
|
6
|
+
*
|
|
7
|
+
* @module scripts/vendor/sync-skill-packs.test
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { describe, it } from "node:test";
|
|
11
|
+
import assert from "node:assert/strict";
|
|
12
|
+
import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, rmSync } from "node:fs";
|
|
13
|
+
import { tmpdir } from "node:os";
|
|
14
|
+
import path from "node:path";
|
|
15
|
+
import { fileURLToPath } from "node:url";
|
|
16
|
+
import { planPack, buildManifest, sha256File } from "./sync-skill-packs.mjs";
|
|
17
|
+
import { PACKS, findPack, manifestDigest } from "./skill-packs.mjs";
|
|
18
|
+
|
|
19
|
+
const REPO_ROOT = path.resolve(fileURLToPath(new URL(".", import.meta.url)), "..", "..");
|
|
20
|
+
const VENDOR = path.join(REPO_ROOT, "plugins", "maestro-skills", "vendor");
|
|
21
|
+
|
|
22
|
+
describe("sync-skill-packs: the content pin", () => {
|
|
23
|
+
it("manifestDigest is order-independent and moves for any file added, removed or changed", () => {
|
|
24
|
+
const a = [{ path: "SKILL.md", sha256: "aa" }, { path: "reference/x.md", sha256: "bb" }];
|
|
25
|
+
assert.equal(manifestDigest(a), manifestDigest([...a].reverse()), "sorted, so a re-sort is not a change");
|
|
26
|
+
assert.notEqual(manifestDigest(a), manifestDigest([...a, { path: "reference/y.md", sha256: "cc" }]));
|
|
27
|
+
assert.notEqual(manifestDigest(a), manifestDigest([a[0]]));
|
|
28
|
+
assert.notEqual(manifestDigest(a), manifestDigest([a[0], { path: "reference/x.md", sha256: "b0" }]));
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("what the syncer would write for each committed pack still matches its pinned digest", () => {
|
|
32
|
+
// buildManifest is what a re-sync produces; if it and the committed
|
|
33
|
+
// UPSTREAM.json disagree, someone hand-edited a vendored tree.
|
|
34
|
+
for (const pack of PACKS) {
|
|
35
|
+
const doc = JSON.parse(readFileSync(path.join(VENDOR, pack.id, "UPSTREAM.json"), "utf8"));
|
|
36
|
+
const rebuilt = buildManifest(pack, doc.files);
|
|
37
|
+
assert.deepEqual(rebuilt.files, doc.files, pack.id);
|
|
38
|
+
assert.deepEqual(rebuilt.omitted, doc.omitted, `${pack.id} omitted[] matches the registry`);
|
|
39
|
+
assert.equal(rebuilt.notes, doc.notes, `${pack.id} notes match the registry`);
|
|
40
|
+
assert.equal(manifestDigest(rebuilt.files), pack.contentSha256, pack.id);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("every recorded sha256 is the sha256 of the file on disk", () => {
|
|
45
|
+
for (const pack of PACKS) {
|
|
46
|
+
const doc = JSON.parse(readFileSync(path.join(VENDOR, pack.id, "UPSTREAM.json"), "utf8"));
|
|
47
|
+
for (const f of doc.files) {
|
|
48
|
+
assert.equal(sha256File(path.join(VENDOR, pack.id, f.path)), f.sha256, `${pack.id}/${f.path}`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe("sync-skill-packs: the planner", () => {
|
|
55
|
+
/** A fake upstream tree, so the planner runs with no network. */
|
|
56
|
+
function upstream(files) {
|
|
57
|
+
const root = mkdtempSync(path.join(tmpdir(), "maestro-upstream-"));
|
|
58
|
+
for (const [rel, body] of Object.entries(files)) {
|
|
59
|
+
const abs = path.join(root, rel);
|
|
60
|
+
mkdirSync(path.dirname(abs), { recursive: true });
|
|
61
|
+
writeFileSync(abs, body);
|
|
62
|
+
}
|
|
63
|
+
return { root, cleanup: () => rmSync(root, { recursive: true, force: true }) };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
it("unlazy's rules take the gate checker and refuse the hook machinery", () => {
|
|
67
|
+
// The pack is vendored for its gate checker. `install-hooks.mjs` writes a
|
|
68
|
+
// Stop hook into a settings file — `--global` into the one every session on
|
|
69
|
+
// the machine reads — so it is excluded at the rule, not discouraged in
|
|
70
|
+
// prose: a file that reaches the seat can be run whatever its mode says.
|
|
71
|
+
const t = upstream({
|
|
72
|
+
"SKILL.md": "---\nname: unlazy\n---\n",
|
|
73
|
+
"SECURITY.md": "#\n",
|
|
74
|
+
"LICENSE": "MIT\n",
|
|
75
|
+
"references/gates.md": "#\n",
|
|
76
|
+
"templates/PLAN.md": "#\n",
|
|
77
|
+
"scripts/gate-check.mjs": "//\n",
|
|
78
|
+
"scripts/lib/gates.mjs": "//\n",
|
|
79
|
+
"scripts/install-hooks.mjs": "// writes a Stop hook\n",
|
|
80
|
+
"scripts/stop-hook.mjs": "// the hook body\n",
|
|
81
|
+
});
|
|
82
|
+
try {
|
|
83
|
+
const { pairs, missing } = planPack(findPack("unlazy"), t.root);
|
|
84
|
+
const took = pairs.map((p) => p.to);
|
|
85
|
+
assert.deepEqual(missing, []);
|
|
86
|
+
assert.ok(took.includes("scripts/gate-check.mjs"));
|
|
87
|
+
assert.ok(took.includes("scripts/lib/gates.mjs"));
|
|
88
|
+
assert.equal(took.includes("scripts/install-hooks.mjs"), false);
|
|
89
|
+
assert.equal(took.includes("scripts/stop-hook.mjs"), false);
|
|
90
|
+
} finally { t.cleanup(); }
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("a rule that produced a path its own allow-list rejects is a defect, not a quiet write", () => {
|
|
94
|
+
const pack = {
|
|
95
|
+
...findPack("unlazy"),
|
|
96
|
+
rules: [{ type: "dir", from: "scripts", to: "../escape", match: /\.mjs$/, recursive: true }],
|
|
97
|
+
};
|
|
98
|
+
const t = upstream({ "scripts/x.mjs": "//\n" });
|
|
99
|
+
try {
|
|
100
|
+
assert.throws(() => planPack(pack, t.root), /allow-list rejects/);
|
|
101
|
+
} finally { t.cleanup(); }
|
|
102
|
+
});
|
|
103
|
+
});
|
|
@@ -16,7 +16,10 @@
|
|
|
16
16
|
# auto-expiring ceiling = liveCount-2 that the resource governor
|
|
17
17
|
# reads to admit fewer sessions. NO kills.
|
|
18
18
|
# HARD (sys 80–90% / claude ≥75%) → SIGTERM the youngest/biggest
|
|
19
|
-
# subagents, sparing the oldest "main" session (MIN_AGE_SECONDS)
|
|
19
|
+
# subagents, sparing the oldest "main" session (MIN_AGE_SECONDS)
|
|
20
|
+
# and — audit F16 — NEVER the seat's interactive main session
|
|
21
|
+
# (`--name <first>-main`) or its feed (scripts/session/feed.mjs),
|
|
22
|
+
# whatever their age or size. See is_protected_cmdline.
|
|
20
23
|
# CRITICAL (sys ≥90%, sustained 2 consecutive runs) → clean daemon RESTART
|
|
21
24
|
# via `launchctl kickstart -k` (the daemon re-reads state/queues +
|
|
22
25
|
# cooldowns + sweeps stale claims). NOT a halt. Only after 3 failed
|
|
@@ -156,6 +159,34 @@ count_claude_processes() {
|
|
|
156
159
|
|
|
157
160
|
# --- Identification ----------------------------------------------------------
|
|
158
161
|
|
|
162
|
+
# Audit F16. The oldest-process heuristic below is not enough: after a relaunch
|
|
163
|
+
# the seat's interactive main session is the YOUNGEST claude process, and the
|
|
164
|
+
# HARD tier killed it. The main session is identified by its command line, not
|
|
165
|
+
# its age: `claude --name <first>-main …` (the supervisor launches it so) and
|
|
166
|
+
# the feed running inside it (`node scripts/session/feed.mjs`). Pure: takes the
|
|
167
|
+
# command line, exits 0 when the process must never be signalled.
|
|
168
|
+
is_protected_cmdline() {
|
|
169
|
+
local cmdline="${1:-}"
|
|
170
|
+
[ -z "$cmdline" ] && return 1
|
|
171
|
+
# `--name <token>-main` as a WHOLE token (not -mainline, not -main-2).
|
|
172
|
+
if printf '%s\n' "$cmdline" | grep -Eq -- '(^|[[:space:]])--name[[:space:]]+[A-Za-z0-9_.-]+-main([[:space:]]|$)'; then
|
|
173
|
+
return 0
|
|
174
|
+
fi
|
|
175
|
+
case "$cmdline" in
|
|
176
|
+
*scripts/session/feed.mjs*) return 0 ;;
|
|
177
|
+
esac
|
|
178
|
+
return 1
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
# Full command line of a pid ("" when the process is gone). `-ww` = unlimited
|
|
182
|
+
# width: without it macOS ps clips `command=` to the terminal width when
|
|
183
|
+
# stdout is a tty (an operator's --check / --dry-run), and a long claude argv
|
|
184
|
+
# can lose its `--name <first>-main` token — which would un-protect the main
|
|
185
|
+
# session exactly when a human is watching.
|
|
186
|
+
cmdline_of() {
|
|
187
|
+
/bin/ps -ww -o command= -p "$1" 2>/dev/null || true
|
|
188
|
+
}
|
|
189
|
+
|
|
159
190
|
identify_main_session() {
|
|
160
191
|
# The oldest claude process is treated as the "main" session and spared by
|
|
161
192
|
# HARD-tier kills. (longest-running heuristic)
|
|
@@ -201,6 +232,11 @@ kill_youngest_biggest_subagents() {
|
|
|
201
232
|
log_event "info" "skip_young" "PID $pid age ${age}s < ${MIN_AGE_SECONDS}s minimum"
|
|
202
233
|
continue
|
|
203
234
|
fi
|
|
235
|
+
# Audit F16: the interactive main session and its feed are never candidates.
|
|
236
|
+
if is_protected_cmdline "$(cmdline_of "$pid")"; then
|
|
237
|
+
log_event "info" "skip_protected" "PID $pid is the main session / feed — never signalled"
|
|
238
|
+
continue
|
|
239
|
+
fi
|
|
204
240
|
if [ "$killed" -ge "$target_count" ]; then
|
|
205
241
|
break
|
|
206
242
|
fi
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* memory-watchdog.test.mjs — audit F16: the HARD tier could SIGTERM the
|
|
3
|
+
* seat's long-lived interactive main session (it only spared the OLDEST
|
|
4
|
+
* claude process, and a freshly relaunched main session is not the oldest).
|
|
5
|
+
*
|
|
6
|
+
* The rule under test is a pure shell function, `is_protected_cmdline`,
|
|
7
|
+
* extracted from the script and run under bash: a command line carrying
|
|
8
|
+
* `--name <anything>-main` or `scripts/session/feed.mjs` is never a kill
|
|
9
|
+
* candidate. The kill loop is also checked textually for the call site, so
|
|
10
|
+
* the function cannot be defined and then not used.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { test } from "node:test";
|
|
14
|
+
import assert from "node:assert/strict";
|
|
15
|
+
import { readFileSync } from "node:fs";
|
|
16
|
+
import { spawnSync } from "node:child_process";
|
|
17
|
+
import { join, dirname } from "node:path";
|
|
18
|
+
import { fileURLToPath } from "node:url";
|
|
19
|
+
|
|
20
|
+
const SCRIPT = join(dirname(fileURLToPath(import.meta.url)), "memory-watchdog.sh");
|
|
21
|
+
|
|
22
|
+
/** Extract `is_protected_cmdline` from the script and evaluate it on one command line. Exit 0 = protected. */
|
|
23
|
+
function isProtected(cmdline) {
|
|
24
|
+
const src = readFileSync(SCRIPT, "utf8");
|
|
25
|
+
const m = src.match(/\nis_protected_cmdline\(\) \{[\s\S]*?\n\}\n/);
|
|
26
|
+
assert.ok(m, "memory-watchdog.sh defines is_protected_cmdline()");
|
|
27
|
+
const r = spawnSync("/bin/bash", ["-c", `${m[0]}\nis_protected_cmdline "$1"`, "_", cmdline], { encoding: "utf8" });
|
|
28
|
+
return r.status === 0;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
test("the main session (`--name <first>-main`) is protected, whatever the first name", () => {
|
|
32
|
+
assert.equal(isProtected("claude --name alex-main --resume 1234 --dangerously-skip-permissions"), true);
|
|
33
|
+
assert.equal(isProtected("/opt/homebrew/bin/claude --dangerously-skip-permissions --name marc-main"), true);
|
|
34
|
+
assert.equal(isProtected("node /Users/x/.npm-global/lib/node_modules/@anthropic-ai/claude-code/cli.js --name olivia-main"), true);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("the session feed (scripts/session/feed.mjs) is protected", () => {
|
|
38
|
+
assert.equal(isProtected("node /Users/alex/alex-ai/scripts/session/feed.mjs"), true);
|
|
39
|
+
assert.equal(isProtected("/usr/local/bin/node scripts/session/feed.mjs --agent-root /x"), true);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("sub-sessions, peers and print spawns are NOT protected", () => {
|
|
43
|
+
assert.equal(isProtected("claude --print --output-format json --dangerously-skip-permissions prompt"), false);
|
|
44
|
+
assert.equal(isProtected("claude --name alex-research --dangerously-skip-permissions"), false);
|
|
45
|
+
assert.equal(isProtected("claude --name alex-mainline"), false, "-main must be a whole token");
|
|
46
|
+
assert.equal(isProtected("claude --name alex-main-2"), false);
|
|
47
|
+
assert.equal(isProtected("node scripts/daemon/maestro-daemon.mjs"), false);
|
|
48
|
+
assert.equal(isProtected(""), false);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("the HARD-tier kill loop consults is_protected_cmdline before SIGTERM and logs the skip", () => {
|
|
52
|
+
const src = readFileSync(SCRIPT, "utf8");
|
|
53
|
+
const loop = src.slice(src.indexOf("kill_youngest_biggest_subagents() {"), src.indexOf('done <<< "$(get_claude_processes)"'));
|
|
54
|
+
assert.match(loop, /is_protected_cmdline/, "the kill loop calls the guard");
|
|
55
|
+
assert.match(loop, /skip_protected/, "a protected skip is logged");
|
|
56
|
+
assert.ok(loop.indexOf("is_protected_cmdline") < loop.indexOf('kill -TERM "$pid"'), "the guard runs before the signal");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("cmdline_of reads the FULL command line (ps -ww) so a long argv is never cut before --name <first>-main on a narrow tty", () => {
|
|
60
|
+
const src = readFileSync(SCRIPT, "utf8");
|
|
61
|
+
const m = src.match(/\ncmdline_of\(\) \{[\s\S]*?\n\}\n/);
|
|
62
|
+
assert.ok(m, "memory-watchdog.sh defines cmdline_of()");
|
|
63
|
+
assert.match(m[0], /\/bin\/ps -ww /, "ps must be given -ww (unlimited width) — macOS truncates `-o command=` to the terminal width otherwise");
|
|
64
|
+
});
|
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
#!/bin/bash
|
|
2
|
-
# =============================================================================
|
|
3
|
-
# boot-claude-session.sh — Launch interactive Claude Code session on boot
|
|
4
|
-
# =============================================================================
|
|
5
|
-
#
|
|
6
|
-
# Opens Terminal.app with a Claude Code interactive session in the agent's
|
|
7
|
-
# working directory. Called via launchd at login.
|
|
8
|
-
#
|
|
9
|
-
# This is separate from the maestro daemon (which runs headless as a Node.js
|
|
10
|
-
# process for polling/dispatching). This script provides a visible, interactive
|
|
11
|
-
# Claude Code session that the operator can observe and interact with.
|
|
12
|
-
#
|
|
13
|
-
# Usage:
|
|
14
|
-
# ./scripts/setup/boot-claude-session.sh <agent-dir>
|
|
15
|
-
#
|
|
16
|
-
# =============================================================================
|
|
17
|
-
|
|
18
|
-
set -euo pipefail
|
|
19
|
-
|
|
20
|
-
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
21
|
-
MAESTRO_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
|
22
|
-
|
|
23
|
-
# Agent directory required as first argument (e.g. ~/<firstname>-ai).
|
|
24
|
-
AGENT_DIR="${1:?Agent directory required as first argument (e.g. ~/<firstname>-ai)}"
|
|
25
|
-
if [ ! -d "$AGENT_DIR" ]; then
|
|
26
|
-
echo "ERROR: Agent directory does not exist: $AGENT_DIR" >&2
|
|
27
|
-
exit 1
|
|
28
|
-
fi
|
|
29
|
-
|
|
30
|
-
AGENT_NAME=$(basename "$AGENT_DIR")
|
|
31
|
-
# Resolve the agent's display name from config/agent.json (SOT), falling
|
|
32
|
-
# back to the directory basename. Used for the Terminal window banner.
|
|
33
|
-
AGENT_DISPLAY="$AGENT_NAME"
|
|
34
|
-
if [ -f "$AGENT_DIR/config/agent.json" ]; then
|
|
35
|
-
if command -v jq >/dev/null 2>&1; then
|
|
36
|
-
fn=$(jq -r '.fullName // empty' "$AGENT_DIR/config/agent.json")
|
|
37
|
-
[ -n "$fn" ] && AGENT_DISPLAY="$fn"
|
|
38
|
-
fi
|
|
39
|
-
fi
|
|
40
|
-
LOG_FILE="$MAESTRO_DIR/logs/watchdog/$(date +%Y-%m-%d)-boot-session.log"
|
|
41
|
-
mkdir -p "$(dirname "$LOG_FILE")"
|
|
42
|
-
|
|
43
|
-
log() {
|
|
44
|
-
echo "[$(date -u +"%Y-%m-%dT%H:%M:%SZ")] $1" | tee -a "$LOG_FILE"
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
# Wait for the system to settle after login (network, services, etc.)
|
|
48
|
-
log "Waiting 15s for system to settle..."
|
|
49
|
-
sleep 15
|
|
50
|
-
|
|
51
|
-
# Check for emergency stop
|
|
52
|
-
if [ -f "$AGENT_DIR/.emergency-stop" ]; then
|
|
53
|
-
log "Emergency stop active — not starting Claude session"
|
|
54
|
-
exit 0
|
|
55
|
-
fi
|
|
56
|
-
|
|
57
|
-
if [ -f "$MAESTRO_DIR/.emergency-stop" ]; then
|
|
58
|
-
log "Emergency stop active in maestro — not starting Claude session"
|
|
59
|
-
exit 0
|
|
60
|
-
fi
|
|
61
|
-
|
|
62
|
-
# Check if claude CLI is available
|
|
63
|
-
if ! command -v claude &>/dev/null; then
|
|
64
|
-
# Try common paths
|
|
65
|
-
CLAUDE_PATH=""
|
|
66
|
-
for p in "$HOME/.local/bin/claude" "/opt/homebrew/bin/claude" "/usr/local/bin/claude"; do
|
|
67
|
-
if [ -x "$p" ]; then
|
|
68
|
-
CLAUDE_PATH="$p"
|
|
69
|
-
break
|
|
70
|
-
fi
|
|
71
|
-
done
|
|
72
|
-
|
|
73
|
-
if [ -z "$CLAUDE_PATH" ]; then
|
|
74
|
-
log "ERROR: claude CLI not found"
|
|
75
|
-
exit 1
|
|
76
|
-
fi
|
|
77
|
-
else
|
|
78
|
-
CLAUDE_PATH=$(which claude)
|
|
79
|
-
fi
|
|
80
|
-
|
|
81
|
-
log "Starting Claude Code session in $AGENT_DIR (claude: $CLAUDE_PATH)"
|
|
82
|
-
|
|
83
|
-
# Open Terminal.app with Claude Code running in the agent directory
|
|
84
|
-
osascript <<APPLESCRIPT
|
|
85
|
-
tell application "Terminal"
|
|
86
|
-
activate
|
|
87
|
-
-- Open a new window with claude running in the agent directory
|
|
88
|
-
do script "cd '$AGENT_DIR' && clear && echo '╔══════════════════════════════════════════════════════════╗' && echo '║ $AGENT_DISPLAY — Boot Session ($(date +%Y-%m-%d))' && echo '╠══════════════════════════════════════════════════════════╣' && echo '║ Agent dir: $AGENT_DIR' && echo '║ Claude: $CLAUDE_PATH' && echo '╚══════════════════════════════════════════════════════════╝' && echo '' && '$CLAUDE_PATH'"
|
|
89
|
-
-- Set the window title
|
|
90
|
-
set custom title of front window to "$AGENT_NAME — Claude Code"
|
|
91
|
-
end tell
|
|
92
|
-
APPLESCRIPT
|
|
93
|
-
|
|
94
|
-
log "Terminal window opened with Claude Code session"
|