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
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* cwdClassify.cjs — THE one answer to "what project is this path?".
|
|
5
|
+
*
|
|
6
|
+
* activeSessions.projectRootOf, ephemeralCwd.isEphemeralCwd and
|
|
7
|
+
* opsOwnership.resolveProjectRoot each used to re-derive that answer and each
|
|
8
|
+
* failed OPEN: when a worktree's `.git` FILE survives but its admin-dir
|
|
9
|
+
* back-reference is gone, worktreeMainRootOf returned null, projectRootOf
|
|
10
|
+
* handed the worktree path back unchanged, isLinkedWorktreeRoot said false, and
|
|
11
|
+
* resolveOpsRoot built `<worktree>/session-manager-operations` — the shape
|
|
12
|
+
* behind the lost-Epics incidents. classifyCwd reports BOTH facts (which
|
|
13
|
+
* project, and how sure we are) so a caller can refuse WRITES for the
|
|
14
|
+
* unprovable case (`unknown`) while dispatch and reads carry on.
|
|
15
|
+
*
|
|
16
|
+
* Kinds:
|
|
17
|
+
* worktree — `.git` is a FILE whose gitdir sits under `<main>/.git/worktrees/`
|
|
18
|
+
* AND the main tree's admin dir back-references it (round trip).
|
|
19
|
+
* projectRoot = <main>.
|
|
20
|
+
* project — `.git` is a directory, OR a FILE whose gitdir is NOT under a
|
|
21
|
+
* worktrees/ dir (separate-git-dir / submodule), OR no `.git` at
|
|
22
|
+
* all. projectRoot = the dir holding `.git` (else the cwd).
|
|
23
|
+
* ephemeral — os.tmpdir() itself, or inside a managed job/epic worktree root,
|
|
24
|
+
* and not provably a live worktree.
|
|
25
|
+
* unknown — a `.git` FILE we cannot resolve (garbled, unreadable, worktrees
|
|
26
|
+
* gitdir with a missing/mismatched back-reference). projectRoot is
|
|
27
|
+
* the (ops-truncated) cwd unchanged; ops WRITES must be refused.
|
|
28
|
+
*
|
|
29
|
+
* Two ops-root facts are returned and deliberately NOT unified:
|
|
30
|
+
* outermostOpsRoot — first `session-manager-operations` segment (indexOf).
|
|
31
|
+
* activeSessions truncates here: a stray nested ops root is itself the bug.
|
|
32
|
+
* innermostOpsRoot — last segment (lastIndexOf). opsOwnership.assertOpsWrite
|
|
33
|
+
* derives the project a write targets from here.
|
|
34
|
+
* Both are null when the path is not inside an ops root.
|
|
35
|
+
*
|
|
36
|
+
* Pure fs, synchronous, never throws. Must stay light — required by
|
|
37
|
+
* scripts/hooks/guard-inline-implementation.cjs (via nearestGitEntry) and
|
|
38
|
+
* activeSessions.cjs; no gitWorktree/config requires.
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
const fs = require('node:fs');
|
|
42
|
+
const os = require('node:os');
|
|
43
|
+
const path = require('node:path');
|
|
44
|
+
const schedulerPaths = require('./schedulerPaths.cjs');
|
|
45
|
+
|
|
46
|
+
const OPS_DIRNAME = 'session-manager-operations';
|
|
47
|
+
const WORKTREES_MARKER = `${path.sep}.git${path.sep}worktrees${path.sep}`;
|
|
48
|
+
// Bound on the ancestor walk — past any real fs depth, guarantees termination.
|
|
49
|
+
const MAX_WALK = 40;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* nearestGitEntry(cwd) → { dir, isFile } for the nearest ancestor-or-self that
|
|
53
|
+
* has a `.git` entry (`isFile` true = linked worktree / submodule pointer file),
|
|
54
|
+
* or null. The tiny pure helper for callers that must not load git tooling.
|
|
55
|
+
*/
|
|
56
|
+
function nearestGitEntry(cwd) {
|
|
57
|
+
let dir = cwd;
|
|
58
|
+
for (let i = 0; i < MAX_WALK; i++) {
|
|
59
|
+
let stat = null;
|
|
60
|
+
try { stat = fs.statSync(path.join(dir, '.git')); } catch { /* none here */ }
|
|
61
|
+
if (stat) return { dir, isFile: stat.isFile() };
|
|
62
|
+
const parent = path.dirname(dir);
|
|
63
|
+
if (parent === dir) return null;
|
|
64
|
+
dir = parent;
|
|
65
|
+
}
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Classify the git shape at/above `start`. O(depth) stats + ≤2 small reads.
|
|
71
|
+
* → { shape: 'none' | 'gitdir' | 'worktree' | 'separate' | 'unknown', dir, mainRoot, reason }
|
|
72
|
+
*/
|
|
73
|
+
function inspectGit(start) {
|
|
74
|
+
const entry = nearestGitEntry(start);
|
|
75
|
+
if (!entry) return { shape: 'none', dir: null, mainRoot: null, reason: 'no .git found' };
|
|
76
|
+
if (!entry.isFile) return { shape: 'gitdir', dir: entry.dir, mainRoot: null, reason: '.git is a directory' };
|
|
77
|
+
|
|
78
|
+
const gitPath = path.join(entry.dir, '.git');
|
|
79
|
+
let body;
|
|
80
|
+
try {
|
|
81
|
+
body = fs.readFileSync(gitPath, 'utf8');
|
|
82
|
+
} catch (e) {
|
|
83
|
+
return { shape: 'unknown', dir: entry.dir, mainRoot: null, reason: `unreadable .git file: ${e?.code || e?.message}` };
|
|
84
|
+
}
|
|
85
|
+
const firstLine = body.split('\n', 1)[0].trim();
|
|
86
|
+
if (!firstLine.startsWith('gitdir:')) {
|
|
87
|
+
return { shape: 'unknown', dir: entry.dir, mainRoot: null, reason: '.git file has no "gitdir:" line' };
|
|
88
|
+
}
|
|
89
|
+
const gitdirRaw = firstLine.slice('gitdir:'.length).trim();
|
|
90
|
+
if (!gitdirRaw) {
|
|
91
|
+
return { shape: 'unknown', dir: entry.dir, mainRoot: null, reason: '.git file has an empty gitdir' };
|
|
92
|
+
}
|
|
93
|
+
// Submodules point at a RELATIVE gitdir; resolve against the file's own dir.
|
|
94
|
+
const gitdir = path.resolve(entry.dir, gitdirRaw);
|
|
95
|
+
const markerIdx = gitdir.indexOf(WORKTREES_MARKER);
|
|
96
|
+
if (markerIdx === -1) {
|
|
97
|
+
// Not a linked worktree: separate-git-dir / submodule. Its own project.
|
|
98
|
+
return { shape: 'separate', dir: entry.dir, mainRoot: null, reason: 'gitdir is not under .git/worktrees/ (separate-git-dir or submodule)' };
|
|
99
|
+
}
|
|
100
|
+
if (markerIdx <= 0) {
|
|
101
|
+
return { shape: 'unknown', dir: entry.dir, mainRoot: null, reason: 'gitdir worktrees marker has no main root above it' };
|
|
102
|
+
}
|
|
103
|
+
const candidateMain = gitdir.slice(0, markerIdx);
|
|
104
|
+
const worktreeName = gitdir.slice(markerIdx + WORKTREES_MARKER.length).split(path.sep)[0];
|
|
105
|
+
if (!worktreeName) {
|
|
106
|
+
return { shape: 'unknown', dir: entry.dir, mainRoot: null, reason: 'gitdir names no worktree' };
|
|
107
|
+
}
|
|
108
|
+
// Round-trip: the .git file is plain text anything could have written, so it
|
|
109
|
+
// must not redirect callers to an arbitrary path. The main tree's own admin
|
|
110
|
+
// dir must point back at THIS .git file.
|
|
111
|
+
const adminGitdirFile = path.join(candidateMain, '.git', 'worktrees', worktreeName, 'gitdir');
|
|
112
|
+
let backRef;
|
|
113
|
+
try {
|
|
114
|
+
backRef = fs.readFileSync(adminGitdirFile, 'utf8').trim();
|
|
115
|
+
} catch {
|
|
116
|
+
return { shape: 'unknown', dir: entry.dir, mainRoot: null, reason: `worktree admin back-reference missing (${adminGitdirFile})` };
|
|
117
|
+
}
|
|
118
|
+
if (path.resolve(backRef) !== path.resolve(gitPath)) {
|
|
119
|
+
return { shape: 'unknown', dir: entry.dir, mainRoot: null, reason: `worktree admin back-reference points elsewhere (${adminGitdirFile})` };
|
|
120
|
+
}
|
|
121
|
+
return { shape: 'worktree', dir: entry.dir, mainRoot: candidateMain, reason: 'linked worktree, back-reference verified' };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* worktreeMainRootOf(cwd) → the main tree's root when cwd sits inside a
|
|
126
|
+
* verified linked worktree, the ancestor itself when `.git` is a directory,
|
|
127
|
+
* else null (no git, separate-git-dir, or anything unprovable).
|
|
128
|
+
*/
|
|
129
|
+
function worktreeMainRootOf(cwd) {
|
|
130
|
+
const g = inspectGit(cwd);
|
|
131
|
+
if (g.shape === 'worktree') return g.mainRoot;
|
|
132
|
+
if (g.shape === 'gitdir') return g.dir;
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function isExactlyTmpdir(absCwd) {
|
|
137
|
+
return absCwd === path.resolve(os.tmpdir());
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function isUnderManagedWorktreeRoot(absCwd) {
|
|
141
|
+
return ['job', 'epic'].map((k) => path.resolve(schedulerPaths.worktreeRoot(k))).some((root) => {
|
|
142
|
+
const rel = path.relative(root, absCwd);
|
|
143
|
+
return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* classifyCwd(cwd) → { kind, projectRoot, outermostOpsRoot, innermostOpsRoot, reason }
|
|
149
|
+
* See the file header. A non-string/empty cwd is `unknown` with a null root.
|
|
150
|
+
*/
|
|
151
|
+
function classifyCwd(cwd) {
|
|
152
|
+
if (!cwd || typeof cwd !== 'string') {
|
|
153
|
+
return { kind: 'unknown', projectRoot: null, outermostOpsRoot: null, innermostOpsRoot: null, reason: 'cwd is not a non-empty string' };
|
|
154
|
+
}
|
|
155
|
+
const parts = cwd.split(path.sep);
|
|
156
|
+
const first = parts.indexOf(OPS_DIRNAME);
|
|
157
|
+
const last = parts.lastIndexOf(OPS_DIRNAME);
|
|
158
|
+
const opsAt = (i) => (i >= 0 ? (parts.slice(0, i + 1).join(path.sep) || path.sep) : null);
|
|
159
|
+
const outermostOpsRoot = opsAt(first);
|
|
160
|
+
const innermostOpsRoot = opsAt(last);
|
|
161
|
+
// Outermost truncation (the activeSessions law). i === 0 is a relative
|
|
162
|
+
// fragment, never truncated.
|
|
163
|
+
const truncated = first > 0 ? (parts.slice(0, first).join(path.sep) || path.sep) : cwd;
|
|
164
|
+
|
|
165
|
+
const g = inspectGit(truncated);
|
|
166
|
+
const facts = { outermostOpsRoot, innermostOpsRoot };
|
|
167
|
+
if (g.shape === 'worktree') {
|
|
168
|
+
return { kind: 'worktree', projectRoot: g.mainRoot, ...facts, reason: g.reason };
|
|
169
|
+
}
|
|
170
|
+
if (path.isAbsolute(cwd)) {
|
|
171
|
+
const abs = path.resolve(cwd);
|
|
172
|
+
if (isExactlyTmpdir(abs) || isUnderManagedWorktreeRoot(abs)) {
|
|
173
|
+
return { kind: 'ephemeral', projectRoot: truncated, ...facts, reason: 'tmpdir or managed job/epic worktree root' };
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
if (g.shape === 'unknown') {
|
|
177
|
+
return { kind: 'unknown', projectRoot: truncated, ...facts, reason: g.reason };
|
|
178
|
+
}
|
|
179
|
+
if (g.shape === 'none') {
|
|
180
|
+
return { kind: 'project', projectRoot: truncated, ...facts, reason: g.reason };
|
|
181
|
+
}
|
|
182
|
+
return { kind: 'project', projectRoot: g.dir, ...facts, reason: g.reason };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
module.exports = { classifyCwd, nearestGitEntry, worktreeMainRootOf, OPS_DIRNAME };
|
|
@@ -91,41 +91,104 @@ function reportExists(key, runsDir = RUNS_DIR) {
|
|
|
91
91
|
return false;
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
-
|
|
95
|
-
// non-user-supplied strings. Commands containing these are marked unverifiable
|
|
96
|
-
// rather than running under a shell, since CLAUDE.md restricts shell:true to
|
|
97
|
-
// watchers.cjs and app:test-fire-hook only.
|
|
98
|
-
const SHELL_META_RE = /[|>&;<`]|\$[({]/;
|
|
94
|
+
const DEFAULT_GATE_TIMEOUT_MS = 300_000;
|
|
99
95
|
|
|
100
96
|
/**
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
* Commands containing shell metacharacters (pipe, redirect, subshell) are
|
|
107
|
-
* skipped — they cannot be run without shell:true which is prohibited here.
|
|
108
|
-
* The parser falls through to the next AC line in that case.
|
|
109
|
-
*
|
|
110
|
-
* Complexity: O(L) where L = number of lines in the body (not user-scaled;
|
|
111
|
-
* PRD bodies are bounded documents, typically < 200 lines).
|
|
112
|
-
*
|
|
113
|
-
* @param {string} prdBody PRD markdown with frontmatter already stripped.
|
|
114
|
-
* @returns {string|null}
|
|
97
|
+
* Quote-aware tokenizer for ONE command segment — no shell. Returns the argv
|
|
98
|
+
* tokens, or null when an unquoted shell metacharacter survives (pipe,
|
|
99
|
+
* redirect, `;`, lone `&`, backtick, `$(`/`${`) or a quote is unbalanced.
|
|
100
|
+
* Complexity: O(n) over the segment's characters.
|
|
115
101
|
*/
|
|
116
|
-
function
|
|
117
|
-
|
|
102
|
+
function tokenizeNoShell(seg) {
|
|
103
|
+
const tokens = [];
|
|
104
|
+
let cur = '';
|
|
105
|
+
let inTok = false;
|
|
106
|
+
let quote = null;
|
|
107
|
+
for (let i = 0; i < seg.length; i++) {
|
|
108
|
+
const c = seg[i];
|
|
109
|
+
if (quote) {
|
|
110
|
+
if (c === quote) quote = null;
|
|
111
|
+
else cur += c;
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (c === '"' || c === "'") { quote = c; inTok = true; continue; }
|
|
115
|
+
if (/\s/.test(c)) {
|
|
116
|
+
if (inTok) { tokens.push(cur); cur = ''; inTok = false; }
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
if ('|<>;&`'.includes(c) || (c === '$' && (seg[i + 1] === '(' || seg[i + 1] === '{'))) return null;
|
|
120
|
+
cur += c;
|
|
121
|
+
inTok = true;
|
|
122
|
+
}
|
|
123
|
+
if (quote) return null;
|
|
124
|
+
if (inTok) tokens.push(cur);
|
|
125
|
+
return tokens;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Split a command chain on `&&` outside quotes and backticks.
|
|
130
|
+
* Complexity: O(n).
|
|
131
|
+
*/
|
|
132
|
+
function splitOnAndAnd(text) {
|
|
133
|
+
const segs = [];
|
|
134
|
+
let cur = '';
|
|
135
|
+
let quote = null;
|
|
136
|
+
for (let i = 0; i < text.length; i++) {
|
|
137
|
+
const c = text[i];
|
|
138
|
+
if (quote) {
|
|
139
|
+
cur += c;
|
|
140
|
+
if (c === quote) quote = null;
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
if (c === '"' || c === "'" || c === '`') { quote = c; cur += c; continue; }
|
|
144
|
+
if (c === '&' && text[i + 1] === '&') { segs.push(cur); cur = ''; i++; continue; }
|
|
145
|
+
cur += c;
|
|
146
|
+
}
|
|
147
|
+
segs.push(cur);
|
|
148
|
+
return segs;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Parse one `&&` chain into [{argv, timeoutMs, env, raw}]. Returns [] when any
|
|
153
|
+
* segment is empty or needs a shell — a half-parsed gate must never run.
|
|
154
|
+
* A leading `timeout N` (or `Ns`) token sets timeoutMs; leading literal
|
|
155
|
+
* `NAME=value` tokens become env (a `TMPDIR=` assignment is dropped — the
|
|
156
|
+
* shadow runner always supplies its own isolated TMPDIR; its value is
|
|
157
|
+
* typically the un-runnable `$(mktemp -d)`, which is handled here without a
|
|
158
|
+
* shell by simply never evaluating it).
|
|
159
|
+
*/
|
|
160
|
+
function parseChain(text) {
|
|
161
|
+
const out = [];
|
|
162
|
+
for (const seg of splitOnAndAnd(text)) {
|
|
163
|
+
const raw = seg.trim();
|
|
164
|
+
if (!raw) return [];
|
|
165
|
+
// A dropped TMPDIR=$(mktemp -d) prefix is the one substitution we accept.
|
|
166
|
+
const stripped = raw.replace(/^TMPDIR=\$\(mktemp -d\)\s+/, '');
|
|
167
|
+
const tokens = tokenizeNoShell(stripped);
|
|
168
|
+
if (!tokens || !tokens.length) return [];
|
|
169
|
+
const env = {};
|
|
170
|
+
while (tokens.length && /^[A-Za-z_][A-Za-z0-9_]*=/.test(tokens[0])) {
|
|
171
|
+
const [k, ...rest] = tokens.shift().split('=');
|
|
172
|
+
if (k !== 'TMPDIR') env[k] = rest.join('=');
|
|
173
|
+
}
|
|
174
|
+
let timeoutMs = DEFAULT_GATE_TIMEOUT_MS;
|
|
175
|
+
if (tokens[0] === 'timeout' && /^\d+s?$/.test(tokens[1] || '')) {
|
|
176
|
+
timeoutMs = parseInt(tokens[1], 10) * 1000;
|
|
177
|
+
tokens.splice(0, 2);
|
|
178
|
+
}
|
|
179
|
+
if (!tokens.length) return [];
|
|
180
|
+
out.push({ argv: tokens, timeoutMs, env, raw });
|
|
181
|
+
}
|
|
182
|
+
return out;
|
|
183
|
+
}
|
|
118
184
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
// immediately. Line-by-line parsing avoids that pitfall.
|
|
123
|
-
const lines = prdBody.split('\n');
|
|
185
|
+
/** Locate the `# Acceptance criteria` lines (whole body when the heading is absent). */
|
|
186
|
+
function acCandidateLines(body) {
|
|
187
|
+
const lines = body.split('\n');
|
|
124
188
|
let inAcSection = false;
|
|
125
189
|
let hasAcSection = false;
|
|
126
190
|
let acHeadingLevel = 0;
|
|
127
191
|
const acLines = [];
|
|
128
|
-
|
|
129
192
|
for (const line of lines) {
|
|
130
193
|
if (/^#+\s/i.test(line)) {
|
|
131
194
|
const level = line.match(/^(#+)/)[1].length;
|
|
@@ -134,42 +197,127 @@ function extractAcCommand(prdBody) {
|
|
|
134
197
|
hasAcSection = true;
|
|
135
198
|
acHeadingLevel = level;
|
|
136
199
|
} else if (inAcSection && level <= acHeadingLevel) {
|
|
137
|
-
// A sibling or parent heading ends the AC section;
|
|
138
|
-
// sub-headings (level > acHeadingLevel) stay inside it.
|
|
139
200
|
inAcSection = false;
|
|
140
201
|
}
|
|
141
202
|
continue;
|
|
142
203
|
}
|
|
143
204
|
if (inAcSection) acLines.push(line);
|
|
144
205
|
}
|
|
206
|
+
return hasAcSection ? acLines : lines;
|
|
207
|
+
}
|
|
145
208
|
|
|
146
|
-
|
|
147
|
-
|
|
209
|
+
/**
|
|
210
|
+
* Read an explicit gate spec: frontmatter `gate:` (`none`, inline `[a, b]`, or
|
|
211
|
+
* a `- item` list) or a fenced ```gate block (one command chain per line).
|
|
212
|
+
* Returns { kind: 'none' } | { kind: 'commands', chains: string[] } | null.
|
|
213
|
+
*/
|
|
214
|
+
function readExplicitGate(prdText) {
|
|
215
|
+
const fmMatch = prdText.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/);
|
|
216
|
+
if (fmMatch) {
|
|
217
|
+
const fmLines = fmMatch[1].split(/\r?\n/);
|
|
218
|
+
for (let i = 0; i < fmLines.length; i++) {
|
|
219
|
+
const m = fmLines[i].match(/^gate:\s*(.*)$/);
|
|
220
|
+
if (!m) continue;
|
|
221
|
+
const rest = m[1].trim();
|
|
222
|
+
if (/^(['"]?)none\1$/i.test(rest)) return { kind: 'none' };
|
|
223
|
+
const chains = [];
|
|
224
|
+
if (rest.startsWith('[') && rest.endsWith(']')) {
|
|
225
|
+
// Inline list — items may hold commas inside quotes, so split quote-aware.
|
|
226
|
+
let cur = ''; let q = null;
|
|
227
|
+
for (const c of rest.slice(1, -1)) {
|
|
228
|
+
if (q) { if (c === q) q = null; else cur += c; continue; }
|
|
229
|
+
if (c === '"' || c === "'") { q = c; continue; }
|
|
230
|
+
if (c === ',') { chains.push(cur.trim()); cur = ''; continue; }
|
|
231
|
+
cur += c;
|
|
232
|
+
}
|
|
233
|
+
chains.push(cur.trim());
|
|
234
|
+
} else if (rest === '') {
|
|
235
|
+
for (let j = i + 1; j < fmLines.length; j++) {
|
|
236
|
+
const li = fmLines[j].match(/^\s+-\s+(.*)$/);
|
|
237
|
+
if (!li) break;
|
|
238
|
+
chains.push(li[1].trim().replace(/^(['"])(.*)\1$/, '$2'));
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
const kept = chains.filter(Boolean);
|
|
242
|
+
if (kept.length) return { kind: 'commands', chains: kept };
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
const fence = prdText.match(/^```gate[ \t]*\r?\n([\s\S]*?)^```/m);
|
|
246
|
+
if (fence) {
|
|
247
|
+
const chains = fence[1].split('\n').map((l) => l.trim()).filter((l) => l && !l.startsWith('#'));
|
|
248
|
+
if (chains.length === 1 && /^none$/i.test(chains[0])) return { kind: 'none' };
|
|
249
|
+
if (chains.length) return { kind: 'commands', chains };
|
|
250
|
+
}
|
|
251
|
+
return null;
|
|
252
|
+
}
|
|
148
253
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
254
|
+
/**
|
|
255
|
+
* Resolve a PRD's gate: explicit spec first (frontmatter `gate:` / ```gate
|
|
256
|
+
* fence), else the first acceptance-criteria line carrying a parseable
|
|
257
|
+
* `timeout N …` chain (backtick span preferred, else bare from the first
|
|
258
|
+
* `timeout N`). Accepts the PRD with or without frontmatter.
|
|
259
|
+
*
|
|
260
|
+
* @param {string} prdText
|
|
261
|
+
* @returns {{ source: 'none'|'explicit'|'ac-line'|'absent', sequence: Array<{argv:string[], timeoutMs:number, env:object, raw:string}> }}
|
|
262
|
+
*/
|
|
263
|
+
function resolveGate(prdText) {
|
|
264
|
+
if (!prdText || typeof prdText !== 'string') return { source: 'absent', sequence: [] };
|
|
265
|
+
const explicit = readExplicitGate(prdText);
|
|
266
|
+
if (explicit) {
|
|
267
|
+
if (explicit.kind === 'none') return { source: 'none', sequence: [] };
|
|
268
|
+
const sequence = [];
|
|
269
|
+
for (const chain of explicit.chains) {
|
|
270
|
+
const parsed = parseChain(chain);
|
|
271
|
+
if (!parsed.length) return { source: 'explicit', sequence: [] };
|
|
272
|
+
sequence.push(...parsed);
|
|
273
|
+
}
|
|
274
|
+
return { source: 'explicit', sequence };
|
|
275
|
+
}
|
|
276
|
+
const body = splitFrontmatter(prdText).body;
|
|
277
|
+
for (const line of acCandidateLines(body)) {
|
|
278
|
+
const backtickMatch = line.match(/`([^`]*\btimeout\s+\d+\s[^`]*)`/i);
|
|
152
279
|
if (backtickMatch) {
|
|
153
|
-
const
|
|
154
|
-
if (
|
|
280
|
+
const seq = parseChain(backtickMatch[1]);
|
|
281
|
+
if (seq.length) return { source: 'ac-line', sequence: seq };
|
|
155
282
|
}
|
|
156
|
-
|
|
157
|
-
//
|
|
158
|
-
// Trim trailing all-lowercase-alpha tokens (prose words like "passes",
|
|
159
|
-
// "and", "the") while keeping at least 4 tokens (timeout, N, binary,
|
|
160
|
-
// first-arg). This prevents over-matching into prose that follows the
|
|
161
|
-
// command on the same AC line (e.g. "timeout 60 npm test and verify").
|
|
283
|
+
// Bare fallback: from the first `timeout N` to end of line, dropping
|
|
284
|
+
// trailing all-lowercase prose words (keeping ≥ 4 tokens, as before).
|
|
162
285
|
const rawMatch = line.match(/\btimeout\s+\d+\s+\S+(?:\s+\S+)*/);
|
|
163
286
|
if (rawMatch) {
|
|
164
|
-
const
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
const
|
|
169
|
-
if (
|
|
287
|
+
const segs = splitOnAndAnd(rawMatch[0]);
|
|
288
|
+
const tokens = segs[segs.length - 1].trim().split(/\s+/);
|
|
289
|
+
while (tokens.length > 4 && /^[a-z]+$/.test(tokens[tokens.length - 1])) tokens.pop();
|
|
290
|
+
segs[segs.length - 1] = tokens.join(' ');
|
|
291
|
+
const seq = parseChain(segs.join('&&'));
|
|
292
|
+
if (seq.length) return { source: 'ac-line', sequence: seq };
|
|
170
293
|
}
|
|
171
294
|
}
|
|
172
|
-
return
|
|
295
|
+
return { source: 'absent', sequence: [] };
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* The gate as an ordered list of no-shell commands: [{argv, timeoutMs}, …].
|
|
300
|
+
* Empty when there is no parseable gate or the PRD opted out with `gate: none`
|
|
301
|
+
* (resolveGate distinguishes the two). Complexity: O(L) over the PRD's lines.
|
|
302
|
+
*
|
|
303
|
+
* @param {string} prdBody PRD markdown (frontmatter optional).
|
|
304
|
+
* @returns {Array<{argv:string[], timeoutMs:number, env:object, raw:string}>}
|
|
305
|
+
*/
|
|
306
|
+
function extractAcSequence(prdBody) {
|
|
307
|
+
return resolveGate(prdBody).sequence;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* The single-command view of the gate, as the original `timeout NNN cmd …`
|
|
312
|
+
* string, or null when the gate is absent or is a multi-command chain
|
|
313
|
+
* (reverifyAc runs one command; the chain is the shadow runner's job).
|
|
314
|
+
*
|
|
315
|
+
* @param {string} prdBody PRD markdown with frontmatter already stripped.
|
|
316
|
+
* @returns {string|null}
|
|
317
|
+
*/
|
|
318
|
+
function extractAcCommand(prdBody) {
|
|
319
|
+
const seq = extractAcSequence(prdBody);
|
|
320
|
+
return seq.length === 1 && seq[0].raw.startsWith('timeout') ? seq[0].raw : null;
|
|
173
321
|
}
|
|
174
322
|
|
|
175
323
|
/**
|
|
@@ -217,8 +365,8 @@ function reverifyAc(job, { timeoutMs = 60_000, prdsDir } = {}) {
|
|
|
217
365
|
const cmd = extractAcCommand(prdBody);
|
|
218
366
|
if (!cmd) return Promise.resolve(unverifiable());
|
|
219
367
|
|
|
220
|
-
// Simple whitespace split —
|
|
221
|
-
// commands that would need a shell. Paths with spaces are not expected in AC
|
|
368
|
+
// Simple whitespace split — extractAcCommand already excludes
|
|
369
|
+
// commands that would need a shell (tokenizeNoShell). Paths with spaces are not expected in AC
|
|
222
370
|
// commands (PRD authoring convention: use relative paths from cwd).
|
|
223
371
|
const argv = cmd.trim().split(/\s+/);
|
|
224
372
|
if (argv.length < 2) return Promise.resolve(unverifiable());
|
|
@@ -231,7 +379,7 @@ function reverifyAc(job, { timeoutMs = 60_000, prdsDir } = {}) {
|
|
|
231
379
|
child = spawn(argv[0], argv.slice(1), {
|
|
232
380
|
cwd: job.cwd,
|
|
233
381
|
stdio: 'ignore',
|
|
234
|
-
// No shell:true —
|
|
382
|
+
// No shell:true — tokenizeNoShell rejects commands with shell metacharacters.
|
|
235
383
|
});
|
|
236
384
|
} catch (err) {
|
|
237
385
|
resolve(unverifiable());
|
|
@@ -532,6 +680,99 @@ function writeReport(key, { acResults = [], riskFlags = [], runsDir } = {}) {
|
|
|
532
680
|
return reportPath;
|
|
533
681
|
}
|
|
534
682
|
|
|
683
|
+
|
|
684
|
+
// ─── Shadow gate runner ────────────────────────────────────────────────────────
|
|
685
|
+
// Runs a PRD's parsed gate sequence WITHOUT deciding anything: the caller
|
|
686
|
+
// records the outcome next to the verdict. Not a claude -p run, so it takes no
|
|
687
|
+
// slot from sessionSlots; instead a single module-level flag keeps at most one
|
|
688
|
+
// shadow gate in flight machine-wide (the scheduler is one process).
|
|
689
|
+
let gateInFlight = false;
|
|
690
|
+
const OUTPUT_TAIL_CHARS = 1000;
|
|
691
|
+
|
|
692
|
+
function runOneGateCommand(cmd, { cwd, env }) {
|
|
693
|
+
return new Promise((resolve) => {
|
|
694
|
+
const startNs = process.hrtime.bigint();
|
|
695
|
+
const ms = () => Math.round(Number(process.hrtime.bigint() - startNs) / 1e6);
|
|
696
|
+
const label = cmd.argv.join(' ');
|
|
697
|
+
let child;
|
|
698
|
+
try {
|
|
699
|
+
child = spawn(cmd.argv[0], cmd.argv.slice(1), {
|
|
700
|
+
cwd,
|
|
701
|
+
env: { ...env, ...cmd.env },
|
|
702
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
703
|
+
// No shell:true — the sequence was tokenized by tokenizeNoShell.
|
|
704
|
+
});
|
|
705
|
+
} catch {
|
|
706
|
+
resolve({ cmd: label, status: 'unavailable', code: null, ms: ms(), timedOut: false, tail: '' });
|
|
707
|
+
return;
|
|
708
|
+
}
|
|
709
|
+
let tail = '';
|
|
710
|
+
const onData = (d) => { tail = (tail + d).slice(-OUTPUT_TAIL_CHARS); };
|
|
711
|
+
child.stdout.on('data', onData);
|
|
712
|
+
child.stderr.on('data', onData);
|
|
713
|
+
let settled = false;
|
|
714
|
+
let timedOut = false;
|
|
715
|
+
let escalate;
|
|
716
|
+
const killTimer = setTimeout(() => {
|
|
717
|
+
timedOut = true;
|
|
718
|
+
try { child.kill('SIGTERM'); } catch { /* already dead */ }
|
|
719
|
+
escalate = setTimeout(() => {
|
|
720
|
+
try { child.kill('SIGKILL'); } catch { /* race */ }
|
|
721
|
+
}, 5_000);
|
|
722
|
+
if (escalate.unref) escalate.unref();
|
|
723
|
+
}, cmd.timeoutMs);
|
|
724
|
+
if (killTimer.unref) killTimer.unref();
|
|
725
|
+
const finish = (r) => {
|
|
726
|
+
if (settled) return;
|
|
727
|
+
settled = true;
|
|
728
|
+
clearTimeout(killTimer);
|
|
729
|
+
clearTimeout(escalate);
|
|
730
|
+
resolve({ cmd: label, ms: ms(), timedOut, tail, ...r });
|
|
731
|
+
};
|
|
732
|
+
child.on('error', () => finish({ status: 'unavailable', code: null }));
|
|
733
|
+
child.on('close', (code) => {
|
|
734
|
+
const exitCode = typeof code === 'number' ? code : -1;
|
|
735
|
+
finish({ status: exitCode === 0 ? 'pass' : 'fail', code: exitCode });
|
|
736
|
+
});
|
|
737
|
+
});
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
/**
|
|
741
|
+
* Run a gate sequence (extractAcSequence output) in `cwd` with `&&` semantics:
|
|
742
|
+
* stop at the first non-pass. Each run gets its own isolated TMPDIR (mkdtemp,
|
|
743
|
+
* removed afterwards) and CI=1, so a suite that sweeps its temp root can never
|
|
744
|
+
* touch a live one.
|
|
745
|
+
*
|
|
746
|
+
* @param {Array<{argv:string[], timeoutMs:number, env?:object}>} sequence
|
|
747
|
+
* @param {{ cwd: string }} opts
|
|
748
|
+
* @returns {Promise<{ status: 'green'|'red'|'unavailable'|'busy', results: object[] }>}
|
|
749
|
+
* 'busy' = another shadow gate is in flight; nothing ran, nothing to record.
|
|
750
|
+
*/
|
|
751
|
+
async function runGateSequence(sequence, { cwd }) {
|
|
752
|
+
if (gateInFlight) return { status: 'busy', results: [] };
|
|
753
|
+
if (!Array.isArray(sequence) || !sequence.length) return { status: 'unavailable', results: [] };
|
|
754
|
+
try { fs.statSync(cwd); } catch { return { status: 'unavailable', results: [] }; }
|
|
755
|
+
gateInFlight = true;
|
|
756
|
+
let tmp = null;
|
|
757
|
+
try {
|
|
758
|
+
tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'sm-gate-shadow-'));
|
|
759
|
+
const env = { ...process.env, TMPDIR: tmp, CI: '1' };
|
|
760
|
+
const results = [];
|
|
761
|
+
for (const cmd of sequence) {
|
|
762
|
+
const r = await runOneGateCommand(cmd, { cwd, env });
|
|
763
|
+
results.push(r);
|
|
764
|
+
if (r.status === 'unavailable') return { status: 'unavailable', results };
|
|
765
|
+
if (r.status === 'fail') return { status: 'red', results };
|
|
766
|
+
}
|
|
767
|
+
return { status: 'green', results };
|
|
768
|
+
} catch {
|
|
769
|
+
return { status: 'unavailable', results: [] };
|
|
770
|
+
} finally {
|
|
771
|
+
gateInFlight = false;
|
|
772
|
+
if (tmp) { try { fs.rmSync(tmp, { recursive: true, force: true }); } catch { /* best-effort */ } }
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
|
|
535
776
|
const WATERMARK_FILENAME = '.dod-watermark.json';
|
|
536
777
|
|
|
537
778
|
/**
|
|
@@ -573,6 +814,9 @@ module.exports = {
|
|
|
573
814
|
reportPathFor,
|
|
574
815
|
reportExists,
|
|
575
816
|
extractAcCommand,
|
|
817
|
+
extractAcSequence,
|
|
818
|
+
resolveGate,
|
|
819
|
+
runGateSequence,
|
|
576
820
|
extractSection,
|
|
577
821
|
reverifyAc,
|
|
578
822
|
reverifyBatch,
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* dispatchLoop.cjs — dispatch's own periodic driver. Cadence is owned here and
|
|
5
|
+
* nothing else can degrade it: no billing backoff, utilization, fire policy or
|
|
6
|
+
* cancel token is read. It only calls the injected `tick` (scheduler's
|
|
7
|
+
* tickQueue, which funnels through sessionSlots/pickNextBatch).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const RECONCILE_INTERVAL_MS = 30_000;
|
|
11
|
+
const MIN_INTERVAL_MS = 10_000;
|
|
12
|
+
const MAX_INTERVAL_MS = 300_000;
|
|
13
|
+
|
|
14
|
+
/** Pure: env override (SM_DISPATCH_LOOP_INTERVAL_MS) or explicit value, clamped to [10s, 300s]. */
|
|
15
|
+
function resolveIntervalMs(intervalMs, env = process.env) {
|
|
16
|
+
const raw = intervalMs ?? env.SM_DISPATCH_LOOP_INTERVAL_MS;
|
|
17
|
+
const n = Number(raw);
|
|
18
|
+
if (raw === undefined || raw === null || raw === '' || !Number.isFinite(n)) return RECONCILE_INTERVAL_MS;
|
|
19
|
+
return Math.min(MAX_INTERVAL_MS, Math.max(MIN_INTERVAL_MS, Math.floor(n)));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @param {{ tick: () => any, intervalMs?: number, onError?: (e: Error) => void }} opts
|
|
24
|
+
* @returns {{ stop: () => void }}
|
|
25
|
+
*/
|
|
26
|
+
function startDispatchLoop({ tick, intervalMs, onError } = {}) {
|
|
27
|
+
if (typeof tick !== 'function') throw new TypeError('startDispatchLoop: tick must be a function');
|
|
28
|
+
if (process.env.SM_DISPATCH_LOOP_DISABLE === '1') return { stop() {} };
|
|
29
|
+
let running = false;
|
|
30
|
+
const report = (e) => { try { if (onError) onError(e); } catch { /* reporting must not throw */ } };
|
|
31
|
+
const timer = setInterval(async () => {
|
|
32
|
+
if (running) return; // previous pass still in flight — no pile-up
|
|
33
|
+
running = true;
|
|
34
|
+
try { await tick(); } catch (e) { report(e); } finally { running = false; }
|
|
35
|
+
}, resolveIntervalMs(intervalMs));
|
|
36
|
+
if (timer.unref) timer.unref();
|
|
37
|
+
return { stop() { clearInterval(timer); } };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
module.exports = { startDispatchLoop, resolveIntervalMs, RECONCILE_INTERVAL_MS, MIN_INTERVAL_MS, MAX_INTERVAL_MS };
|
|
@@ -91,7 +91,14 @@ function computeEffortReachable(deps) {
|
|
|
91
91
|
*/
|
|
92
92
|
async function resolvePersonaModelAlias(cwd, agentType, deps) {
|
|
93
93
|
const getPersonaBody = deps.getPersonaBody || require('../agentLibrary.cjs').getPersonaBody;
|
|
94
|
-
|
|
94
|
+
// The overlay-vs-global provenance question itself ("which of
|
|
95
|
+
// resolvePersonaPaths' two candidates won") is answered by
|
|
96
|
+
// agentModelResolve.cjs's isProjectOverlayPersonaPath, not re-derived here
|
|
97
|
+
// — that module already owns this precedence decision for the launch
|
|
98
|
+
// path (readOverlayAwarePersonaModel), and this observer must never let
|
|
99
|
+
// its own copy of the comparison drift from the one that actually decides
|
|
100
|
+
// what gets spawned.
|
|
101
|
+
const { isProjectOverlayPersonaPath } = deps.agentModelResolve || require('./agentModelResolve.cjs');
|
|
95
102
|
|
|
96
103
|
let persona;
|
|
97
104
|
try {
|
|
@@ -102,15 +109,7 @@ async function resolvePersonaModelAlias(cwd, agentType, deps) {
|
|
|
102
109
|
if (!persona) return { modelAlias: null, modelSource: 'fallback' };
|
|
103
110
|
|
|
104
111
|
const { fm } = splitFrontmatter(persona.text);
|
|
105
|
-
|
|
106
|
-
let fromOverlay = false;
|
|
107
|
-
try {
|
|
108
|
-
const { projectPath } = resolvePersonaPaths(cwd, agentType);
|
|
109
|
-
fromOverlay = Boolean(projectPath) && path.resolve(projectPath) === path.resolve(persona.path);
|
|
110
|
-
} catch {
|
|
111
|
-
// Best-effort overlay signal only — a resolution failure here just means
|
|
112
|
-
// "assume global", never a reason to fail the whole lookup.
|
|
113
|
-
}
|
|
112
|
+
const fromOverlay = isProjectOverlayPersonaPath(cwd, agentType, persona.path, deps);
|
|
114
113
|
|
|
115
114
|
if (fm.model && fm.model !== 'inherit') {
|
|
116
115
|
return { modelAlias: fm.model, modelSource: fromOverlay ? 'persona-overlay' : 'persona' };
|