claude-code-session-manager 0.87.0 → 0.88.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/README.md +26 -53
- package/dist/assets/{AgentLibrary-DyLWzZDf.js → AgentLibrary-BZ6IY2g1.js} +1 -1
- package/dist/assets/{DataModel--mISIJ6h.js → DataModel-dAoGAODX.js} +1 -1
- package/dist/assets/{History-C2ahUXTg.js → History-CF157HyX.js} +1 -1
- package/dist/assets/{Hooks-BiC6oyR2.js → Hooks-CCPPipBk.js} +1 -1
- package/dist/assets/{HostBilko-BPleEOld.js → HostBilko-Dlmz2m06.js} +1 -1
- package/dist/assets/{Library-Dc8Qst1R.js → Library-BaXcboWX.js} +1 -1
- package/dist/assets/{ListDetail-DIXh-OLX.js → ListDetail-CoUCzG5S.js} +1 -1
- package/dist/assets/{MarkdownEditor-C90bkLXK.js → MarkdownEditor-DTg3JlOT.js} +1 -1
- package/dist/assets/{McpServers-DqcbLOLZ.js → McpServers-DdjY1sA3.js} +1 -1
- package/dist/assets/{Memory-CW62MXlh.js → Memory-BJ3Jlw0C.js} +1 -1
- package/dist/assets/{Panel-Bw1FhRuF.js → Panel-ygRSgc41.js} +1 -1
- package/dist/assets/{Permissions-BcUC-5y8.js → Permissions-m0KAnccF.js} +1 -1
- package/dist/assets/{Plugins-BnKx9flD.js → Plugins-CJRl6B3Q.js} +2 -2
- package/dist/assets/{ProvenanceBadge-Bw5vNVPT.js → ProvenanceBadge-w8cnlVvW.js} +1 -1
- package/dist/assets/{SaveBar-CWr0O_w-.js → SaveBar-BpyPcUIk.js} +1 -1
- package/dist/assets/{Scheduler-DYdLuUqq.js → Scheduler-D2ppxSfy.js} +7 -7
- package/dist/assets/{ScopeSwitcher-CrBLbg8s.js → ScopeSwitcher-EyuIwt-j.js} +1 -1
- package/dist/assets/{Settings-DluB-vN1.js → Settings-BHSGGFEc.js} +1 -1
- package/dist/assets/{SkillReferenceGraph-CHLSseay.js → SkillReferenceGraph-BNaZ2JCF.js} +1 -1
- package/dist/assets/{Skills-gNdo_HNK.js → Skills-Dl3q75QV.js} +1 -1
- package/dist/assets/{SystemPrompt-Cru05-Ia.js → SystemPrompt-Bg6F-Cyp.js} +1 -1
- package/dist/assets/{TagLibrary-DNHY0xou.js → TagLibrary-BSutflCy.js} +1 -1
- package/dist/assets/{TiptapBody-I4lmbCgP.js → TiptapBody-CXih4xrc.js} +1 -1
- package/dist/assets/{Toggle-bWMHjmRh.js → Toggle-2qtUHhA9.js} +1 -1
- package/dist/assets/{index-fc_JjdxL.js → index-B3emy2oI.js} +356 -356
- package/dist/assets/{index-DV3PorRY.css → index-BHTX4OTc.css} +1 -1
- package/dist/assets/{settingsSchema-BfhtZnGD.js → settingsSchema-CdzxaqwM.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 +11 -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-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 +267 -51
- 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 +1598 -381
- 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
|
@@ -47,6 +47,24 @@ const MCP_TOOL_CATALOG = [
|
|
|
47
47
|
exampleArgs: { slug: 'add-mcp-tool-catalog', force: false },
|
|
48
48
|
notes: 'Refuses a job whose status is already "completed" unless force:true is passed — resetting a completed job re-executes already-shipped work.',
|
|
49
49
|
},
|
|
50
|
+
{
|
|
51
|
+
name: 'scheduler_pause',
|
|
52
|
+
group: 'scheduler',
|
|
53
|
+
purpose: "Pause the scheduler (user-initiated) via the session-manager app's admin API.",
|
|
54
|
+
whenToUse: 'Use to stop NEW job dispatch on request (e.g. before a risky change). Running jobs are never killed.',
|
|
55
|
+
whenNotToUse: 'Do not use to cancel a running job (use scheduler_cancel_job) or as a workaround for a stuck queue.',
|
|
56
|
+
exampleArgs: {},
|
|
57
|
+
notes: 'A manual pause survives app restart and is never auto-cleared by the rate-limit timer or network recovery — only scheduler_resume clears it.',
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
name: 'scheduler_resume',
|
|
61
|
+
group: 'scheduler',
|
|
62
|
+
purpose: "Clear any scheduler pause (manual or automatic) via the session-manager app's admin API.",
|
|
63
|
+
whenToUse: 'Use to restart dispatch after scheduler_pause, or to lift an auto-pause you have judged stale.',
|
|
64
|
+
whenNotToUse: 'Do not use to retry a parked needs_review job — that is scheduler_reset_job.',
|
|
65
|
+
exampleArgs: {},
|
|
66
|
+
notes: null,
|
|
67
|
+
},
|
|
50
68
|
{
|
|
51
69
|
name: 'scheduler_list_jobs',
|
|
52
70
|
group: 'scheduler',
|
|
@@ -224,65 +242,18 @@ const MCP_TOOL_CATALOG = [
|
|
|
224
242
|
+ 'delivered.',
|
|
225
243
|
},
|
|
226
244
|
{
|
|
227
|
-
name: '
|
|
228
|
-
group: 'project-home',
|
|
229
|
-
purpose: "Fetch the self-sufficient contract for generating this project's Project Home pages via the "
|
|
230
|
-
+ "session-manager app's admin API — the protocol steps, the ProjectPageSummary/ProjectPagePicks JSON "
|
|
231
|
-
+ 'schemas, the component catalog (all 5 lenses: home, marketing, feature, architecture, brief), the '
|
|
232
|
-
+ 'absolute output paths, and the full pipeline spec text. Nothing in the response requires reading '
|
|
233
|
-
+ "session-manager's own repo — this is what makes generation work on a machine with only the npm "
|
|
234
|
-
+ 'package installed.',
|
|
235
|
-
whenToUse: 'Call this FIRST, before composing any ProjectPageSummary content or picking variants, whenever '
|
|
236
|
-
+ 'a project-home-builder session starts generating Project Home pages for a project.',
|
|
237
|
-
whenNotToUse: 'Do not begin composing a ProjectPageSummary or choosing variants before calling this — it '
|
|
238
|
-
+ 'must be the FIRST call a project-home-builder session makes; every other project_home_* tool assumes '
|
|
239
|
-
+ "you already have this response's schema/catalog/paths in hand.",
|
|
240
|
-
exampleArgs: { cwd: '/home/bilko/Projects/session-manager' },
|
|
241
|
-
notes: "cwd is optional — defaults to the calling session's own project root (SM_PROJECT_ROOT or process.cwd()) when omitted.",
|
|
242
|
-
},
|
|
243
|
-
{
|
|
244
|
-
name: 'project_home_validate_summary',
|
|
245
|
-
group: 'project-home',
|
|
246
|
-
purpose: 'Validate a composed ProjectPageSummary against the schema from project_home_get_contract via the '
|
|
247
|
-
+ "session-manager app's admin API, without writing anything to disk.",
|
|
248
|
-
whenToUse: 'Use after composing (or editing) a ProjectPageSummary and before calling project_home_render — '
|
|
249
|
-
+ 'fix every returned {field, message} error and re-validate until valid:true.',
|
|
250
|
-
whenNotToUse: 'Do not skip straight to project_home_render on an unvalidated summary — render does '
|
|
251
|
-
+ 're-validate server-side and rejects with no writes on failure, but catching errors here first saves a '
|
|
252
|
-
+ 'round trip and gives per-field detail sooner.',
|
|
253
|
-
exampleArgs: { cwd: '/home/bilko/Projects/session-manager', summary: { title: 'Example Project' } },
|
|
254
|
-
notes: "cwd is optional — defaults to the calling session's own project root (SM_PROJECT_ROOT or process.cwd()) when omitted.",
|
|
255
|
-
},
|
|
256
|
-
{
|
|
257
|
-
name: 'project_home_render',
|
|
258
|
-
group: 'project-home',
|
|
259
|
-
purpose: 'Render and write all 5 Project Home lenses (home, marketing, feature, architecture, brief) plus '
|
|
260
|
-
+ "summary.json/picks.json/manifest.json via the session-manager app's admin API — the only write path "
|
|
261
|
-
+ 'for Project Home generation.',
|
|
262
|
-
whenToUse: 'Use once project_home_validate_summary reports valid:true and a variant has been picked for '
|
|
263
|
-
+ "every lens/slot in the contract's catalog.",
|
|
264
|
-
whenNotToUse: 'Must NOT be used to fabricate summary content — every field of the summary passed here must '
|
|
265
|
-
+ 'trace to something concrete about the real project (an Epic goal, a source file/dir, a convention, a '
|
|
266
|
-
+ 'git log entry). This tool re-validates the summary server-side and rejects with no writes on schema '
|
|
267
|
-
+ 'failure, but it only checks shape, never truthfulness.',
|
|
268
|
-
exampleArgs: {
|
|
269
|
-
cwd: '/home/bilko/Projects/session-manager',
|
|
270
|
-
summary: { title: 'Example Project' },
|
|
271
|
-
picks: { home: { hero: 'variant-a' } },
|
|
272
|
-
},
|
|
273
|
-
notes: "cwd is optional — defaults to the calling session's own project root (SM_PROJECT_ROOT or process.cwd()) when omitted.",
|
|
274
|
-
},
|
|
275
|
-
{
|
|
276
|
-
name: 'project_home_status',
|
|
245
|
+
name: 'project_home_write',
|
|
277
246
|
group: 'project-home',
|
|
278
|
-
purpose: "
|
|
279
|
-
+ "
|
|
280
|
-
+ '
|
|
281
|
-
whenToUse: 'Use
|
|
282
|
-
+ '
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
247
|
+
purpose: "Write this project's Project Home page — ONE self-contained HTML document — to "
|
|
248
|
+
+ "session-manager-operations/project-pages/home.html via the session-manager app's admin API. "
|
|
249
|
+
+ 'The only write path for Project Home.',
|
|
250
|
+
whenToUse: 'Use when asked to generate or refresh the project home page. Compose the whole page yourself '
|
|
251
|
+
+ '(inline <style>/<script>, data: URIs for assets) from what is really true about the project, then '
|
|
252
|
+
+ 'pass the full document as `html`.',
|
|
253
|
+
whenNotToUse: 'Never fabricate content — every claim must trace to something concrete in the project. The '
|
|
254
|
+
+ 'document must be self-contained: the call is rejected for empty input, more than 1MB, any '
|
|
255
|
+
+ '<script src>, <link href="http…">, @import, or url(http…) — no network egress.',
|
|
256
|
+
exampleArgs: { cwd: '/home/bilko/Projects/session-manager', html: '<!DOCTYPE html><html><body><h1>Example</h1></body></html>' },
|
|
286
257
|
notes: "cwd is optional — defaults to the calling session's own project root (SM_PROJECT_ROOT or process.cwd()) when omitted.",
|
|
287
258
|
},
|
|
288
259
|
{
|
|
@@ -335,25 +306,6 @@ const MCP_RECIPES = [
|
|
|
335
306
|
'The call only delivers a PROPOSED session in the receiving project — nothing runs until a human there presses Approve & start; there is no reply channel.',
|
|
336
307
|
],
|
|
337
308
|
},
|
|
338
|
-
{
|
|
339
|
-
id: 'generate-project-home',
|
|
340
|
-
title: "Generate a project's Project Home pages",
|
|
341
|
-
steps: [
|
|
342
|
-
"Call project_home_get_contract to get the protocol, ProjectPageSummary/ProjectPagePicks schemas, "
|
|
343
|
-
+ 'component catalog, absolute output paths, and pipeline spec — the FIRST call any '
|
|
344
|
-
+ 'project-home-builder session makes.',
|
|
345
|
-
'Author a ProjectPageSummary matching the contract\'s summarySchema — every field must trace to '
|
|
346
|
-
+ 'something concrete about the real project; never fabricate content.',
|
|
347
|
-
'Call project_home_validate_summary with { cwd, summary } and fix every {field, message} error it '
|
|
348
|
-
+ 'returns, re-validating until valid:true.',
|
|
349
|
-
"For each lens and slot in the contract's catalog, choose exactly one variant id by judging the "
|
|
350
|
-
+ 'candidate variants against the summary you composed, assembling a ProjectPagePicks object '
|
|
351
|
-
+ '(lensId -> slotId -> variantId).',
|
|
352
|
-
'Call project_home_render with { cwd, summary, picks } to render all 5 lenses and write '
|
|
353
|
-
+ 'summary.json/picks.json/output files.',
|
|
354
|
-
"Call project_home_status to confirm the new files landed and read manifest.json's generatedAt.",
|
|
355
|
-
],
|
|
356
|
-
},
|
|
357
309
|
];
|
|
358
310
|
|
|
359
311
|
for (const entry of MCP_TOOL_CATALOG) {
|
|
@@ -48,17 +48,10 @@ const OWNERS = Object.freeze({
|
|
|
48
48
|
'scheduler': 'scheduler',
|
|
49
49
|
// Project Home owns the synthesized Brief (generate + hand-edit).
|
|
50
50
|
'project-brief': 'project-home',
|
|
51
|
-
// Project Home
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
// the SAME folder is also written directly by a project-home-builder
|
|
56
|
-
// Epic's own Write tool, which never goes through config.cjs and so is
|
|
57
|
-
// NOT governed by this table at all (see
|
|
58
|
-
// session-manager-operations/project-pages/README.md). Declaring the
|
|
59
|
-
// namespace here only closes the fail-closed gap for the app's own write
|
|
60
|
-
// path — it does not claim exclusivity over the agent's Write-tool calls,
|
|
61
|
-
// which remain outside this law by construction.
|
|
51
|
+
// Project Home owns project-pages/home.html, written only via the
|
|
52
|
+
// project_home_write MCP tool -> /admin/project-home/write route
|
|
53
|
+
// (config.cjs's writeTextAtomic). See
|
|
54
|
+
// session-manager-operations/project-pages/README.md.
|
|
62
55
|
'project-pages': 'project-home',
|
|
63
56
|
// Structured per-tab error log lines (JSONL), tagged for tracing/analysis.
|
|
64
57
|
'logs': 'logs',
|
|
@@ -177,11 +170,13 @@ function assertOpsWrite(absPath, writer) {
|
|
|
177
170
|
// worktree-cwd hazard (PRD 1082; incidents 2026-08-30, 2026-09-01).
|
|
178
171
|
const { inOps } = parseOpsPath(absPath);
|
|
179
172
|
if (inOps) {
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
const {
|
|
184
|
-
|
|
173
|
+
// innermostOpsRoot (lastIndexOf) is the fact THIS gate keys on; activeSessions
|
|
174
|
+
// truncates at the outermost instead — cwdClassify returns both, unified in
|
|
175
|
+
// neither direction.
|
|
176
|
+
const { classifyCwd } = require('./cwdClassify.cjs');
|
|
177
|
+
const projectRoot = path.dirname(classifyCwd(absPath).innermostOpsRoot);
|
|
178
|
+
const { kind, reason } = classifyCwd(projectRoot);
|
|
179
|
+
if (kind === 'ephemeral' || kind === 'worktree') {
|
|
185
180
|
const err = new Error(
|
|
186
181
|
`refusing to write ${OPS_ROOT_DIR}/ state under an ephemeral project root `
|
|
187
182
|
+ `(tmpdir or linked git worktree): "${projectRoot}" — see lib/opsOwnership.cjs resolveProjectRoot`,
|
|
@@ -189,6 +184,17 @@ function assertOpsWrite(absPath, writer) {
|
|
|
189
184
|
err.ephemeral = true;
|
|
190
185
|
throw err;
|
|
191
186
|
}
|
|
187
|
+
if (kind === 'unknown') {
|
|
188
|
+
// Fail closed on the unprovable case (worktree .git file whose admin
|
|
189
|
+
// back-reference is gone, garbled .git file): the exact shape behind the
|
|
190
|
+
// lost-Epics incidents. WRITES only — reads and dispatch stay open.
|
|
191
|
+
const err = new Error(
|
|
192
|
+
`refusing to write ${OPS_ROOT_DIR}/ state under "${projectRoot}": project identity `
|
|
193
|
+
+ `cannot be proven (${reason}) — see lib/cwdClassify.cjs`,
|
|
194
|
+
);
|
|
195
|
+
err.unknownCwd = true;
|
|
196
|
+
throw err;
|
|
197
|
+
}
|
|
192
198
|
}
|
|
193
199
|
}
|
|
194
200
|
|
|
@@ -236,10 +242,12 @@ function resolveProjectRoot(cwd, { opsInternal = 'normalize' } = {}) {
|
|
|
236
242
|
`resolveProjectRoot: cwd must be a project root, not a path inside ${OPS_ROOT_DIR}/, got "${cwd}"`,
|
|
237
243
|
);
|
|
238
244
|
}
|
|
239
|
-
// Lazy:
|
|
240
|
-
const {
|
|
245
|
+
// Lazy: cwdClassify → schedulerPaths; ephemeralCwd → cwdClassify.
|
|
246
|
+
const { classifyCwd } = require('./cwdClassify.cjs');
|
|
241
247
|
const { isEphemeralCwd } = require('./ephemeralCwd.cjs');
|
|
242
|
-
|
|
248
|
+
// `unknown` deliberately does NOT throw here: reads and dispatch must keep
|
|
249
|
+
// working; assertOpsWrite is the choke point that refuses the WRITE.
|
|
250
|
+
const root = classifyCwd(cwd).projectRoot;
|
|
243
251
|
if (isEphemeralCwd(root)) {
|
|
244
252
|
const err = new Error(
|
|
245
253
|
`resolveProjectRoot: refusing ephemeral cwd (tmpdir or linked git worktree), got "${cwd}"`,
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// Seed-when-the-stamp-differs for the bundled PRD_AUTHORING.md guide.
|
|
3
|
+
// Pure fs + injected writer so it is testable without an Electron app.
|
|
4
|
+
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
|
|
7
|
+
const STAMP_RE = /^<!--\s*PRD_AUTHORING\.md\s+(v\d+)\s*-->/;
|
|
8
|
+
|
|
9
|
+
/** Version stamp from line 1 of `text`, or null when absent. */
|
|
10
|
+
function readStamp(text) {
|
|
11
|
+
const first = String(text).split(/\r?\n/, 1)[0];
|
|
12
|
+
const m = STAMP_RE.exec(first);
|
|
13
|
+
return m ? m[1] : null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** True when dest should be (re)written from src: missing/unstamped/differing stamp. */
|
|
17
|
+
function shouldSeed(srcText, destText) {
|
|
18
|
+
if (destText == null) return true;
|
|
19
|
+
const destStamp = readStamp(destText);
|
|
20
|
+
return destStamp === null || destStamp !== readStamp(srcText);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Overwrite `dest` with `src` when shouldSeed. `write(abs, text)` is the atomic
|
|
25
|
+
* writer (config.writeTextAtomic). Resolves true when it wrote, false otherwise.
|
|
26
|
+
*/
|
|
27
|
+
async function seedAuthoringGuide({ src, dest, write }) {
|
|
28
|
+
if (!fs.existsSync(src)) return false;
|
|
29
|
+
const srcText = fs.readFileSync(src, 'utf8');
|
|
30
|
+
const destText = fs.existsSync(dest) ? fs.readFileSync(dest, 'utf8') : null;
|
|
31
|
+
if (!shouldSeed(srcText, destText)) return false;
|
|
32
|
+
await write(dest, srcText);
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
module.exports = { readStamp, shouldSeed, seedAuthoringGuide };
|
|
@@ -173,7 +173,8 @@ function resolvePrdWriteDir(cwd) {
|
|
|
173
173
|
// archived-twin stale-queue-row guard (scheduler.cjs's archivedTwinExists)
|
|
174
174
|
// calls listArchivedPrdDirs directly, uncached — so this is purely a
|
|
175
175
|
// reporting-freshness bound (schedule:list-prds's per-Epic PRD/run counts).
|
|
176
|
-
|
|
176
|
+
// Above the 60 s dispatch-loop interval (scheduler POLL_INTERVAL_MS).
|
|
177
|
+
const ASSEMBLED_DIRS_CACHE_TTL_MS = 120_000;
|
|
177
178
|
const assembledPrdsDirsCache = new Map(); // callerKey -> { freshnessKey, cachedAt, result }
|
|
178
179
|
const assembledArchivedPrdsDirsCache = new Map();
|
|
179
180
|
|
|
@@ -215,6 +216,7 @@ function memoizedAssembledDirs(cache, maxAgeMin, opts, compute) {
|
|
|
215
216
|
}
|
|
216
217
|
|
|
217
218
|
const result = compute(allCwds, activeCwds);
|
|
219
|
+
if (Array.isArray(result)) Object.freeze(result);
|
|
218
220
|
cache.set(callerKey, { freshnessKey, cachedAt: Date.now(), result });
|
|
219
221
|
return result;
|
|
220
222
|
}
|
|
@@ -338,6 +340,50 @@ function deriveEpicIdFromPrdPath(filePath) {
|
|
|
338
340
|
return epicId;
|
|
339
341
|
}
|
|
340
342
|
|
|
343
|
+
/**
|
|
344
|
+
* deriveProjectCwdFromPrdPath(filePath) → projectCwd | null
|
|
345
|
+
*
|
|
346
|
+
* The inverse of resolvePrdWriteDir/resolveEpicPrdWriteDir: given an absolute
|
|
347
|
+
* PRD file path (flat `prds/`, Epic-scoped `epics/<id>/prds/`, or either's
|
|
348
|
+
* `prds-archived/` sibling), returns the project root it was discovered
|
|
349
|
+
* under — by finding the `session-manager-operations` segment and validating
|
|
350
|
+
* the path round-trips back through the same resolver it walked up from.
|
|
351
|
+
* Returns null for anything that doesn't match one of those shapes (never
|
|
352
|
+
* guesses). Used by reconcile()'s fresh-discovery path so a PRD file with no
|
|
353
|
+
* `cwd:` frontmatter still gets a real project cwd instead of null (which
|
|
354
|
+
* would otherwise fall through to schedulerBatch.js's DEFAULT_PROJECT_CWD
|
|
355
|
+
* and relocate the row into the wrong project's queue.json shard).
|
|
356
|
+
*/
|
|
357
|
+
function deriveProjectCwdFromPrdPath(filePath) {
|
|
358
|
+
if (!filePath || typeof filePath !== 'string' || !path.isAbsolute(filePath)) return null;
|
|
359
|
+
const segments = filePath.split(path.sep);
|
|
360
|
+
const opsIdx = segments.lastIndexOf(OPS_ROOT_DIR);
|
|
361
|
+
if (opsIdx <= 0) return null;
|
|
362
|
+
const projectCwd = segments.slice(0, opsIdx).join(path.sep);
|
|
363
|
+
const prdsDir = path.dirname(filePath);
|
|
364
|
+
const dirName = path.basename(prdsDir);
|
|
365
|
+
if (dirName !== 'prds' && dirName !== 'prds-archived') return null;
|
|
366
|
+
if (dirName === 'prds') {
|
|
367
|
+
try {
|
|
368
|
+
if (resolvePrdWriteDir(projectCwd) === prdsDir) return projectCwd;
|
|
369
|
+
} catch { /* fall through to the Epic-scoped check below */ }
|
|
370
|
+
}
|
|
371
|
+
if (dirName === 'prds-archived') {
|
|
372
|
+
// Flat legacy archive sibling of resolvePrdWriteDir's flat `prds/` (see
|
|
373
|
+
// listArchivedPrdDirs' first entry: opsPath(cwd, ...PRD_SUBPATH, '..',
|
|
374
|
+
// 'prds-archived')) — round-trip it the same way the flat `prds` branch
|
|
375
|
+
// above does, before falling through to the Epic-scoped check (which
|
|
376
|
+
// can never match a flat path: deriveEpicIdFromPrdPath requires an
|
|
377
|
+
// `epics/<id>/` segment a flat archive dir doesn't have).
|
|
378
|
+
try {
|
|
379
|
+
if (opsPath(projectCwd, ...PRD_SUBPATH, '..', 'prds-archived') === prdsDir) return projectCwd;
|
|
380
|
+
} catch { /* fall through to the Epic-scoped check below */ }
|
|
381
|
+
}
|
|
382
|
+
const epicId = deriveEpicIdFromPrdPath(filePath);
|
|
383
|
+
if (epicId) return projectCwd;
|
|
384
|
+
return null;
|
|
385
|
+
}
|
|
386
|
+
|
|
341
387
|
module.exports = {
|
|
342
388
|
resolvePrdWriteDir,
|
|
343
389
|
resolvePrdsDirs,
|
|
@@ -347,6 +393,7 @@ module.exports = {
|
|
|
347
393
|
listEpicPrdDirs,
|
|
348
394
|
listArchivedPrdDirs,
|
|
349
395
|
deriveEpicIdFromPrdPath,
|
|
396
|
+
deriveProjectCwdFromPrdPath,
|
|
350
397
|
PRD_SUBPATH,
|
|
351
398
|
EPICS_SUBPATH,
|
|
352
399
|
};
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* procIdentity.cjs — fail-closed process identity, so a recycled pid can
|
|
3
|
+
* never be mistaken for the process that originally held it.
|
|
4
|
+
*
|
|
5
|
+
* identity(pid) reads /proc/<pid>/stat field 22 (starttime) — located AFTER
|
|
6
|
+
* the last ')', because `comm` (field 2) can itself contain spaces or
|
|
7
|
+
* parens — plus /proc/<pid>/cmdline. On a platform with no /proc (macOS),
|
|
8
|
+
* falls back to `ps -o lstart=,command=` (lstart has only second
|
|
9
|
+
* granularity, which is the known limit of that path).
|
|
10
|
+
*
|
|
11
|
+
* isDifferentProcess(recorded, live) is the ONLY decision function callers
|
|
12
|
+
* should use, and it is deliberately fail-closed in the SAFE direction: it
|
|
13
|
+
* returns true — "safe to treat the recorded holder as gone" — ONLY when
|
|
14
|
+
* BOTH sides are a complete identity AND they differ. An unreadable /proc
|
|
15
|
+
* entry, a missing live probe, or a legacy record with no identity fields at
|
|
16
|
+
* all all read as "not different" — i.e. today's pid-only behaviour, never a
|
|
17
|
+
* false positive that could wrongly break a live lock or veto a real kill.
|
|
18
|
+
*
|
|
19
|
+
* SM_PROC_ROOT overrides the /proc root — tests only, so stat/cmdline
|
|
20
|
+
* fixtures can be written under a temp dir instead of touching real /proc.
|
|
21
|
+
*/
|
|
22
|
+
'use strict';
|
|
23
|
+
|
|
24
|
+
const fs = require('node:fs');
|
|
25
|
+
const path = require('node:path');
|
|
26
|
+
const { execFileSync } = require('node:child_process');
|
|
27
|
+
|
|
28
|
+
const MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
29
|
+
|
|
30
|
+
function procRoot() {
|
|
31
|
+
return process.env.SM_PROC_ROOT || '/proc';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function hasProc() {
|
|
35
|
+
try {
|
|
36
|
+
return fs.existsSync(procRoot());
|
|
37
|
+
} catch {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Field 22 (starttime) sits AFTER the last ')' — comm can hold spaces/parens. */
|
|
43
|
+
function parseStatStartTicks(raw) {
|
|
44
|
+
const lastParen = raw.lastIndexOf(')');
|
|
45
|
+
if (lastParen === -1) return null;
|
|
46
|
+
const rest = raw.slice(lastParen + 1).trim();
|
|
47
|
+
if (!rest) return null;
|
|
48
|
+
const fields = rest.split(/\s+/);
|
|
49
|
+
// `rest[0]` is field 3 (state); field N sits at index (N - 3), so field 22
|
|
50
|
+
// (starttime) is index 19.
|
|
51
|
+
const startTicksStr = fields[19];
|
|
52
|
+
if (startTicksStr === undefined) return null;
|
|
53
|
+
const startTicks = Number(startTicksStr);
|
|
54
|
+
return Number.isFinite(startTicks) ? startTicks : null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function readCmdlineFile(pid) {
|
|
58
|
+
const raw = fs.readFileSync(path.join(procRoot(), String(pid), 'cmdline'), 'utf8');
|
|
59
|
+
const cmd = raw.replace(/\0+$/, '').split('\0').join(' ').trim();
|
|
60
|
+
return cmd.length > 0 ? cmd : null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function identityFromProc(pid) {
|
|
64
|
+
let startTicks = null;
|
|
65
|
+
let cmdline = null;
|
|
66
|
+
try {
|
|
67
|
+
const raw = fs.readFileSync(path.join(procRoot(), String(pid), 'stat'), 'utf8');
|
|
68
|
+
startTicks = parseStatStartTicks(raw);
|
|
69
|
+
} catch { /* unreadable stat (dead pid, hidepid, race) — leave null */ }
|
|
70
|
+
try {
|
|
71
|
+
cmdline = readCmdlineFile(pid);
|
|
72
|
+
} catch { /* unreadable cmdline — leave null */ }
|
|
73
|
+
return { pid, startTicks, cmdline, complete: startTicks !== null && cmdline !== null };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function parseLstart(lstart) {
|
|
77
|
+
const m = /^\w{3}\s+(\w{3})\s+(\d{1,2})\s+(\d{2}):(\d{2}):(\d{2})\s+(\d{4})$/.exec(lstart.trim());
|
|
78
|
+
if (!m) return null;
|
|
79
|
+
const monthIdx = MONTHS.indexOf(m[1]);
|
|
80
|
+
if (monthIdx === -1) return null;
|
|
81
|
+
const ms = Date.UTC(Number(m[6]), monthIdx, Number(m[2]), Number(m[3]), Number(m[4]), Number(m[5]));
|
|
82
|
+
return Number.isFinite(ms) ? ms : null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function identityFromPs(pid) {
|
|
86
|
+
let out;
|
|
87
|
+
try {
|
|
88
|
+
out = execFileSync('ps', ['-p', String(pid), '-o', 'lstart=,command='], { encoding: 'utf8' });
|
|
89
|
+
} catch {
|
|
90
|
+
return { pid, startTicks: null, cmdline: null, complete: false };
|
|
91
|
+
}
|
|
92
|
+
const trimmed = out.replace(/\n$/, '');
|
|
93
|
+
const m = /^(\w{3}\s+\w{3}\s+\d{1,2}\s+\d{2}:\d{2}:\d{2}\s+\d{4})\s(.*)$/.exec(trimmed);
|
|
94
|
+
if (!m) return { pid, startTicks: null, cmdline: null, complete: false };
|
|
95
|
+
const startTicks = parseLstart(m[1]);
|
|
96
|
+
const cmdline = m[2].trim() || null;
|
|
97
|
+
return { pid, startTicks, cmdline, complete: startTicks !== null && cmdline !== null };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** identity(pid) → {pid, startTicks, cmdline, complete}. Never throws. */
|
|
101
|
+
function identity(pid) {
|
|
102
|
+
if (!Number.isInteger(pid) || pid <= 0) {
|
|
103
|
+
return { pid, startTicks: null, cmdline: null, complete: false };
|
|
104
|
+
}
|
|
105
|
+
try {
|
|
106
|
+
return hasProc() ? identityFromProc(pid) : identityFromPs(pid);
|
|
107
|
+
} catch {
|
|
108
|
+
return { pid, startTicks: null, cmdline: null, complete: false };
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* isDifferentProcess(recorded, live) — true ONLY when both sides are a
|
|
114
|
+
* COMPLETE identity and differ. Every other case (missing side, incomplete
|
|
115
|
+
* side, a legacy record with no identity fields) is "not different", i.e.
|
|
116
|
+
* today's pid-only behaviour — the fail-closed default.
|
|
117
|
+
*/
|
|
118
|
+
function isDifferentProcess(recorded, live) {
|
|
119
|
+
if (!recorded || !live) return false;
|
|
120
|
+
if (!recorded.complete || !live.complete) return false;
|
|
121
|
+
return recorded.pid !== live.pid
|
|
122
|
+
|| recorded.startTicks !== live.startTicks
|
|
123
|
+
|| recorded.cmdline !== live.cmdline;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
module.exports = { identity, isDifferentProcess };
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* procName.cjs — make Session Manager's spawned processes distinguishable in
|
|
3
|
+
* GNOME System Monitor / ps / htop.
|
|
4
|
+
*
|
|
5
|
+
* - aliasBinFor(realBin, alias): the kernel sets `comm` (the "Process Name"
|
|
6
|
+
* column) to the basename of the path handed to execve, capped at 15 chars
|
|
7
|
+
* (TASK_COMM_LEN 16 incl. NUL). argv0 does NOT affect it, so we exec a
|
|
8
|
+
* symlink named `alias` that points at the real binary.
|
|
9
|
+
* - smArgv0(role, detail): a bounded argv0 label for /proc/pid/cmdline. It
|
|
10
|
+
* ALWAYS contains `claude` as a word — /\bclaude\b/ gates killOrphanClaudePid
|
|
11
|
+
* (scheduler.cjs), claudePidAlive and findLiveProcessForJob (reaperHelpers.cjs).
|
|
12
|
+
*
|
|
13
|
+
* Fail-open is absolute: any error returns the ORIGINAL bin. Naming must never
|
|
14
|
+
* stop a job from launching.
|
|
15
|
+
*
|
|
16
|
+
* DELIBERATE exceptions to CLAUDE.md laws (do not "fix"):
|
|
17
|
+
* - The tmp+rename symlink primitive below is NOT config.cjs writeJson /
|
|
18
|
+
* writeTextAtomic: those write file contents and cannot create symlinks.
|
|
19
|
+
* - procnamesRoot() is intentionally NOT registered with config.cjs validateWrite /
|
|
20
|
+
* addAllowedRoot: that containment governs IPC-reachable writes; this is a
|
|
21
|
+
* main-process-internal write.
|
|
22
|
+
*
|
|
23
|
+
* Not to be confused with procIdentity.cjs (pid-recycling fingerprint).
|
|
24
|
+
*/
|
|
25
|
+
'use strict';
|
|
26
|
+
|
|
27
|
+
const fs = require('node:fs');
|
|
28
|
+
const path = require('node:path');
|
|
29
|
+
|
|
30
|
+
const { procnamesRoot } = require('./schedulerPaths.cjs');
|
|
31
|
+
const MAX_ALIAS_LEN = 15; // Linux TASK_COMM_LEN (16) minus NUL
|
|
32
|
+
const MAX_LABEL_LEN = 120;
|
|
33
|
+
|
|
34
|
+
let tmpSeq = 0;
|
|
35
|
+
|
|
36
|
+
function resolveBare(bin) {
|
|
37
|
+
if (path.isAbsolute(bin)) return bin;
|
|
38
|
+
try {
|
|
39
|
+
// Lazy: openExternalApp requires 'electron', unavailable outside the app.
|
|
40
|
+
const { findCommand } = require('./openExternalApp.cjs');
|
|
41
|
+
const found = findCommand(bin);
|
|
42
|
+
return found && path.isAbsolute(found) ? found : null;
|
|
43
|
+
} catch { return null; }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** O(1) fs ops. Returns alias path on success, else `realBin` unchanged. */
|
|
47
|
+
function aliasBinFor(realBin, alias) {
|
|
48
|
+
if (typeof alias !== 'string' || !alias || alias.length > MAX_ALIAS_LEN || /[\\/\0]/.test(alias) || alias === '.' || alias === '..') {
|
|
49
|
+
throw new Error(`procName: alias "${alias}" must be 1-${MAX_ALIAS_LEN} chars (Linux TASK_COMM_LEN) with no path separators`);
|
|
50
|
+
}
|
|
51
|
+
try {
|
|
52
|
+
if (typeof realBin !== 'string' || !realBin) return realBin;
|
|
53
|
+
const target = resolveBare(realBin);
|
|
54
|
+
if (!target) return realBin;
|
|
55
|
+
const aliasPath = path.join(procnamesRoot(), alias);
|
|
56
|
+
let current = null;
|
|
57
|
+
try { current = fs.readlinkSync(aliasPath); } catch { /* absent or not a link */ }
|
|
58
|
+
if (current !== target) {
|
|
59
|
+
fs.mkdirSync(procnamesRoot(), { recursive: true });
|
|
60
|
+
const tmp = path.join(procnamesRoot(), `.${alias}.${process.pid}.${Date.now()}.${tmpSeq++}.tmp`);
|
|
61
|
+
try {
|
|
62
|
+
fs.symlinkSync(target, tmp);
|
|
63
|
+
fs.renameSync(tmp, aliasPath); // atomic replace; never unlink-then-symlink
|
|
64
|
+
} catch (err) {
|
|
65
|
+
try { fs.unlinkSync(tmp); } catch { /* nothing to clean */ }
|
|
66
|
+
if (err && err.code !== 'EEXIST') throw err; // losing racer: swallow
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
fs.accessSync(aliasPath, fs.constants.X_OK); // noexec / dangling probe
|
|
70
|
+
return aliasPath;
|
|
71
|
+
} catch {
|
|
72
|
+
return realBin;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** `sm-claude-<role>:<detail>`, sanitised to [A-Za-z0-9._-], <= 120 chars. Always contains word `claude`. */
|
|
77
|
+
function smArgv0(role, detail) {
|
|
78
|
+
const clean = (v) => String(v == null ? '' : v).replace(/[^A-Za-z0-9._-]/g, '');
|
|
79
|
+
const r = clean(role) || 'proc';
|
|
80
|
+
const d = clean(detail);
|
|
81
|
+
return `sm-claude-${r}${d ? `:${d}` : ''}`.slice(0, MAX_LABEL_LEN);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Remove alias entries whose target no longer resolves to an executable. Never throws. O(entries). */
|
|
85
|
+
function pruneStaleAliases() {
|
|
86
|
+
try {
|
|
87
|
+
for (const name of fs.readdirSync(procnamesRoot())) {
|
|
88
|
+
const p = path.join(procnamesRoot(), name);
|
|
89
|
+
try {
|
|
90
|
+
fs.accessSync(p, fs.constants.X_OK);
|
|
91
|
+
} catch {
|
|
92
|
+
try { fs.unlinkSync(p); } catch { /* best effort */ }
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
} catch { /* root absent/unreadable */ }
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
module.exports = { aliasBinFor, smArgv0, procnamesRoot, pruneStaleAliases };
|