session-orchestrator 3.20.0 → 3.22.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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.cursor/rules/000-session-orchestrator.mdc +3 -2
- package/.cursor/rules/030-wave-execution.mdc +10 -8
- package/.cursor/rules/040-discovery.mdc +6 -6
- package/.cursor/rules/050-plan.mdc +8 -8
- package/CHANGELOG.md +515 -0
- package/README.md +16 -11
- package/agents/analyst.md +1 -1
- package/agents/architect-reviewer.md +1 -1
- package/agents/code-implementer.md +4 -2
- package/agents/db-specialist.md +1 -1
- package/agents/dialectic-deriver.md +1 -1
- package/agents/docs-writer.md +1 -1
- package/agents/memory-proposal-collector.md +7 -5
- package/agents/qa-strategist.md +1 -1
- package/agents/security-reviewer.md +1 -1
- package/agents/session-reviewer.md +42 -1
- package/agents/skill-applied-judge.md +1 -1
- package/agents/test-writer.md +1 -1
- package/agents/ui-developer.md +1 -1
- package/agents/ux-evaluator.md +1 -1
- package/commands/eli5.md +33 -0
- package/commands/release.md +62 -0
- package/commands/test.md +2 -2
- package/docs/components.md +6 -5
- package/docs/migration-v3.md +9 -6
- package/docs/persona-panel.md +3 -1
- package/docs/scope-collision-guard.md +167 -0
- package/docs/session-config-reference.md +31 -8
- package/hooks/_lib/lock-bootstrap.mjs +19 -13
- package/hooks/enforce-scope.mjs +103 -3
- package/hooks/hooks-codex.json +1 -1
- package/hooks/hooks.json +21 -1
- package/hooks/on-session-end.mjs +76 -97
- package/hooks/on-session-start.mjs +195 -104
- package/hooks/on-stop.mjs +127 -12
- package/hooks/post-bash-write-verify.mjs +8 -32
- package/hooks/pre-auq-clarity.mjs +787 -0
- package/hooks/pre-bash-issue-budget.mjs +17 -18
- package/hooks/pre-task-scope-disjoint.mjs +1042 -0
- package/package.json +3 -1
- package/pi/prompts/eli5.md +12 -0
- package/pi/prompts/release.md +12 -0
- package/scripts/auq-audit.mjs +825 -0
- package/scripts/autopilot.mjs +10 -9
- package/scripts/emit-session.mjs +42 -0
- package/scripts/export-hw-learnings.mjs +61 -2
- package/scripts/lib/auq/clarity.mjs +1314 -0
- package/scripts/lib/auq/parse.mjs +1006 -0
- package/scripts/lib/auq/schema.mjs +1457 -0
- package/scripts/lib/autopilot/worktree-pipeline.mjs +5 -5
- package/scripts/lib/backlog-scan.mjs +106 -15
- package/scripts/lib/build-live-signals.mjs +7 -3
- package/scripts/lib/ci-status-banner.mjs +267 -77
- package/scripts/lib/config/dispatcher-autonomy-capture.mjs +32 -9
- package/scripts/lib/config/vault-integration.mjs +12 -1
- package/scripts/lib/dispatcher/rank.mjs +4 -7
- package/scripts/lib/gates/gate-full.mjs +3 -3
- package/scripts/lib/gates/gate-helpers.mjs +17 -6
- package/scripts/lib/git-config-drift.mjs +471 -0
- package/scripts/lib/harness-audit/categories/category6.mjs +65 -12
- package/scripts/lib/io.mjs +432 -7
- package/scripts/lib/issue-budget.mjs +63 -9
- package/scripts/lib/learnings/select.mjs +157 -3
- package/scripts/lib/memory-cleanup-stamp.mjs +132 -8
- package/scripts/lib/mirror-issues-banner.mjs +266 -0
- package/scripts/lib/named-vault-resolver.mjs +105 -16
- package/scripts/lib/owner-interview.mjs +78 -32
- package/scripts/lib/peer-cards/schema.mjs +6 -2
- package/scripts/lib/peer-discovery.mjs +73 -22
- package/scripts/lib/project-hygiene.mjs +64 -4
- package/scripts/lib/reconcile/renderer.mjs +17 -4
- package/scripts/lib/reconcile/writer.mjs +69 -30
- package/scripts/lib/redact-spans.mjs +89 -0
- package/scripts/lib/resource-probe/evaluate.mjs +330 -149
- package/scripts/lib/resource-probe/probe-platform.mjs +35 -0
- package/scripts/lib/resource-probe.mjs +18 -2
- package/scripts/lib/scope-baseline.mjs +77 -17
- package/scripts/lib/scope-gate.mjs +658 -0
- package/scripts/lib/secret-masker.mjs +262 -0
- package/scripts/lib/session-lock.mjs +34 -10
- package/scripts/lib/session-registry.mjs +9 -1
- package/scripts/lib/spiral-carryover.mjs +23 -2
- package/scripts/lib/state-md/mission-status.mjs +164 -58
- package/scripts/lib/tmux-layout/vcs-detector.mjs +108 -4
- package/scripts/lib/validate/check-agents.mjs +77 -5
- package/scripts/lib/validate/check-auq-clarity.mjs +274 -0
- package/scripts/lib/validate/check-commands.mjs +2 -20
- package/scripts/lib/validate/check-doc-cli-commands.mjs +514 -0
- package/scripts/lib/validate/check-hooks-symmetry.mjs +48 -0
- package/scripts/lib/validate/check-owner-leakage.mjs +185 -17
- package/scripts/lib/validate/check-rules.mjs +153 -9
- package/scripts/lib/validate/check-skills.mjs +191 -0
- package/scripts/lib/validate/check-test-git-config-target.mjs +665 -0
- package/scripts/lib/validate/check-unicode-safety.mjs +22 -2
- package/scripts/lib/validate/check-untracked-test-deps.mjs +925 -0
- package/scripts/lib/validate/check-unwired-features.mjs +219 -11
- package/scripts/lib/validate/check-vcs-repo-flag.mjs +965 -0
- package/scripts/lib/validate/frontmatter-block.mjs +61 -0
- package/scripts/lib/validate/tier-inference.mjs +46 -8
- package/scripts/lib/vault-backfill/glab.mjs +91 -58
- package/scripts/lib/vault-backfill/manifest.mjs +28 -8
- package/scripts/lib/vault-mirror/namespace.mjs +146 -1
- package/scripts/lib/vault-mirror/process.mjs +264 -31
- package/scripts/lib/vault-mirror/render-sessions.mjs +115 -4
- package/scripts/lib/vault-status/board-writer.mjs +300 -56
- package/scripts/lib/vault-status/narrative-mirror.mjs +119 -5
- package/scripts/lib/vcs-repo-spec.mjs +680 -30
- package/scripts/lib/wave-resource-gate.mjs +67 -73
- package/scripts/materialize-wave-scope.mjs +281 -0
- package/scripts/print-learnings-index.mjs +30 -3
- package/scripts/release.mjs +983 -107
- package/scripts/run-quality-gate.mjs +14 -0
- package/scripts/site-numbers.mjs +1049 -0
- package/scripts/validate-plugin.mjs +64 -0
- package/scripts/validate-wave-scope.mjs +286 -12
- package/scripts/vault-backfill.mjs +32 -5
- package/scripts/vault-mirror.mjs +26 -1
- package/skills/_shared/monitor-patterns.md +24 -4
- package/skills/_shared/parallel-aware-auq.md +30 -24
- package/skills/_shared/parallel-aware-preamble.md +31 -2
- package/skills/_shared/state-ownership.md +49 -6
- package/skills/bootstrap/SKILL.md +2 -1
- package/skills/brainstorm/SKILL.md +18 -18
- package/skills/brainstorm/soul.md +12 -0
- package/skills/claude-md-drift-check/SKILL.md +9 -1
- package/skills/debug/SKILL.md +4 -1
- package/skills/discovery/SKILL.md +28 -24
- package/skills/discovery/issue-templates.md +4 -4
- package/skills/discovery/probes-code.md +2 -2
- package/skills/discovery/probes-feature.md +6 -6
- package/skills/discovery/probes-infra.md +2 -2
- package/skills/discovery/probes-session.md +5 -5
- package/skills/dispatcher/SKILL.md +10 -1
- package/skills/eli5/SKILL.md +43 -0
- package/skills/evolve/SKILL.md +8 -9
- package/skills/frontmatter-guard/SKILL.md +9 -1
- package/skills/gitlab-ops/SKILL.md +73 -59
- package/skills/gitlab-portfolio/SKILL.md +10 -1
- package/skills/grill/SKILL.md +6 -6
- package/skills/grill/soul.md +16 -0
- package/skills/memory-cleanup/SKILL.md +20 -7
- package/skills/npm-publish/SKILL.md +23 -51
- package/skills/peekaboo-driver/SKILL.md +3 -3
- package/skills/persona-panel/SKILL.md +3 -1
- package/skills/plan/SKILL.md +18 -16
- package/skills/plan/mode-feature.md +1 -1
- package/skills/plan/mode-new.md +42 -12
- package/skills/plan/soul.md +12 -0
- package/skills/reconcile/SKILL.md +3 -3
- package/skills/repo-audit/SKILL.md +10 -1
- package/skills/session-end/SKILL.md +97 -22
- package/skills/session-end/metrics-collection.md +1 -1
- package/skills/session-end/phase-3-6-tail.md +37 -2
- package/skills/session-end/session-metrics-write.md +4 -10
- package/skills/session-plan/SKILL.md +2 -2
- package/skills/session-plan/wave-template.md +1 -1
- package/skills/session-start/SKILL.md +82 -36
- package/skills/session-start/phase-2-5-docs-planning.md +8 -8
- package/skills/session-start/phase-4-5-resource-health.md +82 -19
- package/skills/session-start/soul.md +110 -0
- package/skills/spinout/SKILL.md +5 -1
- package/skills/sunset-review/SKILL.md +11 -1
- package/skills/test-runner/SKILL.md +2 -2
- package/skills/tmux-layout/SKILL.md +7 -2
- package/skills/using-orchestrator/SKILL.md +1 -1
- package/skills/vault-mirror/SKILL.md +10 -1
- package/skills/vault-sync/SKILL.md +10 -1
- package/skills/vault-sync/validator.mjs +55 -6
- package/skills/wave-executor/wave-loop.md +64 -12
- package/skills/write-executable-plan/SKILL.md +6 -6
- package/scripts/lib/mission-status-schema.mjs +0 -114
- package/scripts/tests/fixtures/fetch-baseline/sample-rule.md +0 -8
- package/skills/vault-sync/tests/fixtures/archive-test-vault/90-archive/bad-archived.md +0 -8
- package/skills/vault-sync/tests/fixtures/archive-test-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/archive-test-vault/live-note.md +0 -8
- package/skills/vault-sync/tests/fixtures/broken-frontmatter-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/broken-frontmatter-vault/bad-type.md +0 -8
- package/skills/vault-sync/tests/fixtures/broken-frontmatter-vault/good-note.md +0 -8
- package/skills/vault-sync/tests/fixtures/clean-vault/.obsidian/config.md +0 -8
- package/skills/vault-sync/tests/fixtures/clean-vault/01-projects/foo/projects-baseline.md +0 -10
- package/skills/vault-sync/tests/fixtures/clean-vault/03-daily/daily-2026-04-13.md +0 -8
- package/skills/vault-sync/tests/fixtures/clean-vault/README.md +0 -3
- package/skills/vault-sync/tests/fixtures/clean-vault/hello-world.md +0 -11
- package/skills/vault-sync/tests/fixtures/dangling-link-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/dangling-link-vault/has-dangling.md +0 -9
- package/skills/vault-sync/tests/fixtures/dangling-link-vault/real-target.md +0 -8
- package/skills/vault-sync/tests/fixtures/empty-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/missing-field-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/missing-field-vault/missing-id.md +0 -7
- package/skills/vault-sync/tests/fixtures/nested-tag-vault/03-daily/daily-2026-04-13.md +0 -9
- package/skills/vault-sync/tests/fixtures/nested-tag-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/nested-tag-vault/nested-tags-note.md +0 -11
- package/skills/vault-sync/tests/fixtures/no-frontmatter-vault/README.md +0 -3
- package/skills/vault-sync/tests/fixtures/no-frontmatter-vault/_MOC.md +0 -3
- package/skills/vault-sync/tests/fixtures/no-frontmatter-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/with-moc-vault/_MOC.md +0 -11
- package/skills/vault-sync/tests/fixtures/with-moc-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/with-moc-vault/hello-world.md +0 -11
- package/skills/vault-sync/tests/schema-drift.test.mjs +0 -133
package/hooks/on-stop.mjs
CHANGED
|
@@ -8,7 +8,10 @@
|
|
|
8
8
|
*
|
|
9
9
|
* Part of v3.0.0 Windows-native migration. Issue #141.
|
|
10
10
|
*
|
|
11
|
-
* Exit codes: 0 always (informational hooks must never block)
|
|
11
|
+
* Exit codes: 0 always (informational hooks must never block) — including when
|
|
12
|
+
* node_modules is absent: zx is imported lazily and a missing package degrades
|
|
13
|
+
* to one rate-limited stderr line instead of an ERR_MODULE_NOT_FOUND stack on
|
|
14
|
+
* every turn end (GH Kanevry/session-orchestrator#63).
|
|
12
15
|
*
|
|
13
16
|
* JSONL format (`.orchestrator/metrics/events.jsonl`) — emitted via the canonical
|
|
14
17
|
* `emitEvent()` so the JSONL record and the optional Clank webhook always carry the
|
|
@@ -18,8 +21,7 @@
|
|
|
18
21
|
*/
|
|
19
22
|
|
|
20
23
|
import path from 'node:path';
|
|
21
|
-
import { promises as fs } from 'node:fs';
|
|
22
|
-
import { $ } from 'zx';
|
|
24
|
+
import { promises as fs, statSync, writeFileSync } from 'node:fs';
|
|
23
25
|
|
|
24
26
|
import { shouldRunHook } from './_lib/profile-gate.mjs';
|
|
25
27
|
// #211: exit 0 immediately (silent allow) when this hook is disabled via profile/env
|
|
@@ -27,7 +29,7 @@ if (!shouldRunHook('on-stop')) process.exit(0);
|
|
|
27
29
|
|
|
28
30
|
import { emitEvent } from '../scripts/lib/events.mjs';
|
|
29
31
|
import { SO_PROJECT_DIR } from '../scripts/lib/platform.mjs';
|
|
30
|
-
import {
|
|
32
|
+
import { heartbeat, logSweepEvent } from '../scripts/lib/session-registry.mjs';
|
|
31
33
|
import { updateHeartbeat } from '../scripts/lib/session-lock.mjs';
|
|
32
34
|
|
|
33
35
|
// ---------------------------------------------------------------------------
|
|
@@ -87,17 +89,87 @@ function discriminate(input) {
|
|
|
87
89
|
return 'stop';
|
|
88
90
|
}
|
|
89
91
|
|
|
92
|
+
// ---------------------------------------------------------------------------
|
|
93
|
+
// dependency degradation (GH Kanevry/session-orchestrator#63)
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
//
|
|
96
|
+
// zx is loaded LAZILY. A static `import { $ } from 'zx'` fails at MODULE LOAD
|
|
97
|
+
// time when node_modules is absent (interrupted install, EPERM sandbox, half-
|
|
98
|
+
// synced plugin cache), so the harness prints a 10-frame ERR_MODULE_NOT_FOUND
|
|
99
|
+
// stack on EVERY turn end with no hint that `npm install` is the fix. This
|
|
100
|
+
// mirrors the missing-`node` degradation in hooks/run-node.sh (§5): one
|
|
101
|
+
// actionable stderr line per 6h window, then carry on with reduced features.
|
|
102
|
+
|
|
103
|
+
/** Rate-limit window for the dependencies-missing warning — mirrors run-node.sh's 6h TTL. */
|
|
104
|
+
const DEP_WARN_TTL_MS = 6 * 60 * 60 * 1000;
|
|
105
|
+
|
|
106
|
+
/** Plugin root (the directory that owns package.json / node_modules). */
|
|
107
|
+
const PLUGIN_ROOT = path.resolve(import.meta.dirname, '..');
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Marker path for the dependencies-missing warning. Deliberately a DIFFERENT
|
|
111
|
+
* name from run-node.sh's `session-orchestrator-node-missing-*`: sharing one
|
|
112
|
+
* marker would let a missing-node warning mask a missing-deps warning (and
|
|
113
|
+
* vice versa), leaving the operator with half a diagnostic.
|
|
114
|
+
* @returns {string}
|
|
115
|
+
*/
|
|
116
|
+
function depWarnMarkerPath() {
|
|
117
|
+
// `||` alone falls back only on falsy values — a whitespace-only TMPDIR is
|
|
118
|
+
// truthy and would yield a garbage path (see .claude/rules/development.md).
|
|
119
|
+
const tmpDir = (process.env.TMPDIR || '').trim() || '/tmp';
|
|
120
|
+
const user = (process.env.USER || '').trim() || 'uid';
|
|
121
|
+
return path.join(tmpDir, `session-orchestrator-deps-missing-${user}`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Print ONE actionable stderr line telling the operator to run `npm install`,
|
|
126
|
+
* at most once per DEP_WARN_TTL_MS. Marker mtime is the clock, exactly like
|
|
127
|
+
* run-node.sh's `find -mmin +360` check. Best-effort throughout: a marker we
|
|
128
|
+
* cannot stat is treated as expired (warn), a marker we cannot write means the
|
|
129
|
+
* next invocation warns again — noisier, never silent-broken.
|
|
130
|
+
*/
|
|
131
|
+
function warnDependenciesMissingOnce() {
|
|
132
|
+
const marker = depWarnMarkerPath();
|
|
133
|
+
try {
|
|
134
|
+
if (Date.now() - statSync(marker).mtimeMs < DEP_WARN_TTL_MS) return;
|
|
135
|
+
} catch { /* missing / unreadable marker → treat as expired */ }
|
|
136
|
+
try { writeFileSync(marker, ''); } catch { /* best-effort */ }
|
|
137
|
+
process.stderr.write(
|
|
138
|
+
`session-orchestrator: dependencies missing — run 'npm install' in ${PLUGIN_ROOT}. `
|
|
139
|
+
+ 'Hook features degraded (this warning is rate-limited to once per 6h).\n',
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Load zx's `$` lazily. Returns null when the package is not installed (after
|
|
145
|
+
* emitting the rate-limited advisory). Any OTHER import failure is re-thrown —
|
|
146
|
+
* a corrupt zx install is not a missing-dependency problem and must not be
|
|
147
|
+
* mislabelled as one.
|
|
148
|
+
* @returns {Promise<Function|null>}
|
|
149
|
+
*/
|
|
150
|
+
async function loadZx() {
|
|
151
|
+
try {
|
|
152
|
+
return (await import('zx')).$;
|
|
153
|
+
} catch (err) {
|
|
154
|
+
if (err?.code !== 'ERR_MODULE_NOT_FOUND') throw err;
|
|
155
|
+
warnDependenciesMissingOnce();
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
90
160
|
// ---------------------------------------------------------------------------
|
|
91
161
|
// git helpers
|
|
92
162
|
// ---------------------------------------------------------------------------
|
|
93
163
|
|
|
94
164
|
/**
|
|
95
165
|
* Returns { commit, branch } from the git repo at projectRoot, or null values
|
|
96
|
-
* if git is unavailable or the directory is not a git repo.
|
|
166
|
+
* if git is unavailable, zx is not installed, or the directory is not a git repo.
|
|
97
167
|
* @param {string} projectRoot — working directory for git commands
|
|
98
168
|
* @returns {Promise<{commit:string|null, branch:string|null}>}
|
|
99
169
|
*/
|
|
100
170
|
async function gitInfo(projectRoot) {
|
|
171
|
+
const $ = await loadZx();
|
|
172
|
+
if ($ === null) return { commit: null, branch: null };
|
|
101
173
|
$.verbose = false;
|
|
102
174
|
$.quiet = true;
|
|
103
175
|
const opts = projectRoot ? { cwd: projectRoot } : {};
|
|
@@ -180,16 +252,59 @@ async function handleStop(input) {
|
|
|
180
252
|
|
|
181
253
|
const sessionId = await resolveSessionId(input, projectRoot);
|
|
182
254
|
|
|
183
|
-
// v3.1.0 multi-session registry (#169) —
|
|
184
|
-
// entry
|
|
185
|
-
//
|
|
255
|
+
// v3.1.0 multi-session registry (#169), corrected in #1047 — REFRESH the
|
|
256
|
+
// registry entry here; never remove it.
|
|
257
|
+
//
|
|
258
|
+
// Stop fires at TURN end, not session end (see the file docblock). The
|
|
259
|
+
// original #169 wiring called deregisterSelf() here, so every assistant turn
|
|
260
|
+
// deleted this session's registry entry while the session was still live:
|
|
261
|
+
// measured on this host as 1 surviving entry (dead PID) against 12 live
|
|
262
|
+
// sockets, with sweep.log recording deletions of sessions aged 72/335/351/369
|
|
263
|
+
// minutes. Epic #583 fixed exactly this class for `.orchestrator/session.lock`
|
|
264
|
+
// (release → updateHeartbeat, below); the host registry never got the same
|
|
265
|
+
// correction. Deregistration now lives in hooks/on-session-end.mjs, which
|
|
266
|
+
// fires at the real end of the session.
|
|
267
|
+
//
|
|
268
|
+
// CODEX CAVEAT — stated here because THIS file runs on Codex and the file
|
|
269
|
+
// that owns deregistration does not. `hooks-codex.json` wires SessionStart +
|
|
270
|
+
// Stop but no SessionEnd (the Codex contract rejects the event), so on that
|
|
271
|
+
// bridge a session registers and never deregisters: its entry persists until
|
|
272
|
+
// sweepZombies() reaps it at the next SessionStart, up to `thresholdMin`
|
|
273
|
+
// (default 60 min). Accepted deliberately — it is the same path crash and
|
|
274
|
+
// Ctrl-C already take on every platform, and it is safe precisely BECAUSE
|
|
275
|
+
// the heartbeat below now advances, so a live session never ages into the
|
|
276
|
+
// sweep. Do NOT add a platform-detecting deregister branch here; that is the
|
|
277
|
+
// two-teardown-paths shape Epic #583 removed from the lock.
|
|
278
|
+
// (pi is unaffected: hooks-pi.json maps session_shutdown to on-session-end.mjs.
|
|
279
|
+
// Cursor is unaffected: it wires no SessionStart, so it never registers.)
|
|
280
|
+
//
|
|
281
|
+
// Failures are logged to sweep.log for observability but never re-thrown
|
|
282
|
+
// (hook must remain silent and non-blocking).
|
|
186
283
|
if (sessionId) {
|
|
187
284
|
try {
|
|
188
|
-
|
|
285
|
+
// heartbeat() returns null when no entry exists — a SILENT no-op that
|
|
286
|
+
// would otherwise make the loss permanent for the rest of the session
|
|
287
|
+
// (e.g. after a zombie sweep, or a harness UUID rotation with no fresh
|
|
288
|
+
// SessionStart). We do NOT re-register here: this hook has no access to
|
|
289
|
+
// the entry's platform / mode / host_class, and a re-registration would
|
|
290
|
+
// reset started_at to now — fabricating a session age instead of
|
|
291
|
+
// reporting one. Emit an observability breadcrumb instead, so the miss
|
|
292
|
+
// is visible in sweep.log rather than invisible. One line per turn while
|
|
293
|
+
// the entry is absent; that volume IS the signal, and the next
|
|
294
|
+
// SessionStart's registerSelf() ends it.
|
|
295
|
+
const refreshed = await heartbeat(sessionId);
|
|
296
|
+
if (refreshed === null) {
|
|
297
|
+
logSweepEvent({
|
|
298
|
+
event: 'heartbeat-missing',
|
|
299
|
+
session_id: sessionId,
|
|
300
|
+
error: 'no registry entry to refresh at turn end',
|
|
301
|
+
});
|
|
302
|
+
}
|
|
189
303
|
} catch (err) {
|
|
190
|
-
//
|
|
191
|
-
// Do NOT throw, do NOT write to stderr: the hook is
|
|
192
|
-
|
|
304
|
+
// Refresh failed at the fs layer — emit an observability breadcrumb to
|
|
305
|
+
// sweep.log. Do NOT throw, do NOT write to stderr: the hook is
|
|
306
|
+
// informational-only.
|
|
307
|
+
logSweepEvent({ event: 'heartbeat-failed', session_id: sessionId, error: err?.message ?? String(err) });
|
|
193
308
|
}
|
|
194
309
|
|
|
195
310
|
// Epic #583 W5-F1c — refresh session.lock heartbeat on every turn-end.
|
|
@@ -154,6 +154,13 @@ import { readFileSync, mkdirSync, writeFileSync, renameSync, realpathSync, statS
|
|
|
154
154
|
import { readStdin, writeStdoutLineSync } from '../scripts/lib/io.mjs';
|
|
155
155
|
import { resolveProjectDir } from '../scripts/lib/platform.mjs';
|
|
156
156
|
import { findScopeFile, pathMatchesPattern } from '../scripts/lib/hardening.mjs';
|
|
157
|
+
// #1057 — `sessionAgeMs` and its private `clockAgeMs` helper MOVED to the lib so
|
|
158
|
+
// hooks/enforce-scope.mjs can read the same session clock without a hook->hook
|
|
159
|
+
// import. Re-exported below, so this hook's public surface — and
|
|
160
|
+
// tests/hooks/post-bash-write-verify.test.mjs, which imports the named export —
|
|
161
|
+
// is unchanged. Two byte-identical copies of a clock is exactly the one-fact-two-
|
|
162
|
+
// copies class this repo keeps paying for.
|
|
163
|
+
import { sessionAgeMs } from '../scripts/lib/scope-gate.mjs';
|
|
157
164
|
|
|
158
165
|
// ---------------------------------------------------------------------------
|
|
159
166
|
// Constants
|
|
@@ -538,30 +545,6 @@ function readDirtyPaths(repoRoot) {
|
|
|
538
545
|
}
|
|
539
546
|
}
|
|
540
547
|
|
|
541
|
-
/**
|
|
542
|
-
* Age in ms of one session clock: a JSON file carrying an ISO start timestamp.
|
|
543
|
-
*
|
|
544
|
-
* Never throws. Absent / unparseable / non-string / non-ISO ⇒ null. A NEGATIVE
|
|
545
|
-
* age (timestamp in the future) is also null rather than a negative number —
|
|
546
|
-
* see `sessionAgeMs` for why that matters once two clocks are combined.
|
|
547
|
-
*
|
|
548
|
-
* @param {string} file absolute path to the JSON file
|
|
549
|
-
* @param {string} field name of the ISO-timestamp property
|
|
550
|
-
* @param {number} now
|
|
551
|
-
* @returns {number|null}
|
|
552
|
-
*/
|
|
553
|
-
function clockAgeMs(file, field, now) {
|
|
554
|
-
try {
|
|
555
|
-
const parsed = JSON.parse(readFileSync(file, 'utf8'));
|
|
556
|
-
const startedAt = Date.parse(parsed?.[field]);
|
|
557
|
-
if (!Number.isFinite(startedAt)) return null;
|
|
558
|
-
const age = now - startedAt;
|
|
559
|
-
return age >= 0 ? age : null;
|
|
560
|
-
} catch {
|
|
561
|
-
return null;
|
|
562
|
-
}
|
|
563
|
-
}
|
|
564
|
-
|
|
565
548
|
/**
|
|
566
549
|
* Milliseconds since the current session started, or null when unknown — the
|
|
567
550
|
* MINIMUM of two independently-written session clocks (#957 finding 2):
|
|
@@ -634,14 +617,7 @@ function clockAgeMs(file, field, now) {
|
|
|
634
617
|
* @param {number} [now]
|
|
635
618
|
* @returns {number|null}
|
|
636
619
|
*/
|
|
637
|
-
export
|
|
638
|
-
const dir = path.join(repoRoot, '.orchestrator');
|
|
639
|
-
const ages = [
|
|
640
|
-
clockAgeMs(path.join(dir, 'current-session.json'), 'timestamp', now),
|
|
641
|
-
clockAgeMs(path.join(dir, 'session.lock'), 'started_at', now),
|
|
642
|
-
].filter((age) => age !== null);
|
|
643
|
-
return ages.length > 0 ? Math.min(...ages) : null;
|
|
644
|
-
}
|
|
620
|
+
export { sessionAgeMs };
|
|
645
621
|
|
|
646
622
|
/** @returns {object|null} */
|
|
647
623
|
function readSnapshot(file) {
|