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
package/src/main/index.cjs
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
const { PROC_NAMES, setProcessTitle, inhibitHolderShell } = require('./lib/smProcNames.cjs');
|
|
2
|
+
// Self-describing `comm` in System Monitor (pn-03). process.argv is a JS copy
|
|
3
|
+
// made at startup, so the argv-memory overwrite below never affects the
|
|
4
|
+
// `process.argv.includes('--simple')` read further down.
|
|
5
|
+
setProcessTitle(PROC_NAMES.main);
|
|
1
6
|
const { app, BrowserWindow, ipcMain, dialog, Menu, session, systemPreferences, globalShortcut, shell, clipboard, powerSaveBlocker, protocol } = require('electron');
|
|
2
7
|
const { spawn, execFile, execFileSync } = require('node:child_process');
|
|
3
8
|
const path = require('node:path');
|
|
@@ -39,8 +44,9 @@ const epicWorktreeProjectConfig = require('./lib/epicWorktreeProjectConfig.cjs')
|
|
|
39
44
|
const agentLibrary = require('./agentLibrary.cjs');
|
|
40
45
|
const agentModelResolve = require('./lib/agentModelResolve.cjs');
|
|
41
46
|
const { resolveEffectiveModelInfo } = require('./lib/effectiveModelInfo.cjs');
|
|
42
|
-
const { checkDelegationReadiness, ensureGuardsInstalled, installPrdWriteGuard, installDestructiveGitGuard, installInlineImplementationGuard } = require('./lib/delegationReadiness.cjs');
|
|
47
|
+
const { checkDelegationReadiness, ensureGuardsInstalled, installPrdWriteGuard, installDestructiveGitGuard, installInlineImplementationGuard, installSelfScheduleGuard } = require('./lib/delegationReadiness.cjs');
|
|
43
48
|
const { resolveProjectContext } = require('./lib/projectRootResolve.cjs');
|
|
49
|
+
const upgradeDrain = require('./lib/upgradeDrain.cjs');
|
|
44
50
|
const { writeGuardShims } = require('./lib/guardShims.cjs');
|
|
45
51
|
const { MCP_TOOL_CATALOG, MCP_RECIPES } = require('./lib/mcpToolCatalog.cjs');
|
|
46
52
|
const { sendIfAlive } = require('./lib/sendToRenderer.cjs');
|
|
@@ -58,7 +64,6 @@ chatRunner.registerAdminRoute(adminHttp);
|
|
|
58
64
|
crossProjectFeedback.registerAdminRoute(adminHttp);
|
|
59
65
|
const supervisor = require('./supervisor.cjs');
|
|
60
66
|
const watchers = require('./watchers.cjs');
|
|
61
|
-
const teams = require('./teams.cjs');
|
|
62
67
|
const queueOps = require('./queueOps.cjs');
|
|
63
68
|
const pluginInstall = require('./pluginInstall.cjs');
|
|
64
69
|
const { seedDevPlugin } = require('./seedDevPlugin.cjs');
|
|
@@ -135,7 +140,10 @@ function startSystemdInhibit() {
|
|
|
135
140
|
'--who=Claude Session Manager',
|
|
136
141
|
'--why=Scheduler polling and claude -p jobs must survive idle',
|
|
137
142
|
'--mode=block',
|
|
138
|
-
|
|
143
|
+
// sm-inhibit-hold: aliased shell so the permanent holder is attributable
|
|
144
|
+
// in System Monitor (its `sleep 5` child stays a bare `sleep`, parented
|
|
145
|
+
// under it). Falls back to plain /bin/sh if the alias can't be made.
|
|
146
|
+
inhibitHolderShell(), '-c', `while kill -0 ${process.pid} 2>/dev/null; do sleep 5; done`,
|
|
139
147
|
], { stdio: 'ignore', detached: false });
|
|
140
148
|
child.on('error', (e) => {
|
|
141
149
|
logs.writeLine({ scope: 'main', level: 'warn', message: 'systemd-inhibit spawn failed', meta: { error: e?.message } });
|
|
@@ -308,7 +316,26 @@ function relaunchViaNpx() {
|
|
|
308
316
|
return child;
|
|
309
317
|
}
|
|
310
318
|
|
|
311
|
-
|
|
319
|
+
/**
|
|
320
|
+
* rebootApp({force}) — restart is a REQUEST that drains first and never kills
|
|
321
|
+
* (lib/upgradeDrain.cjs): dispatch pauses, running jobs finish, then the
|
|
322
|
+
* scheduler's heartbeat-driven drain calls performReboot(). `force: true`
|
|
323
|
+
* keeps the old immediate behaviour (kills running executors) for humans who
|
|
324
|
+
* accept the cost.
|
|
325
|
+
*/
|
|
326
|
+
async function rebootApp({ force = false } = {}) {
|
|
327
|
+
if (force) {
|
|
328
|
+
logReboot('force reboot requested — immediate, not draining');
|
|
329
|
+
return performReboot();
|
|
330
|
+
}
|
|
331
|
+
const request = upgradeDrain.requestRestart({ reason: 'user-reboot', requestedBy: 'user' });
|
|
332
|
+
logReboot(`restart requested (drain first): ${request.requestedAt}`);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Registered as the scheduler's drain-complete exit path (zero running jobs).
|
|
336
|
+
scheduler.setRestartHandler(() => performReboot());
|
|
337
|
+
|
|
338
|
+
async function performReboot() {
|
|
312
339
|
ptyManager.killAll();
|
|
313
340
|
configMgr.closeAllWatchers();
|
|
314
341
|
closeAllProjectPagesWatchers();
|
|
@@ -343,6 +370,9 @@ async function rebootApp() {
|
|
|
343
370
|
return;
|
|
344
371
|
}
|
|
345
372
|
|
|
373
|
+
// Bounded-lifetime marker: the watchdog must not relaunch a second instance
|
|
374
|
+
// during this deliberate exit window (single-instance-lock race).
|
|
375
|
+
upgradeDrain.markRestarting();
|
|
346
376
|
let childOk = false;
|
|
347
377
|
try {
|
|
348
378
|
const child = relaunchViaNpx();
|
|
@@ -588,12 +618,22 @@ function logGuardInstallAttempt(guard, cwd, outcome) {
|
|
|
588
618
|
}
|
|
589
619
|
|
|
590
620
|
async function handleGuardInstall(guard, installFn, payload) {
|
|
621
|
+
// Normalized the SAME way the sibling app:delegation-readiness handler
|
|
622
|
+
// above normalizes cwd, before either installing or logging — without
|
|
623
|
+
// this, a worktree/ops-internal cwd installs the guard into an ephemeral
|
|
624
|
+
// `.claude/settings.json` that vanishes with the worktree (this repo's
|
|
625
|
+
// "Worktree cwd ops-root hazard" incident class), diverging from the
|
|
626
|
+
// main-tree root the readiness banner (and its own auto-heal path,
|
|
627
|
+
// ensureGuardsInstalled) actually reads.
|
|
628
|
+
const resolved = resolveProjectContext({ cwd: payload.cwd });
|
|
629
|
+
const cwd = resolved.cwd || payload.cwd;
|
|
630
|
+
const normalizedPayload = { ...payload, cwd };
|
|
591
631
|
try {
|
|
592
|
-
const result = await installFn(
|
|
593
|
-
logGuardInstallAttempt(guard,
|
|
632
|
+
const result = await installFn(normalizedPayload);
|
|
633
|
+
logGuardInstallAttempt(guard, cwd, result);
|
|
594
634
|
return result;
|
|
595
635
|
} catch (err) {
|
|
596
|
-
logGuardInstallAttempt(guard,
|
|
636
|
+
logGuardInstallAttempt(guard, cwd, { ok: false, action: 'error', error: err?.message ?? String(err) });
|
|
597
637
|
throw err;
|
|
598
638
|
}
|
|
599
639
|
}
|
|
@@ -615,6 +655,9 @@ ipcMain.handle('app:install-destructive-git-guard', validated(schemas.delegation
|
|
|
615
655
|
ipcMain.handle('app:install-inline-implementation-guard', validated(schemas.delegationReadinessCwd, (payload) =>
|
|
616
656
|
handleGuardInstall('inline-implementation-guard', installInlineImplementationGuard, payload)
|
|
617
657
|
));
|
|
658
|
+
ipcMain.handle('app:install-self-schedule-guard', validated(schemas.delegationReadinessCwd, (payload) =>
|
|
659
|
+
handleGuardInstall('self-schedule-guard', installSelfScheduleGuard, payload)
|
|
660
|
+
));
|
|
618
661
|
|
|
619
662
|
ipcMain.handle('app:engage-rules-path', () => process.env.SESSION_MANAGER_ENGAGE_RULES || null);
|
|
620
663
|
|
|
@@ -641,7 +684,7 @@ ipcMain.handle('app:pick-directory', async () => {
|
|
|
641
684
|
return result.filePaths[0];
|
|
642
685
|
});
|
|
643
686
|
|
|
644
|
-
ipcMain.on('app:reboot-app', () => rebootApp());
|
|
687
|
+
ipcMain.on('app:reboot-app', (_e, opts) => { rebootApp({ force: opts?.force === true }).catch((e) => logReboot(`reboot failed: ${e?.message}`)); });
|
|
645
688
|
|
|
646
689
|
// Image paste — Ctrl+V in the Terminal pane. Reads the OS clipboard via
|
|
647
690
|
// Electron's native API (renderer's navigator.clipboard.read() doesn't expose
|
|
@@ -916,7 +959,6 @@ voiceHotkey.registerHotkeyHandlers();
|
|
|
916
959
|
voiceWizard.registerWizardHandlers();
|
|
917
960
|
scheduler.registerScheduleHandlers();
|
|
918
961
|
watchers.registerWatcherHandlers();
|
|
919
|
-
teams.registerTeamsHandlers();
|
|
920
962
|
queueOps.registerQueueOpsHandlers({ retireCompletedSlugs: scheduler.retireCompletedSlugs });
|
|
921
963
|
registerHistoryAggregatorHandlers();
|
|
922
964
|
registerHistoryDashboardHandlers();
|
|
@@ -1235,6 +1277,7 @@ app.whenReady().then(async () => {
|
|
|
1235
1277
|
|
|
1236
1278
|
const heapSnapshotMenuItem = heapSnapshot.buildMenuItem(() => mainWindow);
|
|
1237
1279
|
|
|
1280
|
+
// Application menu rules + rationale: session-manager-operations/architecture/application-menu.md
|
|
1238
1281
|
const template = [
|
|
1239
1282
|
{
|
|
1240
1283
|
label: 'Session Manager',
|
|
@@ -1251,7 +1294,7 @@ app.whenReady().then(async () => {
|
|
|
1251
1294
|
{
|
|
1252
1295
|
label: 'Restart Session Manager App',
|
|
1253
1296
|
accelerator: 'CmdOrCtrl+Shift+R',
|
|
1254
|
-
click: rebootApp,
|
|
1297
|
+
click: () => { rebootApp().catch((e) => logReboot(`reboot failed: ${e?.message}`)); },
|
|
1255
1298
|
},
|
|
1256
1299
|
{
|
|
1257
1300
|
label: 'Restart Terminal in Active Tab',
|
|
@@ -1443,6 +1486,14 @@ function runShutdownCleanup() {
|
|
|
1443
1486
|
// Mark a clean exit so the next boot can distinguish a graceful quit from an
|
|
1444
1487
|
// OOM-kill / native crash (which leaves the sentinel `open`).
|
|
1445
1488
|
crashDiagnostics.markCleanShutdown();
|
|
1489
|
+
// Release scheduler ownership on every teardown path, not just before-quit
|
|
1490
|
+
// — the app.exit() sites (in-app reboot, dist-missing abort) bypass
|
|
1491
|
+
// before-quit entirely and previously left a stale lock behind (fail-closed
|
|
1492
|
+
// proc-identity recovery covers a hard crash, but a clean app.exit() should
|
|
1493
|
+
// never need it). Pid-scoped and idempotent, so a redundant call from a
|
|
1494
|
+
// later before-quit/will-quit firing is a safe no-op.
|
|
1495
|
+
releaseSchedulerOwnership();
|
|
1496
|
+
try { scheduler.stop(); } catch { /* */ }
|
|
1446
1497
|
// PRD F1 v2 §IPC plumbing: must unregisterAll on will-quit.
|
|
1447
1498
|
try { globalShortcut.unregisterAll(); } catch { /* */ }
|
|
1448
1499
|
voiceHotkey.disposeOnQuit();
|
package/src/main/ipcSchemas.cjs
CHANGED
|
@@ -674,21 +674,12 @@ const projectPagesCwd = z.object({
|
|
|
674
674
|
cwd: z.string().min(1).max(4096),
|
|
675
675
|
}).strict();
|
|
676
676
|
|
|
677
|
-
// ──────────────────────────────────────────── Project Home admin
|
|
678
|
-
//
|
|
679
|
-
//
|
|
680
|
-
|
|
681
|
-
const projectHomeAdminCwdQuery = z.object({
|
|
677
|
+
// ──────────────────────────────────────────── Project Home admin route
|
|
678
|
+
// Real path validation is projectRootResolve.cjs + config.cjs's validatePath;
|
|
679
|
+
// html content limits live in projectHomeAdminRoutes.cjs's validateHomeHtml.
|
|
680
|
+
const projectHomeAdminWriteBody = z.object({
|
|
682
681
|
cwd: z.string().min(1).max(4096),
|
|
683
|
-
|
|
684
|
-
const projectHomeAdminValidateSummaryBody = z.object({
|
|
685
|
-
cwd: z.string().min(1).max(4096),
|
|
686
|
-
summary: z.unknown(),
|
|
687
|
-
}).strict();
|
|
688
|
-
const projectHomeAdminRenderBody = z.object({
|
|
689
|
-
cwd: z.string().min(1).max(4096),
|
|
690
|
-
summary: z.unknown(),
|
|
691
|
-
picks: z.unknown(),
|
|
682
|
+
html: z.string(),
|
|
692
683
|
}).strict();
|
|
693
684
|
|
|
694
685
|
// ──────────────────────────────────────────── Host on Bilko.run
|
|
@@ -1115,9 +1106,7 @@ module.exports = {
|
|
|
1115
1106
|
memoryStale,
|
|
1116
1107
|
projectBriefCwd,
|
|
1117
1108
|
projectPagesCwd,
|
|
1118
|
-
|
|
1119
|
-
projectHomeAdminValidateSummaryBody,
|
|
1120
|
-
projectHomeAdminRenderBody,
|
|
1109
|
+
projectHomeAdminWriteBody,
|
|
1121
1110
|
bilkoHostCwd,
|
|
1122
1111
|
bilkoHostPrepareBundle,
|
|
1123
1112
|
bilkoHostAddDocument,
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* auditLog.test.cjs — readTail returns a bounded reverse tail of complete lines.
|
|
3
|
+
*
|
|
4
|
+
* Run: timeout 120 npx vitest run src/main/lib/__tests__/auditLog.test.cjs
|
|
5
|
+
*/
|
|
6
|
+
'use strict';
|
|
7
|
+
|
|
8
|
+
import { test, expect, beforeAll, afterAll } from 'vitest';
|
|
9
|
+
const fs = require('node:fs');
|
|
10
|
+
const os = require('node:os');
|
|
11
|
+
const path = require('node:path');
|
|
12
|
+
const { readTail } = require('../auditLog.cjs');
|
|
13
|
+
|
|
14
|
+
let dir;
|
|
15
|
+
beforeAll(() => { dir = fs.mkdtempSync(path.join(os.tmpdir(), 'auditlog-tail-')); });
|
|
16
|
+
afterAll(() => { fs.rmSync(dir, { recursive: true, force: true }); });
|
|
17
|
+
|
|
18
|
+
test('missing file → []', () => {
|
|
19
|
+
expect(readTail(1024, path.join(dir, 'nope.jsonl'))).toEqual([]);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test('window larger than file → every line', () => {
|
|
23
|
+
const f = path.join(dir, 'small.jsonl');
|
|
24
|
+
fs.writeFileSync(f, '{"n":1}\n{"n":2}\n');
|
|
25
|
+
expect(readTail(1_000_000, f)).toEqual(['{"n":1}', '{"n":2}']);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('bounded window drops the partial leading line and keeps the newest', () => {
|
|
29
|
+
const f = path.join(dir, 'big.jsonl');
|
|
30
|
+
const lines = Array.from({ length: 1000 }, (_, i) => JSON.stringify({ n: i, pad: 'x'.repeat(50) }));
|
|
31
|
+
fs.writeFileSync(f, lines.join('\n') + '\n');
|
|
32
|
+
const tail = readTail(500, f);
|
|
33
|
+
expect(tail.length).toBeGreaterThan(0);
|
|
34
|
+
expect(tail.length).toBeLessThan(20);
|
|
35
|
+
expect(tail[tail.length - 1]).toBe(lines[999]);
|
|
36
|
+
for (const l of tail) expect(() => JSON.parse(l)).not.toThrow();
|
|
37
|
+
expect(readTail(0, f)).toEqual([]);
|
|
38
|
+
});
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* buildIdentity.test.cjs — resolveBuildIdentity() resolution order:
|
|
3
|
+
* build-info.json first, git fallback only when a real .git sits AT the
|
|
4
|
+
* package root (never walking up), else null.
|
|
5
|
+
*
|
|
6
|
+
* Each test injects a distinct fixture `packageRoot` (see buildIdentity.cjs's
|
|
7
|
+
* `opts.packageRoot`), which also keeps memoization scoped per-root so the
|
|
8
|
+
* three cases don't clobber each other's cached result in one process.
|
|
9
|
+
*
|
|
10
|
+
* Run: timeout 120 npx vitest run src/main/lib/__tests__/buildIdentity.test.cjs
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
'use strict';
|
|
14
|
+
|
|
15
|
+
import { test, expect, afterEach } from 'vitest';
|
|
16
|
+
const fs = require('node:fs');
|
|
17
|
+
const os = require('node:os');
|
|
18
|
+
const path = require('node:path');
|
|
19
|
+
const { execFileSync } = require('node:child_process');
|
|
20
|
+
const { resolveBuildIdentity } = require('../buildIdentity.cjs');
|
|
21
|
+
|
|
22
|
+
const cleanupDirs = [];
|
|
23
|
+
|
|
24
|
+
function mkFixtureRoot(prefix) {
|
|
25
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
|
|
26
|
+
cleanupDirs.push(dir);
|
|
27
|
+
fs.mkdirSync(path.join(dir, 'src', 'main'), { recursive: true });
|
|
28
|
+
return dir;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function writePackageJson(root, version) {
|
|
32
|
+
fs.writeFileSync(path.join(root, 'package.json'), JSON.stringify({ name: 'fixture-pkg', version }));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function mkRealGitRepo(root) {
|
|
36
|
+
execFileSync('git', ['-C', root, 'init', '-q']);
|
|
37
|
+
execFileSync('git', ['-C', root, 'config', 'user.email', 'test@test.com']);
|
|
38
|
+
execFileSync('git', ['-C', root, 'config', 'user.name', 'Test']);
|
|
39
|
+
fs.writeFileSync(path.join(root, 'README.md'), 'fixture\n');
|
|
40
|
+
execFileSync('git', ['-C', root, 'add', 'README.md']);
|
|
41
|
+
execFileSync('git', ['-C', root, 'commit', '-q', '-m', 'init']);
|
|
42
|
+
return execFileSync('git', ['-C', root, 'rev-parse', '--short', 'HEAD'], { encoding: 'utf8' }).trim();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
afterEach(() => {
|
|
46
|
+
while (cleanupDirs.length) {
|
|
47
|
+
const dir = cleanupDirs.pop();
|
|
48
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test('build-info.json present: its fields win over both git and package.json', () => {
|
|
53
|
+
const root = mkFixtureRoot('sm-buildid-present-');
|
|
54
|
+
writePackageJson(root, '0.1.0');
|
|
55
|
+
fs.writeFileSync(path.join(root, 'src', 'main', 'build-info.json'), JSON.stringify({
|
|
56
|
+
version: '9.9.9',
|
|
57
|
+
gitSha: 'deadbeefdeadbeefdeadbeefdeadbeefdeadbeef',
|
|
58
|
+
gitShortSha: 'deadbee',
|
|
59
|
+
gitBranch: 'main',
|
|
60
|
+
builtAt: '2026-01-01T00:00:00.000Z',
|
|
61
|
+
dirty: false,
|
|
62
|
+
}));
|
|
63
|
+
|
|
64
|
+
const identity = resolveBuildIdentity({ packageRoot: root, bootedAt: '2026-02-02T00:00:00.000Z' });
|
|
65
|
+
|
|
66
|
+
expect(identity.version).toBe('9.9.9');
|
|
67
|
+
expect(identity.codeSha).toBe('deadbee');
|
|
68
|
+
expect(identity.builtAt).toBe('2026-01-01T00:00:00.000Z');
|
|
69
|
+
expect(identity.appPath).toBe(root);
|
|
70
|
+
expect(identity.bootedAt).toBe('2026-02-02T00:00:00.000Z');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test('build-info.json absent, real .git at the package root: codeSha falls back to git, builtAt stays null', () => {
|
|
74
|
+
const root = mkFixtureRoot('sm-buildid-git-');
|
|
75
|
+
writePackageJson(root, '1.2.3');
|
|
76
|
+
const sha = mkRealGitRepo(root);
|
|
77
|
+
|
|
78
|
+
const identity = resolveBuildIdentity({ packageRoot: root });
|
|
79
|
+
|
|
80
|
+
expect(identity.version).toBe('1.2.3');
|
|
81
|
+
expect(identity.codeSha).toBe(sha);
|
|
82
|
+
expect(identity.builtAt).toBeNull();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('build-info.json absent, no .git at the package root: codeSha is null, never walks up to an ancestor .git', () => {
|
|
86
|
+
const ancestor = mkFixtureRoot('sm-buildid-ancestor-');
|
|
87
|
+
mkRealGitRepo(ancestor); // a real repo one level UP from the fixture root — must never be found
|
|
88
|
+
const root = path.join(ancestor, 'nested-pkg');
|
|
89
|
+
fs.mkdirSync(path.join(root, 'src', 'main'), { recursive: true });
|
|
90
|
+
writePackageJson(root, '2.0.0');
|
|
91
|
+
|
|
92
|
+
const identity = resolveBuildIdentity({ packageRoot: root });
|
|
93
|
+
|
|
94
|
+
expect(identity.version).toBe('2.0.0');
|
|
95
|
+
expect(identity.codeSha).toBeNull();
|
|
96
|
+
expect(identity.builtAt).toBeNull();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test('bootedAt is echoed back verbatim, never re-captured, and defaults to null', () => {
|
|
100
|
+
const root = mkFixtureRoot('sm-buildid-bootedat-');
|
|
101
|
+
writePackageJson(root, '1.0.0');
|
|
102
|
+
|
|
103
|
+
expect(resolveBuildIdentity({ packageRoot: root }).bootedAt).toBeNull();
|
|
104
|
+
expect(resolveBuildIdentity({ packageRoot: root, bootedAt: 'fixed-value' }).bootedAt).toBe('fixed-value');
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test('installChannel is reused from machineProfile.resolveInstallChannel, keyed off appPath', () => {
|
|
108
|
+
const root = mkFixtureRoot('sm-buildid-channel-');
|
|
109
|
+
writePackageJson(root, '1.0.0');
|
|
110
|
+
const prevSmDev = process.env.SM_DEV;
|
|
111
|
+
delete process.env.SM_DEV;
|
|
112
|
+
try {
|
|
113
|
+
const identity = resolveBuildIdentity({ packageRoot: root });
|
|
114
|
+
// fixture root lives under os.tmpdir(), not an .npm/_npx cache path, and
|
|
115
|
+
// SM_DEV is unset here, so resolveInstallChannel falls through to 'unknown'.
|
|
116
|
+
expect(identity.installChannel).toBe('unknown');
|
|
117
|
+
} finally {
|
|
118
|
+
if (prevSmDev === undefined) delete process.env.SM_DEV;
|
|
119
|
+
else process.env.SM_DEV = prevSmDev;
|
|
120
|
+
}
|
|
121
|
+
});
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// cwdClassify.cjs — one classifier for "what project is this path", failing
|
|
2
|
+
// CLOSED on the unprovable worktree case. Fixtures live under a per-run
|
|
3
|
+
// mkdtemp sandbox, never the live roots.
|
|
4
|
+
const assert = require('node:assert/strict');
|
|
5
|
+
const fs = require('node:fs');
|
|
6
|
+
const os = require('node:os');
|
|
7
|
+
const path = require('node:path');
|
|
8
|
+
const { classifyCwd, nearestGitEntry, worktreeMainRootOf } = require('../cwdClassify.cjs');
|
|
9
|
+
const { assertOpsWrite, resolveOpsRoot } = require('../opsOwnership.cjs');
|
|
10
|
+
const { isEphemeralCwd } = require('../ephemeralCwd.cjs');
|
|
11
|
+
const { projectRootOf } = require('../activeSessions.cjs');
|
|
12
|
+
|
|
13
|
+
const OPS = 'session-manager-operations';
|
|
14
|
+
let sandbox;
|
|
15
|
+
const real = (p) => fs.realpathSync(p);
|
|
16
|
+
|
|
17
|
+
beforeEach(() => { sandbox = real(fs.mkdtempSync(path.join(os.tmpdir(), 'sm-cwdclassify-'))); });
|
|
18
|
+
afterEach(() => { fs.rmSync(sandbox, { recursive: true, force: true }); });
|
|
19
|
+
|
|
20
|
+
function mkWorktree({ backRef = true } = {}) {
|
|
21
|
+
const main = path.join(sandbox, 'main');
|
|
22
|
+
const wt = path.join(sandbox, 'wt');
|
|
23
|
+
const admin = path.join(main, '.git', 'worktrees', 'wt1');
|
|
24
|
+
fs.mkdirSync(admin, { recursive: true });
|
|
25
|
+
fs.mkdirSync(wt, { recursive: true });
|
|
26
|
+
fs.writeFileSync(path.join(wt, '.git'), `gitdir: ${admin}\n`);
|
|
27
|
+
if (backRef) fs.writeFileSync(path.join(admin, 'gitdir'), `${path.join(wt, '.git')}\n`);
|
|
28
|
+
return { main, wt };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
test('healthy worktree → worktree, projectRoot is the main tree', () => {
|
|
32
|
+
const { main, wt } = mkWorktree();
|
|
33
|
+
const c = classifyCwd(wt);
|
|
34
|
+
assert.equal(c.kind, 'worktree');
|
|
35
|
+
assert.equal(c.projectRoot, main);
|
|
36
|
+
assert.equal(projectRootOf(wt), main);
|
|
37
|
+
assert.equal(isEphemeralCwd(wt), true);
|
|
38
|
+
assert.equal(worktreeMainRootOf(wt), main);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('broken back-reference → unknown; ops writes refused, resolution and dispatch stay open', () => {
|
|
42
|
+
const { wt } = mkWorktree({ backRef: false });
|
|
43
|
+
const c = classifyCwd(wt);
|
|
44
|
+
assert.equal(c.kind, 'unknown');
|
|
45
|
+
assert.equal(c.projectRoot, wt);
|
|
46
|
+
assert.match(c.reason, /back-reference/);
|
|
47
|
+
assert.equal(projectRootOf(wt), wt); // total — never throws
|
|
48
|
+
assert.equal(resolveOpsRoot(wt), path.join(wt, OPS)); // reads/dispatch unaffected
|
|
49
|
+
assert.throws(
|
|
50
|
+
() => assertOpsWrite(path.join(wt, OPS, 'scheduler', 'state', 'queue.json'), 'scheduler'),
|
|
51
|
+
(e) => e.unknownCwd === true && /back-reference/.test(e.message),
|
|
52
|
+
);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('garbled .git file → unknown', () => {
|
|
56
|
+
const dir = path.join(sandbox, 'garbled');
|
|
57
|
+
fs.mkdirSync(dir);
|
|
58
|
+
fs.writeFileSync(path.join(dir, '.git'), 'not a pointer\n');
|
|
59
|
+
assert.equal(classifyCwd(dir).kind, 'unknown');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('separate-git-dir → project (gitdir not under worktrees/)', () => {
|
|
63
|
+
const dir = path.join(sandbox, 'sep');
|
|
64
|
+
fs.mkdirSync(dir);
|
|
65
|
+
fs.writeFileSync(path.join(dir, '.git'), `gitdir: ${path.join(sandbox, 'elsewhere.git')}\n`);
|
|
66
|
+
const c = classifyCwd(dir);
|
|
67
|
+
assert.equal(c.kind, 'project');
|
|
68
|
+
assert.equal(c.projectRoot, dir);
|
|
69
|
+
assert.doesNotThrow(() => assertOpsWrite(path.join(dir, OPS, 'scheduler', 'state', 'queue.json'), 'scheduler'));
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('submodule (relative gitdir) → project rooted at the submodule', () => {
|
|
73
|
+
const sup = path.join(sandbox, 'super');
|
|
74
|
+
const sub = path.join(sup, 'libs', 'sub');
|
|
75
|
+
fs.mkdirSync(path.join(sup, '.git', 'modules', 'sub'), { recursive: true });
|
|
76
|
+
fs.mkdirSync(path.join(sub, 'src'), { recursive: true });
|
|
77
|
+
fs.writeFileSync(path.join(sub, '.git'), 'gitdir: ../../.git/modules/sub\n');
|
|
78
|
+
const c = classifyCwd(path.join(sub, 'src'));
|
|
79
|
+
assert.equal(c.kind, 'project');
|
|
80
|
+
assert.equal(c.projectRoot, sub);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('plain project (.git dir, and no git at all) → project', () => {
|
|
84
|
+
const proj = path.join(sandbox, 'proj');
|
|
85
|
+
fs.mkdirSync(path.join(proj, '.git'), { recursive: true });
|
|
86
|
+
fs.mkdirSync(path.join(proj, 'src'));
|
|
87
|
+
assert.deepEqual(
|
|
88
|
+
{ k: classifyCwd(path.join(proj, 'src')).kind, r: classifyCwd(path.join(proj, 'src')).projectRoot },
|
|
89
|
+
{ k: 'project', r: proj },
|
|
90
|
+
);
|
|
91
|
+
const bare = path.join(sandbox, 'nogit');
|
|
92
|
+
fs.mkdirSync(bare);
|
|
93
|
+
assert.equal(classifyCwd(bare).projectRoot, bare);
|
|
94
|
+
assert.equal(nearestGitEntry(bare), null);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test('nested ops root: outermost (indexOf) and innermost (lastIndexOf) are both reported, neither unified', () => {
|
|
98
|
+
const proj = path.join(sandbox, 'proj');
|
|
99
|
+
fs.mkdirSync(path.join(proj, '.git'), { recursive: true });
|
|
100
|
+
const nested = path.join(proj, OPS, 'scheduler', 'prds', OPS, 'scheduler', 'state');
|
|
101
|
+
const c = classifyCwd(nested);
|
|
102
|
+
assert.equal(c.projectRoot, proj);
|
|
103
|
+
assert.equal(c.outermostOpsRoot, path.join(proj, OPS));
|
|
104
|
+
assert.equal(c.innermostOpsRoot, path.join(proj, OPS, 'scheduler', 'prds', OPS));
|
|
105
|
+
assert.equal(classifyCwd(proj).outermostOpsRoot, null);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test('ephemeral: os.tmpdir() itself', () => {
|
|
109
|
+
assert.equal(classifyCwd(path.resolve(os.tmpdir())).kind, 'ephemeral');
|
|
110
|
+
assert.equal(isEphemeralCwd(os.tmpdir()), true);
|
|
111
|
+
});
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* definitionOfDoneSequence.test.cjs — extractAcSequence / resolveGate parsing
|
|
3
|
+
* matrix plus real gate lines lifted from archived PRDs (gateFixtures.json).
|
|
4
|
+
*
|
|
5
|
+
* Run: timeout 120 npx vitest run src/main/lib/__tests__/definitionOfDoneSequence.test.cjs
|
|
6
|
+
*/
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
const assert = require('node:assert/strict');
|
|
10
|
+
const { extractAcSequence, extractAcCommand, resolveGate } = require('../definitionOfDone.cjs');
|
|
11
|
+
const fixtures = require('./gateFixtures.json');
|
|
12
|
+
|
|
13
|
+
const ac = (...lines) => ['# Acceptance criteria', '', ...lines, '', '# Out of scope', ''].join('\n');
|
|
14
|
+
const argvs = (seq) => seq.map((c) => c.argv);
|
|
15
|
+
|
|
16
|
+
test('splits a && chain, honouring a leading timeout N per command', () => {
|
|
17
|
+
const seq = extractAcSequence(ac('- [ ] Gate: `timeout 300 npm run typecheck && timeout 60 npm run lint`'));
|
|
18
|
+
assert.deepEqual(argvs(seq), [['npm', 'run', 'typecheck'], ['npm', 'run', 'lint']]);
|
|
19
|
+
assert.deepEqual(seq.map((c) => c.timeoutMs), [300_000, 60_000]);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test('a command with no timeout token gets the default timeout', () => {
|
|
23
|
+
const seq = extractAcSequence(ac('- [ ] `timeout 5 npm run a && npm run b`'));
|
|
24
|
+
assert.equal(seq[0].timeoutMs, 5_000);
|
|
25
|
+
assert.equal(seq[1].timeoutMs, 300_000);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('&& inside quotes is never a split point', () => {
|
|
29
|
+
const seq = extractAcSequence(ac('- [ ] `timeout 30 node -e "a && b" && timeout 30 echo \'x && y\'`'));
|
|
30
|
+
assert.deepEqual(argvs(seq), [['node', '-e', 'a && b'], ['echo', 'x && y']]);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('a leading TMPDIR=$(mktemp -d) is dropped, other literal env is kept', () => {
|
|
34
|
+
const seq = extractAcSequence(ac('- [ ] `TMPDIR=$(mktemp -d) timeout 900 npm run test:unit && FOO=bar timeout 10 npm run x`'));
|
|
35
|
+
assert.deepEqual(argvs(seq), [['npm', 'run', 'test:unit'], ['npm', 'run', 'x']]);
|
|
36
|
+
assert.deepEqual(seq[0].env, {});
|
|
37
|
+
assert.deepEqual(seq[1].env, { FOO: 'bar' });
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('any surviving shell metacharacter makes the whole gate unparseable (no shell)', () => {
|
|
41
|
+
for (const bad of [
|
|
42
|
+
'`timeout 60 npm run a | tail -5`', '`timeout 60 npm run a > out.txt`', '`timeout 60 npm run a; rm x`',
|
|
43
|
+
'`timeout 60 npm run a & echo`', '`timeout 60 echo $(whoami)`', '`timeout 60 npm run a && timeout 60 npm run b || true`',
|
|
44
|
+
]) {
|
|
45
|
+
assert.deepEqual(extractAcSequence(ac(`- [ ] ${bad}`)), [], bad);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('an empty segment (dangling &&) is unparseable', () => {
|
|
50
|
+
assert.deepEqual(extractAcSequence(ac('- [ ] `timeout 60 npm run a &&`')), []);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('frontmatter gate: list takes priority over the AC line', () => {
|
|
54
|
+
const text = '---\ntitle: t\ngate:\n - timeout 10 npm run one\n - "timeout 20 npm run two && timeout 30 npm run three"\n---\n' +
|
|
55
|
+
ac('- [ ] `timeout 999 npm run ignored`');
|
|
56
|
+
const g = resolveGate(text);
|
|
57
|
+
assert.equal(g.source, 'explicit');
|
|
58
|
+
assert.deepEqual(argvs(g.sequence), [['npm', 'run', 'one'], ['npm', 'run', 'two'], ['npm', 'run', 'three']]);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('frontmatter inline gate list', () => {
|
|
62
|
+
const g = resolveGate('---\ngate: [timeout 10 npm run one, timeout 20 npm run two]\n---\nbody');
|
|
63
|
+
assert.deepEqual(argvs(g.sequence), [['npm', 'run', 'one'], ['npm', 'run', 'two']]);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('a fenced ```gate block takes priority', () => {
|
|
67
|
+
const text = ac('- [ ] `timeout 999 npm run ignored`', '', '```gate', 'timeout 10 npm run a && timeout 20 npm run b', '```');
|
|
68
|
+
const g = resolveGate(text);
|
|
69
|
+
assert.equal(g.source, 'explicit');
|
|
70
|
+
assert.deepEqual(argvs(g.sequence), [['npm', 'run', 'a'], ['npm', 'run', 'b']]);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test('gate: none is an explicit opt-out, distinct from absent', () => {
|
|
74
|
+
const none = resolveGate('---\ngate: none\n---\n' + ac('- [ ] `timeout 60 npm test`'));
|
|
75
|
+
assert.equal(none.source, 'none');
|
|
76
|
+
assert.deepEqual(none.sequence, []);
|
|
77
|
+
assert.equal(resolveGate(ac('- [ ] prose only')).source, 'absent');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('extractAcCommand delegates: single command → original string, chain → null', () => {
|
|
81
|
+
assert.equal(extractAcCommand(ac('- [ ] `timeout 60 npm test`')), 'timeout 60 npm test');
|
|
82
|
+
assert.equal(extractAcCommand(ac('- [ ] `timeout 60 npm a && timeout 60 npm b`')), null);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('real archived gate lines: count how many now parse', () => {
|
|
86
|
+
const parsed = fixtures.filter((line) => extractAcSequence(ac(line)).length > 0);
|
|
87
|
+
console.log(`[gate-fixtures] ${parsed.length}/${fixtures.length} real gate lines now parse`);
|
|
88
|
+
const chains = fixtures.filter((l) => l.includes('&&') && /^- \[ \] (Gate|Locally)/.test(l));
|
|
89
|
+
for (const line of chains) assert.ok(extractAcSequence(ac(line)).length >= 2, line);
|
|
90
|
+
// The three deliberately un-runnable fixtures (shell loop, pipe, prose) stay empty.
|
|
91
|
+
const unparseable = fixtures.filter((l) => /for i in|\| tail|Prose only/.test(l));
|
|
92
|
+
assert.equal(unparseable.length, 3);
|
|
93
|
+
for (const line of unparseable) assert.deepEqual(extractAcSequence(ac(line)), [], line);
|
|
94
|
+
assert.equal(parsed.length, fixtures.length - 3);
|
|
95
|
+
});
|
|
@@ -694,7 +694,7 @@ test('the installed destructive-git command actually DENIES a shared-tree stash
|
|
|
694
694
|
const denied = run('git stash', cwd);
|
|
695
695
|
expect(denied.hookSpecificOutput?.permissionDecision).toBe('deny');
|
|
696
696
|
|
|
697
|
-
const worktreeCwd = path.join(os2.tmpdir(), 'session-manager-job-worktrees', 'somehash', 'some-slug');
|
|
697
|
+
const worktreeCwd = path.join(process.env.SM_WORKTREE_ROOT || os2.tmpdir(), 'session-manager-job-worktrees', 'somehash', 'some-slug');
|
|
698
698
|
const allowed = run('git stash', worktreeCwd);
|
|
699
699
|
expect(allowed.hookSpecificOutput?.permissionDecision).not.toBe('deny');
|
|
700
700
|
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dispatchLoop.test.cjs — interval clamping, no overlap, throw isolation, stop(), kill switch.
|
|
3
|
+
* Run: timeout 120 npx vitest run src/main/lib/__tests__/dispatchLoop.test.cjs
|
|
4
|
+
*/
|
|
5
|
+
'use strict';
|
|
6
|
+
|
|
7
|
+
import { test, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
8
|
+
const { startDispatchLoop, resolveIntervalMs } = require('../dispatchLoop.cjs');
|
|
9
|
+
|
|
10
|
+
beforeEach(() => { vi.useFakeTimers(); delete process.env.SM_DISPATCH_LOOP_DISABLE; delete process.env.SM_DISPATCH_LOOP_INTERVAL_MS; });
|
|
11
|
+
afterEach(() => { vi.useRealTimers(); delete process.env.SM_DISPATCH_LOOP_DISABLE; delete process.env.SM_DISPATCH_LOOP_INTERVAL_MS; });
|
|
12
|
+
|
|
13
|
+
test('interval defaults to 30s and clamps to [10s, 300s]', () => {
|
|
14
|
+
expect(resolveIntervalMs(undefined, {})).toBe(30_000);
|
|
15
|
+
expect(resolveIntervalMs(undefined, { SM_DISPATCH_LOOP_INTERVAL_MS: '1' })).toBe(10_000);
|
|
16
|
+
expect(resolveIntervalMs(undefined, { SM_DISPATCH_LOOP_INTERVAL_MS: '9999999' })).toBe(300_000);
|
|
17
|
+
expect(resolveIntervalMs(undefined, { SM_DISPATCH_LOOP_INTERVAL_MS: 'abc' })).toBe(30_000);
|
|
18
|
+
expect(resolveIntervalMs(45_000, {})).toBe(45_000);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test('ticks on the interval and stop() clears the timer', async () => {
|
|
22
|
+
const tick = vi.fn(async () => {});
|
|
23
|
+
const h = startDispatchLoop({ tick });
|
|
24
|
+
await vi.advanceTimersByTimeAsync(90_000);
|
|
25
|
+
expect(tick).toHaveBeenCalledTimes(3);
|
|
26
|
+
h.stop();
|
|
27
|
+
expect(vi.getTimerCount()).toBe(0);
|
|
28
|
+
await vi.advanceTimersByTimeAsync(90_000);
|
|
29
|
+
expect(tick).toHaveBeenCalledTimes(3);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('skips a pass while the previous one is still running', async () => {
|
|
33
|
+
let release;
|
|
34
|
+
const tick = vi.fn(() => new Promise((r) => { release = r; }));
|
|
35
|
+
const h = startDispatchLoop({ tick, intervalMs: 10_000 });
|
|
36
|
+
await vi.advanceTimersByTimeAsync(50_000);
|
|
37
|
+
expect(tick).toHaveBeenCalledTimes(1);
|
|
38
|
+
release();
|
|
39
|
+
await vi.advanceTimersByTimeAsync(10_000);
|
|
40
|
+
expect(tick).toHaveBeenCalledTimes(2);
|
|
41
|
+
h.stop();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('a throwing/rejecting tick is isolated, reported, and the loop keeps running', async () => {
|
|
45
|
+
const onError = vi.fn();
|
|
46
|
+
let n = 0;
|
|
47
|
+
const tick = vi.fn(async () => { n++; if (n === 1) throw new Error('boom'); if (n === 2) throw new Error('boom2'); });
|
|
48
|
+
const h = startDispatchLoop({ tick, intervalMs: 10_000, onError });
|
|
49
|
+
await vi.advanceTimersByTimeAsync(40_000);
|
|
50
|
+
expect(tick).toHaveBeenCalledTimes(4);
|
|
51
|
+
expect(onError).toHaveBeenCalledTimes(2);
|
|
52
|
+
h.stop();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('SM_DISPATCH_LOOP_DISABLE=1 never arms the loop', async () => {
|
|
56
|
+
process.env.SM_DISPATCH_LOOP_DISABLE = '1';
|
|
57
|
+
const tick = vi.fn();
|
|
58
|
+
const h = startDispatchLoop({ tick });
|
|
59
|
+
expect(vi.getTimerCount()).toBe(0);
|
|
60
|
+
await vi.advanceTimersByTimeAsync(600_000);
|
|
61
|
+
expect(tick).not.toHaveBeenCalled();
|
|
62
|
+
h.stop();
|
|
63
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
[
|
|
2
|
+
"- [ ] Gate (last thing the run does, must be green): `timeout 300 npm run typecheck && timeout 300 npm run lint && timeout 600 npm run test:unit`.",
|
|
3
|
+
"- [ ] Gate (last thing the run does, must be green): `timeout 300 npm run typecheck && timeout 300 npm run lint && TMPDIR=$(mktemp -d) timeout 900 npm run test:unit`.",
|
|
4
|
+
"- [ ] Gate (last thing the run does, must be green): `timeout 300 npm run typecheck && timeout 300 npm run lint && TMPDIR=$(mktemp -d) timeout 900 npm run test:unit` — run the unit suite with a scratch TMPDIR so gitWorktree.test.cjs sweeps a throwaway root, never the live job-worktree root.",
|
|
5
|
+
"- [ ] Gate (last thing the run does, must be green): `timeout 300 npm run typecheck && timeout 300 npm run lint && TMPDIR=$(mktemp -d) timeout 900 npm run test:unit` — scratch TMPDIR so gitWorktree.test.cjs sweeps a throwaway root, never the live job-worktree root.",
|
|
6
|
+
"- [ ] No zustand selector returns a freshly-built value; `timeout 300 npm run lint:selectors` passes.",
|
|
7
|
+
"- [ ] No hook is declared below a top-level early return — `timeout 300 npm run lint:hooks` exits 0.",
|
|
8
|
+
"- [ ] Tests: `timeout 300 npm run typecheck` and `timeout 300 npm run lint` pass.",
|
|
9
|
+
"- [ ] Tests: `TMPDIR=$(mktemp -d) timeout 300 npx vitest run src/main/__tests__/activeIndexMerge.test.cjs` passes.",
|
|
10
|
+
"- [ ] Both tests pass: `timeout 900 npm run test:unit` and `TMPDIR=$(mktemp -d) timeout 900 npm run test:unit`.",
|
|
11
|
+
"- [ ] All 16 tests in `timeout 120 npx vitest run tests/unit/chatRunner.spec.ts` pass.",
|
|
12
|
+
"- [ ] `SM_CHAT_CONCURRENCY=3 timeout 120 npx vitest run tests/unit/chatRunner.spec.ts` passes",
|
|
13
|
+
"- [ ] Each handled basename is removed from the ALLOWLIST map; `timeout 120 node scripts/check-unregistered-tests.cjs` exits 0.",
|
|
14
|
+
"- [ ] `timeout 120 npm run lint:docs` exits 0 on the current tree.",
|
|
15
|
+
"- [ ] Locally, the same sequence passes: `timeout 300 npm run typecheck && timeout 300 npm run lint && timeout 600 npm run build:project-pages`.",
|
|
16
|
+
"- [ ] After the commit lands, `timeout 60 gh run list --branch main --limit 1` prints the newest run without error.",
|
|
17
|
+
"- [ ] `for i in 1 2 3; do TMPDIR=$(mktemp -d) timeout 180 npx vitest run src/main/__tests__/scheduler-tick-wedge.test.cjs; done` is green 3/3 (shell loop — not a no-shell gate).",
|
|
18
|
+
"- [ ] The gate is `timeout 300 npm run typecheck | tail -5` (pipe — needs a shell).",
|
|
19
|
+
"- [ ] Prose only, no command at all."
|
|
20
|
+
]
|