claude-code-session-manager 0.87.0 → 0.88.1
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/README.md +26 -53
- package/dist/assets/{AgentLibrary-DyLWzZDf.js → AgentLibrary-kQF8_wAw.js} +1 -1
- package/dist/assets/{DataModel--mISIJ6h.js → DataModel-CY_mMnsr.js} +1 -1
- package/dist/assets/{History-C2ahUXTg.js → History-Sji2brb0.js} +1 -1
- package/dist/assets/{Hooks-BiC6oyR2.js → Hooks-CtqpO-p5.js} +1 -1
- package/dist/assets/{HostBilko-BPleEOld.js → HostBilko-DN0ydOCr.js} +1 -1
- package/dist/assets/{Library-Dc8Qst1R.js → Library-CoiLZTKh.js} +1 -1
- package/dist/assets/{ListDetail-DIXh-OLX.js → ListDetail-CmqiQ_io.js} +1 -1
- package/dist/assets/{MarkdownEditor-C90bkLXK.js → MarkdownEditor-HPoNeKhT.js} +1 -1
- package/dist/assets/{McpServers-DqcbLOLZ.js → McpServers-Bj1X3_NT.js} +1 -1
- package/dist/assets/{Memory-CW62MXlh.js → Memory-1PKjsM3b.js} +1 -1
- package/dist/assets/{Panel-Bw1FhRuF.js → Panel-I1s6ghPo.js} +1 -1
- package/dist/assets/{Permissions-BcUC-5y8.js → Permissions-BhiiZjHQ.js} +1 -1
- package/dist/assets/{Plugins-BnKx9flD.js → Plugins-gv3_fkPX.js} +2 -2
- package/dist/assets/{ProvenanceBadge-Bw5vNVPT.js → ProvenanceBadge-BGcgBd8V.js} +1 -1
- package/dist/assets/{SaveBar-CWr0O_w-.js → SaveBar-BGr9eZ4g.js} +1 -1
- package/dist/assets/{Scheduler-DYdLuUqq.js → Scheduler-Bdo_awPq.js} +7 -7
- package/dist/assets/{ScopeSwitcher-CrBLbg8s.js → ScopeSwitcher-ukl0qWaA.js} +1 -1
- package/dist/assets/{Settings-DluB-vN1.js → Settings-DJkFj22G.js} +1 -1
- package/dist/assets/{SkillReferenceGraph-CHLSseay.js → SkillReferenceGraph-0t1Qblp6.js} +1 -1
- package/dist/assets/{Skills-gNdo_HNK.js → Skills-CeVdBGE-.js} +1 -1
- package/dist/assets/{SystemPrompt-Cru05-Ia.js → SystemPrompt-CfKvNHi3.js} +1 -1
- package/dist/assets/{TagLibrary-DNHY0xou.js → TagLibrary-DAzNaUPd.js} +1 -1
- package/dist/assets/{TiptapBody-I4lmbCgP.js → TiptapBody--S_rGXoA.js} +1 -1
- package/dist/assets/{Toggle-bWMHjmRh.js → Toggle-Dszm0xnp.js} +1 -1
- package/dist/assets/{index-DV3PorRY.css → index-BHTX4OTc.css} +1 -1
- package/dist/assets/{index-fc_JjdxL.js → index-Dp4Rc--q.js} +356 -356
- package/dist/assets/{settingsSchema-BfhtZnGD.js → settingsSchema-BLbssMYo.js} +1 -1
- package/dist/assets/{whisperWorker-Dbia1OpC.js → whisperWorker-C7ZGQwKg.js} +7 -7
- package/dist/index.html +2 -2
- package/dist/vad/ort-wasm-simd-threaded.asyncify.mjs +106 -110
- package/dist/vad/ort-wasm-simd-threaded.asyncify.wasm +0 -0
- package/dist/vad/ort-wasm-simd-threaded.jsep.mjs +98 -98
- package/dist/vad/ort-wasm-simd-threaded.jsep.wasm +0 -0
- package/dist/vad/ort-wasm-simd-threaded.jspi.mjs +99 -102
- package/dist/vad/ort-wasm-simd-threaded.jspi.wasm +0 -0
- package/dist/vad/ort-wasm-simd-threaded.mjs +46 -46
- package/dist/vad/ort-wasm-simd-threaded.wasm +0 -0
- package/package.json +10 -13
- package/scripts/README.md +59 -0
- package/scripts/audit-ops-hygiene.cjs +350 -0
- package/scripts/hooks/guard-destructive-git.cjs +10 -34
- package/scripts/hooks/guard-inline-implementation.cjs +14 -8
- package/scripts/hooks/guard-prd-writes.cjs +7 -36
- package/scripts/hooks/guard-self-schedule.cjs +175 -0
- package/scripts/ops-sweep.cjs +355 -0
- package/scripts/scheduler-mcp-server.cjs +28 -71
- package/src/main/__tests__/bilkoHost-integration.test.cjs +3 -3
- package/src/main/__tests__/chat-cancel-terminal.test.cjs +6 -9
- package/src/main/__tests__/chat-exit-close-race.test.cjs +3 -3
- package/src/main/__tests__/chat-mcp-consent-notice.test.cjs +4 -5
- package/src/main/__tests__/chat-queue.test.cjs +2 -2
- package/src/main/__tests__/chat-stop-signal.test.cjs +2 -2
- package/src/main/__tests__/dep-orphan-archive-health.test.cjs +77 -0
- package/src/main/__tests__/dod-batchkey.test.cjs +2 -2
- package/src/main/__tests__/dod-drain-hook.test.cjs +2 -2
- package/src/main/__tests__/dod-report.test.cjs +2 -2
- package/src/main/__tests__/dod-reverify.test.cjs +2 -2
- package/src/main/__tests__/epicMint.test.cjs +2 -2
- package/src/main/__tests__/exchanges.test.cjs +2 -2
- package/src/main/__tests__/extractJson.test.cjs +2 -2
- package/src/main/__tests__/files-reject-credentials.test.cjs +1 -1
- package/src/main/__tests__/fixtures/1218-fo-01-move-scripts-lib-into-src-main-lib.log +556 -0
- package/src/main/__tests__/health-build-freshness.test.cjs +39 -0
- package/src/main/__tests__/health-delegation-chain.test.cjs +15 -1
- package/src/main/__tests__/health-queue-dispatch.test.cjs +58 -7
- package/src/main/__tests__/health-tick-liveness.test.cjs +27 -3
- package/src/main/__tests__/health-usage-poller.test.cjs +70 -23
- package/src/main/__tests__/historyRollup.test.cjs +2 -2
- package/src/main/__tests__/kg-augment.test.cjs +2 -2
- package/src/main/__tests__/mcpStatus.test.cjs +2 -2
- package/src/main/__tests__/memoryAggregate.test.cjs +1 -1
- package/src/main/__tests__/memoryStale.test.cjs +1 -1
- package/src/main/__tests__/opsErrorLogTelemetryTap.test.cjs +25 -1
- package/src/main/__tests__/pollLoop-dispatch-on-failure.test.cjs +33 -3
- package/src/main/__tests__/prd-group-allocator.test.cjs +2 -2
- package/src/main/__tests__/prdAdminRouteParity.test.cjs +2 -0
- package/src/main/__tests__/prdAdminRoutes.test.cjs +14 -2
- package/src/main/__tests__/prdAuthoringSeed.test.cjs +39 -0
- package/src/main/__tests__/prdLocationsArchived.test.cjs +20 -13
- package/src/main/__tests__/proc-role-env.test.cjs +125 -0
- package/src/main/__tests__/procname-claude-spawn-sites.test.cjs +304 -0
- package/src/main/__tests__/procname-sm-processes.test.cjs +127 -0
- package/src/main/__tests__/projectHomeAdminRoutes.test.cjs +81 -401
- package/src/main/__tests__/projectPages.test.cjs +63 -149
- package/src/main/__tests__/queue-health-verdict.test.cjs +9 -9
- package/src/main/__tests__/queue-starvation-dispatch-driver.test.cjs +117 -20
- package/src/main/__tests__/queueHistory.test.cjs +2 -2
- package/src/main/__tests__/rateLimitPollerStreak.test.cjs +38 -3
- package/src/main/__tests__/runVerify-landed-commit-outranks.test.cjs +181 -0
- package/src/main/__tests__/runVerify.test.cjs +5 -5
- package/src/main/__tests__/scheduleJobStatusDrift.test.cjs +3 -3
- package/src/main/__tests__/scheduleJobTransitions.test.cjs +2 -2
- package/src/main/__tests__/scheduler-adopted-run-supervision.test.cjs +143 -0
- package/src/main/__tests__/scheduler-autofix-select.test.cjs +2 -2
- package/src/main/__tests__/scheduler-autopromote.test.cjs +2 -2
- package/src/main/__tests__/scheduler-bash-timeout-env.test.cjs +3 -5
- package/src/main/__tests__/scheduler-boot-orphans.test.cjs +78 -97
- package/src/main/__tests__/scheduler-default-eligible-heal.test.cjs +161 -0
- package/src/main/__tests__/scheduler-dispatch-loop.test.cjs +58 -0
- package/src/main/__tests__/scheduler-epic-digest.test.cjs +3 -5
- package/src/main/__tests__/scheduler-force-tick-outcome.test.cjs +2 -2
- package/src/main/__tests__/scheduler-gate-shadow.test.cjs +119 -0
- package/src/main/__tests__/scheduler-guard-verdict-autoresolve.test.cjs +46 -0
- package/src/main/__tests__/scheduler-heartbeat-payload.test.cjs +80 -0
- package/src/main/__tests__/scheduler-inplace-salvage.test.cjs +25 -18
- package/src/main/__tests__/scheduler-investigation-prompt.test.cjs +4 -4
- package/src/main/__tests__/scheduler-launch-failure.test.cjs +3 -5
- package/src/main/__tests__/scheduler-looks-done.test.cjs +26 -7
- package/src/main/__tests__/scheduler-manual-pause.test.cjs +118 -0
- package/src/main/__tests__/scheduler-meta-code-sha.test.cjs +26 -3
- package/src/main/__tests__/scheduler-prd-missing-skip.test.cjs +17 -2
- package/src/main/__tests__/scheduler-prd-persona-spawn.test.cjs +3 -5
- package/src/main/__tests__/scheduler-quiet-machine-lease.test.cjs +41 -6
- package/src/main/__tests__/scheduler-rate-limit-pause.test.cjs +62 -6
- package/src/main/__tests__/scheduler-rate-limit-spin-guard.test.cjs +3 -5
- package/src/main/__tests__/scheduler-reap-dead-running-jobs.test.cjs +23 -4
- package/src/main/__tests__/scheduler-reconcile-cwd-preserve.test.cjs +100 -0
- package/src/main/__tests__/scheduler-reconcile-invalid-repair.test.cjs +3 -3
- package/src/main/__tests__/scheduler-reconcile-quarantine.test.cjs +4 -4
- package/src/main/__tests__/scheduler-shard-quarantine.test.cjs +110 -0
- package/src/main/__tests__/scheduler-shared-tree-guard.test.cjs +81 -1
- package/src/main/__tests__/scheduler-stall-per-project.test.cjs +14 -0
- package/src/main/__tests__/scheduler-starve-escalation.test.cjs +4 -4
- package/src/main/__tests__/scheduler-stranded-autofix-park.test.cjs +245 -0
- package/src/main/__tests__/scheduler-supervisor-record.test.cjs +81 -0
- package/src/main/__tests__/scheduler-tick-cancel-token.test.cjs +2 -2
- package/src/main/__tests__/scheduler-tick-wedge.test.cjs +172 -0
- package/src/main/__tests__/scheduler-transient-failure.test.cjs +2 -2
- package/src/main/__tests__/scheduler-worktree-cap-defer.test.cjs +37 -11
- package/src/main/__tests__/scheduler-worktree-exec-cwd.test.cjs +3 -5
- package/src/main/__tests__/usageSingleFlight.test.cjs +157 -0
- package/src/main/__tests__/workTypeLibrary.test.cjs +1 -1
- package/src/main/bilkoHost.cjs +16 -7
- package/src/main/build-info.json +8 -0
- package/src/main/chatRunner.cjs +4 -2
- package/src/main/config.cjs +2 -3
- package/src/main/docEdit.cjs +4 -2
- package/src/main/health.cjs +276 -52
- package/src/main/heapSnapshot.cjs +2 -2
- package/src/main/historyAggregator.cjs +1 -1
- package/src/main/index.cjs +61 -10
- package/src/main/ipcSchemas.cjs +6 -17
- package/src/main/lib/__tests__/auditLog.test.cjs +38 -0
- package/src/main/lib/__tests__/buildIdentity.test.cjs +121 -0
- package/src/main/lib/__tests__/cwdClassify.test.cjs +111 -0
- package/src/main/lib/__tests__/definitionOfDoneSequence.test.cjs +95 -0
- package/src/main/lib/__tests__/delegationReadiness.test.cjs +1 -1
- package/src/main/lib/__tests__/dispatchLoop.test.cjs +63 -0
- package/src/main/lib/__tests__/gateFixtures.json +20 -0
- package/src/main/lib/__tests__/gitWorktree.test.cjs +8 -1
- package/src/main/lib/__tests__/instanceLock.test.cjs +93 -7
- package/src/main/lib/__tests__/jobSupervisorRecord.test.cjs +78 -0
- package/src/main/lib/__tests__/jobWorktreeBootLive.test.cjs +11 -0
- package/src/main/lib/__tests__/localAdminHttp.test.cjs +1 -1
- package/src/main/lib/__tests__/mcpToolCatalog.test.cjs +6 -1
- package/src/main/lib/__tests__/procIdentity.test.cjs +119 -0
- package/src/main/lib/__tests__/procName.test.cjs +92 -0
- package/src/main/lib/__tests__/queueStoreMachineStateRecovery.test.cjs +67 -0
- package/src/main/lib/__tests__/schedulerMcpServerHelp.test.cjs +6 -7
- package/src/main/lib/__tests__/schedulerMcpServerProjectHome.test.cjs +37 -204
- package/src/main/lib/__tests__/schedulerPaths.test.cjs +226 -0
- package/src/main/lib/__tests__/schedulerPathsWorktree.test.cjs +94 -0
- package/src/main/lib/__tests__/schedulerRuntimeState.test.cjs +56 -0
- package/src/main/lib/__tests__/sessionSlots.test.cjs +45 -0
- package/src/main/lib/__tests__/telemetryBacklog.test.cjs +1 -1
- package/src/main/lib/__tests__/upgradeDrain.test.cjs +130 -0
- package/src/main/lib/__tests__/usageCircuit.test.cjs +61 -0
- package/src/main/lib/__tests__/watchdog-helpers.test.cjs +63 -0
- package/src/main/lib/__tests__/watchdog-relaunch.test.cjs +73 -0
- package/src/main/lib/activeIndexRebuild.cjs +1 -3
- package/src/main/lib/activeSessions.cjs +20 -88
- package/src/main/lib/adoptedRunSupervisor.cjs +136 -0
- package/src/main/lib/agentModelResolve.cjs +33 -1
- package/src/main/lib/agentPersonaSchema.cjs +2 -2
- package/src/main/lib/auditLog.cjs +30 -5
- package/src/main/lib/buildIdentity.cjs +113 -0
- package/src/main/lib/classifyPromptTicket.cjs +3 -2
- package/src/main/lib/claudeBin.cjs +37 -2
- package/src/main/lib/cleanEnv.cjs +27 -1
- package/src/main/lib/credentials.cjs +4 -2
- package/src/main/lib/cwdClassify.cjs +185 -0
- package/src/main/lib/definitionOfDone.cjs +296 -52
- package/src/main/lib/dispatchLoop.cjs +40 -0
- package/src/main/lib/effectiveModelInfo.cjs +9 -10
- package/src/main/lib/ephemeralCwd.cjs +7 -28
- package/src/main/lib/gitWorktree.cjs +95 -8
- package/src/main/lib/guardShims.cjs +3 -3
- package/src/main/lib/historyRollup.cjs +6 -7
- package/src/main/lib/instanceLock.cjs +32 -5
- package/src/main/lib/jobSupervisorRecord.cjs +147 -0
- package/src/main/lib/jobWorktreeBootLive.cjs +9 -5
- package/src/main/lib/localAdminHttp.cjs +9 -27
- package/src/main/lib/mcpToolCatalog.cjs +29 -77
- package/src/main/lib/opsOwnership.cjs +27 -19
- package/src/main/lib/prdAuthoringSeed.cjs +36 -0
- package/src/main/lib/prdLocations.cjs +48 -1
- package/src/main/lib/procIdentity.cjs +126 -0
- package/src/main/lib/procName.cjs +98 -0
- package/src/main/lib/projectHomeAdminRoutes.cjs +56 -327
- package/src/main/lib/queueHistory.cjs +8 -7
- package/src/main/lib/queueStore.cjs +57 -37
- package/src/main/lib/quietMachineLease.cjs +19 -2
- package/src/main/lib/reservationExpiry.cjs +30 -0
- package/src/main/lib/runClaudeP.cjs +4 -2
- package/src/main/lib/runLogRetention.cjs +3 -2
- package/src/main/lib/scheduleJobSchema.cjs +2 -2
- package/src/main/lib/scheduleJobTransitions.cjs +29 -2
- package/src/main/lib/schedulerBatch.cjs +27 -2
- package/src/main/lib/schedulerPaths.cjs +175 -0
- package/src/main/lib/schedulerRuntimeState.cjs +59 -0
- package/src/main/lib/sessionSlots.cjs +47 -9
- package/src/main/lib/smProcNames.cjs +51 -0
- package/src/main/lib/upgradeDrain.cjs +188 -0
- package/src/main/lib/usageCircuit.cjs +53 -6
- package/src/main/lib/watchdogHelpers.cjs +70 -37
- package/src/main/lib/withTimeout.cjs +33 -0
- package/src/main/mcpStatus.cjs +4 -2
- package/src/main/pluginInstall.cjs +6 -2
- package/src/main/projectPages.cjs +41 -145
- package/src/main/pty.cjs +8 -1
- package/src/main/queueOps.cjs +10 -10
- package/src/main/runVerify.cjs +69 -3
- package/src/main/scheduler.cjs +1607 -383
- package/src/main/seedAgentPersonas.cjs +1 -1
- package/src/main/seedDevPlugin.cjs +1 -1
- package/src/main/seedSchedulerMcp.cjs +7 -6
- package/src/main/seedStatus.cjs +1 -1
- package/src/main/supervisor.cjs +5 -3
- package/src/main/usage.cjs +126 -62
- package/src/preload/api.d.ts +18 -64
- package/src/preload/index.cjs +2 -4
- package/src/seed/agents/project-home-builder.md +31 -48
- package/screenshots/.gitkeep +0 -0
- package/screenshots/README-screenshots.md +0 -13
- package/src/main/lib/projectPageSummarySchema.cjs +0 -181
- package/src/main/teams.cjs +0 -95
- package/src/main/templates/project-pages-catalog.json +0 -741
- package/src/main/templates/project-pages-default-home.html +0 -123
- package/src/main/templates/project-pages-pipeline.md +0 -417
- package/src/seed/prompts/code-review/ac-coverage-check.md +0 -8
- package/src/seed/prompts/code-review/correctness-only.md +0 -8
- package/src/seed/prompts/code-review/full-spectrum-high.md +0 -8
- package/src/seed/prompts/code-review/hallucination-check.md +0 -8
- package/src/seed/prompts/code-review/public-api-compat.md +0 -8
- package/src/seed/prompts/code-review/readability-naming.md +0 -8
- package/src/seed/prompts/debugging/bug-as-failing-test.md +0 -8
- package/src/seed/prompts/debugging/git-bisect-regression.md +0 -8
- package/src/seed/prompts/debugging/instrument-intermittent-bug.md +0 -8
- package/src/seed/prompts/debugging/localize-pipeline-failure.md +0 -8
- package/src/seed/prompts/debugging/reproduce-then-diagnose.md +0 -8
- package/src/seed/prompts/documentation/adr-from-change.md +0 -8
- package/src/seed/prompts/documentation/module-readme.md +0 -8
- package/src/seed/prompts/documentation/onboarding-plan.md +0 -8
- package/src/seed/prompts/documentation/refresh-claude-md.md +0 -8
- package/src/seed/prompts/documentation/tsdoc-public-exports.md +0 -8
- package/src/seed/prompts/git-pr/conventional-commit.md +0 -8
- package/src/seed/prompts/git-pr/draft-pr-title-body.md +0 -8
- package/src/seed/prompts/git-pr/pre-commit-safety-sweep.md +0 -8
- package/src/seed/prompts/git-pr/release-notes-block.md +0 -8
- package/src/seed/prompts/git-pr/split-large-pr.md +0 -8
- package/src/seed/prompts/performance/bundle-startup-audit.md +0 -8
- package/src/seed/prompts/performance/complexity-audit.md +0 -8
- package/src/seed/prompts/performance/cpu-profile-hot-path.md +0 -8
- package/src/seed/prompts/performance/db-query-plan-review.md +0 -8
- package/src/seed/prompts/performance/memory-leak-hunt.md +0 -8
- package/src/seed/prompts/qa/api-contract-tests.md +0 -8
- package/src/seed/prompts/qa/e2e-critical-path.md +0 -8
- package/src/seed/prompts/qa/failing-test-for-bug.md +0 -8
- package/src/seed/prompts/qa/find-missing-test-coverage.md +0 -8
- package/src/seed/prompts/qa/stabilize-flaky-test.md +0 -8
- package/src/seed/prompts/qa/tdd-red-first.md +0 -8
- package/src/seed/prompts/qa/visual-regression-review.md +0 -8
- package/src/seed/prompts/qa/wcag-axe-scan.md +0 -8
- package/src/seed/prompts/refactoring/dead-code-sweep.md +0 -8
- package/src/seed/prompts/refactoring/extract-duplicated-pattern.md +0 -8
- package/src/seed/prompts/refactoring/modernize-legacy-file.md +0 -8
- package/src/seed/prompts/refactoring/reduce-cyclomatic-complexity.md +0 -8
- package/src/seed/prompts/refactoring/tighten-module-boundaries.md +0 -8
- package/src/seed/prompts/security/authz-audit.md +0 -8
- package/src/seed/prompts/security/crypto-correctness.md +0 -8
- package/src/seed/prompts/security/cwe-top-25-hunt.md +0 -8
- package/src/seed/prompts/security/dependency-audit.md +0 -8
- package/src/seed/prompts/security/ipc-boundary-hardening.md +0 -8
- package/src/seed/prompts/security/owasp-top-10-staged-diff.md +0 -8
- package/src/seed/prompts/security/secret-credential-scan.md +0 -8
- package/web/README.md +0 -41
- package/web/project-pages/logic/dist/logic.cjs +0 -4709
- package/web/project-pages/render.cjs +0 -70
- package/web/project-pages/renderer/dist/renderer.cjs +0 -18900
- package/web/project-pages/validate-summary.cjs +0 -62
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* guard-self-schedule.cjs — PreToolUse hook that denies `ScheduleWakeup`,
|
|
4
|
+
* `CronCreate`, and any backgrounded `Task`/`Agent` invocation
|
|
5
|
+
* (`run_in_background` truthy) from INSIDE a scheduler-spawned headless
|
|
6
|
+
* `claude -p` run.
|
|
7
|
+
*
|
|
8
|
+
* Why this exists: standards.md's Execution discipline section already says,
|
|
9
|
+
* in prose, "You ARE the executor — never re-queue or self-schedule" — and
|
|
10
|
+
* spells out the ScheduleWakeup/background-agent case explicitly. Prose has
|
|
11
|
+
* now failed three times: PRD 460 invoked `/develop` and exited 0 having done
|
|
12
|
+
* nothing; PRD 479 landed its commit, then backgrounded `/code-review --fix`
|
|
13
|
+
* + `/security-review` and called ScheduleWakeup to "wait" for them; PRD 1204
|
|
14
|
+
* (2026-09-13) did the same with the `code-review` skill's 8-agent fan-out —
|
|
15
|
+
* correct, committed-ready work sat in a `ListAgents` wait loop until the
|
|
16
|
+
* reaper terminalized the row, work still uncommitted. A headless run has no
|
|
17
|
+
* later turn: nothing ever delivers the wakeup or collects the background
|
|
18
|
+
* agent's result, so the process just exits and the job parks in
|
|
19
|
+
* `needs_review` even when the underlying work was right. A PreToolUse hook
|
|
20
|
+
* is the one lever that sits OUTSIDE the model and can refuse the call before
|
|
21
|
+
* it schedules anything — see guard-prd-writes.cjs's header for the fuller
|
|
22
|
+
* argument for why a hook, not a rule, is the fix.
|
|
23
|
+
*
|
|
24
|
+
* ── Scope: headless scheduler runs ONLY ──────────────────────────────────
|
|
25
|
+
* Detection is purely env-based: `scheduler.cjs`'s `executeJob` stamps every
|
|
26
|
+
* spawned job's child process (and everything it inherits its env to,
|
|
27
|
+
* including PreToolUse hook subprocesses) with `SM_SCHEDULER_JOB_SLUG` — the
|
|
28
|
+
* same marker `scheduler-mcp-server.cjs` already reads to refuse a self-queue
|
|
29
|
+
* `scheduler_create_prd` call, and `prdCreate.cjs` reads to pick the
|
|
30
|
+
* non-interactive default for a disposition prompt. When that env var is
|
|
31
|
+
* absent (an interactive Epic's Terminal/Chat session), this hook allows
|
|
32
|
+
* everything — an interactive Epic must keep full, unrestricted use of
|
|
33
|
+
* ScheduleWakeup and background agents; that is a legitimate, common pattern
|
|
34
|
+
* this guard must never touch.
|
|
35
|
+
*
|
|
36
|
+
* Unlike `SM_SCHEDULER_JOB_MAY_QUEUE` (which exempts an `architect` persona's
|
|
37
|
+
* job so it may still call `scheduler_create_prd`), this guard does NOT carve
|
|
38
|
+
* out any persona. Every headless job — whatever its `agentType` — runs as a
|
|
39
|
+
* single, un-resumable process; scheduling a wakeup or backgrounding a review
|
|
40
|
+
* agent is equally structurally wrong no matter who the job is running as.
|
|
41
|
+
*
|
|
42
|
+
* ── Fail-open everywhere else ─────────────────────────────────────────────
|
|
43
|
+
* Any internal error (malformed stdin JSON, an unexpected payload shape) is
|
|
44
|
+
* logged to stderr and allowed through — a broken guard must never brick the
|
|
45
|
+
* whole tool. This mirrors guard-prd-writes.cjs, not guard-destructive-git.cjs's
|
|
46
|
+
* fail-closed-on-ambiguity stance: unlike an irreversible git operation, a
|
|
47
|
+
* parse failure here just means the run keeps whatever behavior it already
|
|
48
|
+
* had, which is the safer default for a guard scoped this narrowly.
|
|
49
|
+
*
|
|
50
|
+
* ── Install ───────────────────────────────────────────────────────────────
|
|
51
|
+
* Registered in `guardShims.cjs`'s `GUARD_NAMES` and auto-installed by
|
|
52
|
+
* `delegationReadiness.cjs`'s `ensureGuardsInstalled` (the New Epic readiness
|
|
53
|
+
* banner's silent self-heal path) exactly like the other three guards, via
|
|
54
|
+
* the same stable shim (`~/.claude/session-manager/hooks/guard-self-schedule.cjs`)
|
|
55
|
+
* — never a hard-coded ephemeral `npx` app path. Add to a project's
|
|
56
|
+
* `.claude/settings.json` (project scope only):
|
|
57
|
+
*
|
|
58
|
+
* {
|
|
59
|
+
* "hooks": {
|
|
60
|
+
* "PreToolUse": [
|
|
61
|
+
* {
|
|
62
|
+
* "matcher": "ScheduleWakeup|CronCreate|Task|Agent",
|
|
63
|
+
* "hooks": [
|
|
64
|
+
* { "type": "command", "command": "node scripts/hooks/guard-self-schedule.cjs" }
|
|
65
|
+
* ]
|
|
66
|
+
* }
|
|
67
|
+
* ]
|
|
68
|
+
* }
|
|
69
|
+
* }
|
|
70
|
+
*
|
|
71
|
+
* ── Uninstall ────────────────────────────────────────────────────────────
|
|
72
|
+
* Remove this hook's entry from `.claude/settings.json`. The script is inert
|
|
73
|
+
* with no settings.json entry pointing at it.
|
|
74
|
+
*
|
|
75
|
+
* ── Contract ─────────────────────────────────────────────────────────────
|
|
76
|
+
* - Reads a PreToolUse payload on stdin: { tool_name, tool_input, ... }.
|
|
77
|
+
* - Allowed by construction whenever `process.env.SM_SCHEDULER_JOB_SLUG` is
|
|
78
|
+
* unset — the whole point of this guard is to be provably inert outside a
|
|
79
|
+
* headless scheduler run.
|
|
80
|
+
* - Denies `ScheduleWakeup` and `CronCreate` unconditionally when the marker
|
|
81
|
+
* is present.
|
|
82
|
+
* - Denies `Task`/`Agent` (the harness's own tool name for the Agent tool is
|
|
83
|
+
* `Task` — see classifyTranscriptLine.cjs, which checks both names for the
|
|
84
|
+
* same reason) only when `tool_input.run_in_background` is truthy. An
|
|
85
|
+
* inline/synchronous call (absent or explicitly `false`) is always allowed
|
|
86
|
+
* — standards.md sanctions running a review pass synchronously, inline,
|
|
87
|
+
* before the finish protocol.
|
|
88
|
+
* - Every other tool (including the `Skill` tool generally) is allowed by
|
|
89
|
+
* construction — this guard only ever inspects the four names above.
|
|
90
|
+
*/
|
|
91
|
+
'use strict';
|
|
92
|
+
|
|
93
|
+
const ALWAYS_DENY_TOOLS = new Set(['ScheduleWakeup', 'CronCreate']);
|
|
94
|
+
const BACKGROUNDABLE_AGENT_TOOLS = new Set(['Task', 'Agent']);
|
|
95
|
+
|
|
96
|
+
const STANDARDS_QUOTE = 'You ARE the executor — never re-queue or self-schedule.';
|
|
97
|
+
const STANDARDS_CITE = 'plugins/session-manager-dev/skills/develop/standards.md:85 (Execution discipline)';
|
|
98
|
+
|
|
99
|
+
function readStdin() {
|
|
100
|
+
return new Promise((resolve) => {
|
|
101
|
+
let data = '';
|
|
102
|
+
process.stdin.setEncoding('utf8');
|
|
103
|
+
process.stdin.on('data', (chunk) => { data += chunk; });
|
|
104
|
+
process.stdin.on('end', () => resolve(data));
|
|
105
|
+
// Fail open if stdin never closes/errors for some reason.
|
|
106
|
+
process.stdin.on('error', () => resolve(data));
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function allow() {
|
|
111
|
+
process.stdout.write(JSON.stringify({ continue: true }));
|
|
112
|
+
process.exit(0);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function deny(reason) {
|
|
116
|
+
process.stdout.write(JSON.stringify({
|
|
117
|
+
continue: true,
|
|
118
|
+
decision: 'block',
|
|
119
|
+
reason,
|
|
120
|
+
hookSpecificOutput: {
|
|
121
|
+
hookEventName: 'PreToolUse',
|
|
122
|
+
permissionDecision: 'deny',
|
|
123
|
+
permissionDecisionReason: reason,
|
|
124
|
+
},
|
|
125
|
+
}));
|
|
126
|
+
process.exit(0);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function main() {
|
|
130
|
+
const raw = await readStdin();
|
|
131
|
+
let payload;
|
|
132
|
+
try {
|
|
133
|
+
payload = raw ? JSON.parse(raw) : {};
|
|
134
|
+
} catch (e) {
|
|
135
|
+
console.error(`[guard-self-schedule] malformed stdin JSON, failing open: ${e?.message}`);
|
|
136
|
+
return allow();
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
try {
|
|
140
|
+
const jobSlug = process.env.SM_SCHEDULER_JOB_SLUG;
|
|
141
|
+
if (!jobSlug) return allow(); // interactive session — this guard is inert here
|
|
142
|
+
|
|
143
|
+
const toolName = payload.tool_name;
|
|
144
|
+
|
|
145
|
+
if (ALWAYS_DENY_TOOLS.has(toolName)) {
|
|
146
|
+
const reason = [
|
|
147
|
+
`Blocked: \`${toolName}\` from inside a headless scheduler-spawned run (job '${jobSlug}').`,
|
|
148
|
+
`${STANDARDS_CITE}: "${STANDARDS_QUOTE}"`,
|
|
149
|
+
'A headless run has no next turn — nothing will ever deliver this wakeup, so the job exits with its work stranded and parks in needs_review even if it was already correct.',
|
|
150
|
+
'Do it now instead: commit what you have, then run any required review synchronously inline (call the reviewer and read its result in this same turn) before the finish protocol — never schedule it for later.',
|
|
151
|
+
].join(' ');
|
|
152
|
+
return deny(reason);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (BACKGROUNDABLE_AGENT_TOOLS.has(toolName) && payload.tool_input && payload.tool_input.run_in_background) {
|
|
156
|
+
const reason = [
|
|
157
|
+
`Blocked: a backgrounded ${toolName} call (run_in_background: true) from inside a headless scheduler-spawned run (job '${jobSlug}').`,
|
|
158
|
+
`${STANDARDS_CITE}: "${STANDARDS_QUOTE}"`,
|
|
159
|
+
'A headless run has no later turn to collect a background agent\'s result, so waiting on it (ListAgents, Monitor, ScheduleWakeup) strands the job in needs_review with its work already done but uncommitted.',
|
|
160
|
+
'Call it synchronously instead — omit run_in_background (or set it to false) and read its result in this same turn, before the finish protocol.',
|
|
161
|
+
].join(' ');
|
|
162
|
+
return deny(reason);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return allow();
|
|
166
|
+
} catch (e) {
|
|
167
|
+
console.error(`[guard-self-schedule] internal error, failing open: ${e?.message}`);
|
|
168
|
+
return allow();
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
main().catch((e) => {
|
|
173
|
+
console.error(`[guard-self-schedule] unhandled error, failing open: ${e?.message}`);
|
|
174
|
+
allow();
|
|
175
|
+
});
|
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* ops-sweep.cjs — generalized version of
|
|
4
|
+
* session-manager-operations/architecture/ops-maintenance-protocol.md, runnable
|
|
5
|
+
* against ANY project's `session-manager-operations/` folder (Pattern H in that
|
|
6
|
+
* doc). Read-only: never moves, archives, or deletes anything — it only reports.
|
|
7
|
+
*
|
|
8
|
+
* Unlike this project's own audit-ops-hygiene.cjs (which hardcodes
|
|
9
|
+
* session-manager's own OWNERS vocabulary), this script does not assume any
|
|
10
|
+
* particular namespace list. It re-derives the source-of-truth hierarchy for
|
|
11
|
+
* the TARGET project from what's actually on disk there:
|
|
12
|
+
* 1. That project's own CLAUDE.md (if present) — the declared architecture.
|
|
13
|
+
* 2. Each namespace's own README.md under its ops root.
|
|
14
|
+
* 3. On-disk content — folder names, and archived/processed subfolders.
|
|
15
|
+
*
|
|
16
|
+
* It flags, per namespace:
|
|
17
|
+
* - MISSING_README — namespace dir has no README.md
|
|
18
|
+
* - UNDOCUMENTED — namespace name never mentioned anywhere in the
|
|
19
|
+
* project's ownership doc (see below)
|
|
20
|
+
* - CONTRADICTION — the ownership doc contains both "retired/gone"
|
|
21
|
+
* language AND "owner/owns" language for the same
|
|
22
|
+
* namespace, OR the namespace's own README and the
|
|
23
|
+
* ownership doc disagree on retired-vs-active status
|
|
24
|
+
* - NO_RETENTION_POLICY — namespace has an archived/processed subfolder with
|
|
25
|
+
* files but no retention/prune/expire language in its
|
|
26
|
+
* README (or no README at all)
|
|
27
|
+
* - NESTED_CLAUDE_MD_OVER_BUDGET — any CLAUDE.md strictly below the repo root
|
|
28
|
+
* (excluding node_modules) exceeds 4000 bytes. Nested
|
|
29
|
+
* CLAUDE.md files are scoped-context notes, not the
|
|
30
|
+
* root law file, so they get a fixed 4000-byte cap
|
|
31
|
+
* rather than the root's self-declared SIZE BUDGET.
|
|
32
|
+
*
|
|
33
|
+
* Ownership-doc resolution (UNDOCUMENTED + the retired/owner-signal checks that
|
|
34
|
+
* feed CONTRADICTION): the single-writer OWNERS enumeration used to live only in
|
|
35
|
+
* the target project's root CLAUDE.md. It can now instead live in a scoped
|
|
36
|
+
* `session-manager-operations/CLAUDE.md`, so these checks read
|
|
37
|
+
* `<targetCwd>/session-manager-operations/CLAUDE.md` when that file exists and
|
|
38
|
+
* fall back to `<targetCwd>/CLAUDE.md` otherwise — so a project that hasn't
|
|
39
|
+
* split its ops docs out yet keeps working unchanged. The root CLAUDE.md's own
|
|
40
|
+
* CLAUDE_MD_OVER_BUDGET check (against its self-declared SIZE BUDGET header) is
|
|
41
|
+
* unaffected by this fallback and always reads the root file.
|
|
42
|
+
*
|
|
43
|
+
* Never deletes/migrates/archives anything and never files Epics itself — it
|
|
44
|
+
* only prints a JSON report. Routing findings into proposed Epics is the
|
|
45
|
+
* calling skill's job (session-manager-dev:ops-sweep), reported to the human
|
|
46
|
+
* pointed at the target project's own cwd.
|
|
47
|
+
*
|
|
48
|
+
* Usage: node scripts/ops-sweep.cjs <target-project-cwd>
|
|
49
|
+
*/
|
|
50
|
+
'use strict';
|
|
51
|
+
|
|
52
|
+
const fs = require('node:fs');
|
|
53
|
+
const path = require('node:path');
|
|
54
|
+
const { parseClaudeMdBudget, evaluateClaudeMdBudget, computeEpicIndexDrift } = require('../src/main/health.cjs');
|
|
55
|
+
|
|
56
|
+
const targetCwd = process.argv[2];
|
|
57
|
+
if (!targetCwd) {
|
|
58
|
+
process.stderr.write('usage: ops-sweep.cjs <target-project-cwd>\n');
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const OPS_ROOT = path.join(targetCwd, 'session-manager-operations');
|
|
63
|
+
const CLAUDE_MD_PATH = path.join(targetCwd, 'CLAUDE.md');
|
|
64
|
+
const OPS_CLAUDE_MD_PATH = path.join(OPS_ROOT, 'CLAUDE.md');
|
|
65
|
+
const NESTED_CLAUDE_MD_BUDGET_BYTES = 4000;
|
|
66
|
+
|
|
67
|
+
// computeClaudeMdSize(claudeMdText) -> { chars, budget, withinBudget } | null
|
|
68
|
+
//
|
|
69
|
+
// Reuses health.cjs's parseClaudeMdBudget/evaluateClaudeMdBudget (same
|
|
70
|
+
// SIZE BUDGET header convention, same byte-length-not-String.length rule)
|
|
71
|
+
// rather than re-implementing the parse here. Returns null when there's no
|
|
72
|
+
// CLAUDE.md at all, matching hasClaudeMd's existing null-safe shape.
|
|
73
|
+
function computeClaudeMdSize(claudeMdText) {
|
|
74
|
+
if (claudeMdText == null) return null;
|
|
75
|
+
const chars = Buffer.byteLength(claudeMdText, 'utf8');
|
|
76
|
+
const budget = parseClaudeMdBudget(claudeMdText);
|
|
77
|
+
const evaluated = evaluateClaudeMdBudget(chars, budget);
|
|
78
|
+
return { chars, budget, withinBudget: evaluated.ok };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function readText(p) {
|
|
82
|
+
try { return fs.readFileSync(p, 'utf8'); } catch { return null; }
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function listDirEntries(p) {
|
|
86
|
+
try { return fs.readdirSync(p, { withFileTypes: true }); } catch { return []; }
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function countFilesRecursive(dir, depth = 0) {
|
|
90
|
+
if (depth > 4) return 0;
|
|
91
|
+
let count = 0;
|
|
92
|
+
for (const entry of listDirEntries(dir)) {
|
|
93
|
+
if (entry.name.startsWith('.')) continue;
|
|
94
|
+
const full = path.join(dir, entry.name);
|
|
95
|
+
if (entry.isDirectory()) count += countFilesRecursive(full, depth + 1);
|
|
96
|
+
else count += 1;
|
|
97
|
+
}
|
|
98
|
+
return count;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function namespaceMentionRegex(name) {
|
|
102
|
+
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
103
|
+
// The two negative lookaheads right after `name` reject a match that's
|
|
104
|
+
// actually a SUBFOLDER reference ("scheduler/prds/") rather than a mention
|
|
105
|
+
// of the namespace itself: `(?![\w-])` rejects a longer identifier
|
|
106
|
+
// ("scheduler2"), `(?!/[\w-])` rejects a continuing path segment
|
|
107
|
+
// ("scheduler/prds"). A bare mention or a trailing-slash-then-boundary
|
|
108
|
+
// mention ("`scheduler`", "scheduler/") still matches. Without this, a
|
|
109
|
+
// namespace's own README describing a retired SUBFOLDER (e.g.
|
|
110
|
+
// "scheduler/prds/ is RETIRED") reads as the whole namespace being
|
|
111
|
+
// retired.
|
|
112
|
+
return new RegExp(`[\`/]?${escaped}(?![\\w-])(?!/[\\w-])/?[\`]?`, 'i');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Extracts just the immediate clause around a mention (up to the nearest
|
|
116
|
+
// comma/semicolon/period/parenthesis/em-dash boundary, capped at `radius`
|
|
117
|
+
// chars each way) — CLAUDE.md-style docs often list many namespace names in
|
|
118
|
+
// one long sentence, so scoping to "same sentence" is still too coarse and
|
|
119
|
+
// makes every co-listed namespace look contradictory. Scoping to "same
|
|
120
|
+
// clause" keeps a retired/owner signal tied to the namespace it actually
|
|
121
|
+
// describes.
|
|
122
|
+
const CLAUSE_BOUNDARY_RE = /[,;.()]|—|--/;
|
|
123
|
+
function clauseAround(text, matchIndex, matchLen, radius) {
|
|
124
|
+
const start = Math.max(0, matchIndex - radius);
|
|
125
|
+
const end = Math.min(text.length, matchIndex + matchLen + radius);
|
|
126
|
+
let left = text.slice(start, matchIndex);
|
|
127
|
+
let right = text.slice(matchIndex + matchLen, end);
|
|
128
|
+
const leftBoundary = [...left.matchAll(new RegExp(CLAUSE_BOUNDARY_RE, 'g'))].pop();
|
|
129
|
+
if (leftBoundary) left = left.slice(leftBoundary.index + leftBoundary[0].length);
|
|
130
|
+
const rightBoundary = right.match(CLAUSE_BOUNDARY_RE);
|
|
131
|
+
if (rightBoundary) right = right.slice(0, rightBoundary.index);
|
|
132
|
+
return left + text.slice(matchIndex, matchIndex + matchLen) + right;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const RETIRED_RE = /\bretir(ed|ement)?\b|\bis gone\b|\bno longer\b|\bfully retired\b/i;
|
|
136
|
+
const OWNER_RE = /\bOWNERS?\b|\bowns?\b|\bowning\b|\bsole writer\b/i;
|
|
137
|
+
const RETENTION_RE = /\bretention\b|\bprune|\bexpire|\bage out|\bkept for\b|\bkeep(s)? .{0,20}(day|week|month|year)/i;
|
|
138
|
+
const ARCHIVE_DIR_RE = /archiv|process/i;
|
|
139
|
+
|
|
140
|
+
// Scans every namespace-level mention in `text` (per `mentionRe`) and reports
|
|
141
|
+
// whether the immediately surrounding clause carries retired/owner language.
|
|
142
|
+
// Shared by the ownership-doc scan and the namespace's own README scan so
|
|
143
|
+
// both sides of the CONTRADICTION check are scoped identically.
|
|
144
|
+
function clauseMentionSignals(text, mentionRe) {
|
|
145
|
+
let retired = false;
|
|
146
|
+
let owner = false;
|
|
147
|
+
if (!text) return { retired, owner };
|
|
148
|
+
const re = new RegExp(mentionRe.source, 'gi');
|
|
149
|
+
let match;
|
|
150
|
+
while ((match = re.exec(text)) !== null) {
|
|
151
|
+
const clause = clauseAround(text, match.index, match[0].length, 120);
|
|
152
|
+
if (RETIRED_RE.test(clause)) retired = true;
|
|
153
|
+
if (OWNER_RE.test(clause)) owner = true;
|
|
154
|
+
if (re.lastIndex === match.index) re.lastIndex++;
|
|
155
|
+
}
|
|
156
|
+
return { retired, owner };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (!fs.existsSync(OPS_ROOT) || !fs.statSync(OPS_ROOT).isDirectory()) {
|
|
160
|
+
process.stdout.write(JSON.stringify({
|
|
161
|
+
targetCwd,
|
|
162
|
+
opsRoot: OPS_ROOT,
|
|
163
|
+
error: 'no session-manager-operations/ folder found at target cwd',
|
|
164
|
+
hasClaudeMd: fs.existsSync(CLAUDE_MD_PATH),
|
|
165
|
+
claudeMdSize: computeClaudeMdSize(readText(CLAUDE_MD_PATH)),
|
|
166
|
+
namespaces: [],
|
|
167
|
+
findings: [],
|
|
168
|
+
}, null, 2) + '\n');
|
|
169
|
+
process.exit(0);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const claudeMd = readText(CLAUDE_MD_PATH);
|
|
173
|
+
const claudeMdSize = computeClaudeMdSize(claudeMd);
|
|
174
|
+
// The OWNERS enumeration may now live in a scoped session-manager-operations/
|
|
175
|
+
// CLAUDE.md instead of the root one — see the header comment. Namespace-level
|
|
176
|
+
// UNDOCUMENTED/retired/owner-signal checks read that file when it exists.
|
|
177
|
+
const ownershipDocPath = fs.existsSync(OPS_CLAUDE_MD_PATH) ? OPS_CLAUDE_MD_PATH : CLAUDE_MD_PATH;
|
|
178
|
+
const ownershipDoc = ownershipDocPath === CLAUDE_MD_PATH ? claudeMd : readText(ownershipDocPath);
|
|
179
|
+
const namespaceDirs = listDirEntries(OPS_ROOT).filter((e) => e.isDirectory());
|
|
180
|
+
|
|
181
|
+
const namespaces = [];
|
|
182
|
+
const findings = [];
|
|
183
|
+
|
|
184
|
+
// Epic index drift (orphan_rows/orphan_files/unmirrored) — same three counts
|
|
185
|
+
// health.cjs's epic_index component reports, reused here (not
|
|
186
|
+
// re-implemented) so the two never disagree. See computeEpicIndexDrift's
|
|
187
|
+
// header in src/main/health.cjs.
|
|
188
|
+
const epicIndexDrift = computeEpicIndexDrift(targetCwd);
|
|
189
|
+
if (epicIndexDrift.orphan_rows > 0) {
|
|
190
|
+
findings.push({
|
|
191
|
+
type: 'EPIC_INDEX_ORPHAN_ROWS',
|
|
192
|
+
detail: `prompt-sessions/active-index.json has ${epicIndexDrift.orphan_rows} row(s) with no matching `
|
|
193
|
+
+ `prompt-sessions/<id>.json file (${epicIndexDrift.orphanRowIds.join(', ')})`,
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
if (claudeMdSize && !claudeMdSize.withinBudget) {
|
|
197
|
+
findings.push({
|
|
198
|
+
type: 'CLAUDE_MD_OVER_BUDGET',
|
|
199
|
+
detail: `CLAUDE.md is ${claudeMdSize.chars} chars, over its ${claudeMdSize.budget}-char budget by ${claudeMdSize.chars - claudeMdSize.budget}`,
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
for (const entry of namespaceDirs) {
|
|
204
|
+
const name = entry.name;
|
|
205
|
+
const nsPath = path.join(OPS_ROOT, name);
|
|
206
|
+
const readmePath = path.join(nsPath, 'README.md');
|
|
207
|
+
const readme = readText(readmePath);
|
|
208
|
+
|
|
209
|
+
const mentionRe = namespaceMentionRegex(name);
|
|
210
|
+
const mentionedInClaudeMd = ownershipDoc ? mentionRe.test(ownershipDoc) : false;
|
|
211
|
+
|
|
212
|
+
// Clause-scoped, not whole-document: a namespace's own README describing a
|
|
213
|
+
// retired SUBFOLDER of itself ("scheduler/prds/ is RETIRED") must not read
|
|
214
|
+
// as the namespace itself being retired. Scoping both sides (ownership doc
|
|
215
|
+
// AND the namespace's own README) to the clause immediately around an
|
|
216
|
+
// actual namespace-level mention (see namespaceMentionRegex) keeps the two
|
|
217
|
+
// signals comparable apples-to-apples.
|
|
218
|
+
const { retired: claudeMdRetiredSignal, owner: claudeMdOwnerSignal } =
|
|
219
|
+
clauseMentionSignals(ownershipDoc, mentionRe);
|
|
220
|
+
const { retired: readmeRetiredSignal } = clauseMentionSignals(readme, mentionRe);
|
|
221
|
+
|
|
222
|
+
const archiveSubdirs = listDirEntries(nsPath)
|
|
223
|
+
.filter((e) => e.isDirectory() && ARCHIVE_DIR_RE.test(e.name));
|
|
224
|
+
const archivedFileCount = archiveSubdirs.reduce(
|
|
225
|
+
(sum, d) => sum + countFilesRecursive(path.join(nsPath, d.name)),
|
|
226
|
+
0
|
|
227
|
+
);
|
|
228
|
+
const readmeHasRetentionPolicy = readme ? RETENTION_RE.test(readme) : false;
|
|
229
|
+
|
|
230
|
+
const nsFindings = [];
|
|
231
|
+
|
|
232
|
+
if (!readme) {
|
|
233
|
+
nsFindings.push({ type: 'MISSING_README', detail: `${name}/ has no README.md` });
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const ownershipDocRelPath = path.relative(targetCwd, ownershipDocPath);
|
|
237
|
+
|
|
238
|
+
if (ownershipDoc && !mentionedInClaudeMd) {
|
|
239
|
+
nsFindings.push({
|
|
240
|
+
type: 'UNDOCUMENTED',
|
|
241
|
+
detail: `${name}/ is never mentioned in ${ownershipDocRelPath} — no declared ownership or explicit non-owned status`,
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (ownershipDoc && claudeMdRetiredSignal && claudeMdOwnerSignal) {
|
|
246
|
+
nsFindings.push({
|
|
247
|
+
type: 'CONTRADICTION',
|
|
248
|
+
detail: `${name}/ has both "retired/gone" language and "owner/owns" language in ${ownershipDocRelPath} — the doc contradicts itself on this namespace's status`,
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (ownershipDoc && readme && claudeMdRetiredSignal !== readmeRetiredSignal && mentionedInClaudeMd) {
|
|
253
|
+
nsFindings.push({
|
|
254
|
+
type: 'CONTRADICTION',
|
|
255
|
+
detail: `${name}/README.md and ${ownershipDocRelPath} disagree on whether this namespace is retired (README retired=${readmeRetiredSignal}, CLAUDE.md retired=${claudeMdRetiredSignal})`,
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (archivedFileCount > 0 && !readmeHasRetentionPolicy) {
|
|
260
|
+
nsFindings.push({
|
|
261
|
+
type: 'NO_RETENTION_POLICY',
|
|
262
|
+
detail: `${name}/ has ${archivedFileCount} file(s) under archived/processed subfolder(s) (${archiveSubdirs.map((d) => d.name).join(', ')}) but no stated retention policy in its README`,
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
namespaces.push({
|
|
267
|
+
name,
|
|
268
|
+
hasReadme: !!readme,
|
|
269
|
+
mentionedInClaudeMd,
|
|
270
|
+
claudeMdRetiredSignal,
|
|
271
|
+
claudeMdOwnerSignal,
|
|
272
|
+
readmeRetiredSignal,
|
|
273
|
+
archivedFileCount,
|
|
274
|
+
readmeHasRetentionPolicy,
|
|
275
|
+
findings: nsFindings,
|
|
276
|
+
});
|
|
277
|
+
findings.push(...nsFindings.map((f) => ({ namespace: name, ...f })));
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Source lint (PRD 1082): every path inside the ops root must be built by
|
|
281
|
+
// opsOwnership.cjs's opsPath/resolveOpsRoot — the one place that normalizes
|
|
282
|
+
// a worktree or ops-internal cwd and refuses an ephemeral one. Any other file
|
|
283
|
+
// under src/main/ spelling the ops-root literal is a regression of that choke
|
|
284
|
+
// point and is reported as OPS_PATH_LITERAL. Only meaningful when the target
|
|
285
|
+
// IS the session-manager repo (the module has to exist to be bypassed).
|
|
286
|
+
const opsPathLint = (() => {
|
|
287
|
+
const resolverPath = path.join(targetCwd, 'src', 'main', 'lib', 'opsOwnership.cjs');
|
|
288
|
+
if (!fs.existsSync(resolverPath)) return { applicable: false, violations: [] };
|
|
289
|
+
// The shared scanner is dev-only (not in package.json files); an npm-installed copy of this
|
|
290
|
+
// script has no scanner and simply skips the rule.
|
|
291
|
+
let scan;
|
|
292
|
+
try { ({ scan } = require('./check-ops-path-literals.cjs')); } catch { return { applicable: false, violations: [] }; }
|
|
293
|
+
const violations = scan(targetCwd);
|
|
294
|
+
return { applicable: true, violations };
|
|
295
|
+
})();
|
|
296
|
+
for (const v of opsPathLint.violations) {
|
|
297
|
+
findings.push({
|
|
298
|
+
type: 'OPS_PATH_LITERAL',
|
|
299
|
+
file: v.file,
|
|
300
|
+
line: v.line,
|
|
301
|
+
detail: `ops-root literal outside lib/opsOwnership.cjs — build the path with opsPath()/resolveOpsRoot() instead: ${v.text}`,
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Nested-CLAUDE.md budget lint: every CLAUDE.md strictly below the repo root
|
|
306
|
+
// (excluding node_modules and .git) gets a fixed 4000-byte cap — these are
|
|
307
|
+
// scoped-context notes (house style: H1 + "What's here"/"Who consumes
|
|
308
|
+
// this"/"What must NOT be assumed"), not the root law file, so they don't
|
|
309
|
+
// carry a self-declared SIZE BUDGET header the way root CLAUDE.md does.
|
|
310
|
+
const NESTED_CLAUDE_MD_EXCLUDE_DIRS = new Set(['node_modules', '.git']);
|
|
311
|
+
function findNestedClaudeMdFiles(root) {
|
|
312
|
+
const rootClaudeMdPath = path.join(root, 'CLAUDE.md');
|
|
313
|
+
const found = [];
|
|
314
|
+
const walk = (dir) => {
|
|
315
|
+
let entries;
|
|
316
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
|
|
317
|
+
for (const e of entries) {
|
|
318
|
+
if (NESTED_CLAUDE_MD_EXCLUDE_DIRS.has(e.name)) continue;
|
|
319
|
+
const full = path.join(dir, e.name);
|
|
320
|
+
if (e.isDirectory()) { walk(full); continue; }
|
|
321
|
+
if (e.name === 'CLAUDE.md' && full !== rootClaudeMdPath) found.push(full);
|
|
322
|
+
}
|
|
323
|
+
};
|
|
324
|
+
walk(root);
|
|
325
|
+
return found;
|
|
326
|
+
}
|
|
327
|
+
const nestedClaudeMdFiles = findNestedClaudeMdFiles(targetCwd);
|
|
328
|
+
for (const p of nestedClaudeMdFiles) {
|
|
329
|
+
const bytes = Buffer.byteLength(fs.readFileSync(p, 'utf8'), 'utf8');
|
|
330
|
+
if (bytes > NESTED_CLAUDE_MD_BUDGET_BYTES) {
|
|
331
|
+
findings.push({
|
|
332
|
+
type: 'NESTED_CLAUDE_MD_OVER_BUDGET',
|
|
333
|
+
file: path.relative(targetCwd, p),
|
|
334
|
+
detail: `${path.relative(targetCwd, p)} is ${bytes} bytes, over the ${NESTED_CLAUDE_MD_BUDGET_BYTES}-byte nested-CLAUDE.md cap by ${bytes - NESTED_CLAUDE_MD_BUDGET_BYTES}`,
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
process.stdout.write(JSON.stringify({
|
|
340
|
+
targetCwd,
|
|
341
|
+
opsRoot: OPS_ROOT,
|
|
342
|
+
hasClaudeMd: !!claudeMd,
|
|
343
|
+
claudeMdSize,
|
|
344
|
+
ownershipDocPath: path.relative(targetCwd, ownershipDocPath),
|
|
345
|
+
opsPathLint,
|
|
346
|
+
nestedClaudeMdFiles: nestedClaudeMdFiles.map((p) => path.relative(targetCwd, p)),
|
|
347
|
+
epicIndexDrift: {
|
|
348
|
+
orphan_rows: epicIndexDrift.orphan_rows,
|
|
349
|
+
orphan_files: epicIndexDrift.orphan_files,
|
|
350
|
+
unmirrored: epicIndexDrift.unmirrored,
|
|
351
|
+
},
|
|
352
|
+
namespaceCount: namespaces.length,
|
|
353
|
+
namespaces,
|
|
354
|
+
findings,
|
|
355
|
+
}, null, 2) + '\n');
|