@deftai/directive-core 0.104.0 → 0.106.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/dist/authz/classify.js +1108 -67
- package/dist/check/cached-orchestrator.d.ts +1 -1
- package/dist/check/cached-orchestrator.js +2 -2
- package/dist/check/named-cause.d.ts +1 -1
- package/dist/check/named-cause.js +34 -4
- package/dist/codebase/default-extractor.d.ts +5 -0
- package/dist/codebase/default-extractor.js +7 -15
- package/dist/deposit/copy-tree.js +60 -0
- package/dist/deposit/python-free.js +26 -18
- package/dist/doctor/checks.d.ts +8 -0
- package/dist/doctor/checks.js +37 -25
- package/dist/doctor/doctor-state.d.ts +4 -0
- package/dist/doctor/doctor-state.js +7 -1
- package/dist/doctor/main.js +9 -19
- package/dist/doctor/types.d.ts +9 -0
- package/dist/forward-coverage/diff-coverage.d.ts +39 -0
- package/dist/forward-coverage/diff-coverage.js +112 -0
- package/dist/forward-coverage/diff-lines.d.ts +19 -0
- package/dist/forward-coverage/diff-lines.js +173 -0
- package/dist/forward-coverage/evaluate.d.ts +24 -9
- package/dist/forward-coverage/evaluate.js +125 -19
- package/dist/fs/contained-write.d.ts +115 -1
- package/dist/fs/contained-write.js +241 -5
- package/dist/fs/mutation-ledger.d.ts +54 -0
- package/dist/fs/mutation-ledger.js +133 -0
- package/dist/fs/non-product-dirs.d.ts +79 -0
- package/dist/fs/non-product-dirs.js +96 -0
- package/dist/hooks/dispatcher.d.ts +3 -1
- package/dist/hooks/dispatcher.js +14 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -0
- package/dist/init-deposit/agent-hooks.d.ts +1 -1
- package/dist/init-deposit/agent-hooks.js +18 -16
- package/dist/init-deposit/core-guard-pin-content.d.ts +7 -0
- package/dist/init-deposit/core-guard-pin-content.js +393 -0
- package/dist/init-deposit/gitignore.js +9 -0
- package/dist/init-deposit/hygiene.d.ts +47 -11
- package/dist/init-deposit/hygiene.js +172 -37
- package/dist/init-deposit/legacy-detect.js +6 -2
- package/dist/init-deposit/refresh.d.ts +21 -1
- package/dist/init-deposit/refresh.js +106 -9
- package/dist/init-deposit/scaffold.js +28 -279
- package/dist/init-deposit/slash-deposit.js +13 -7
- package/dist/init-deposit/xbrief-projections.d.ts +8 -1
- package/dist/init-deposit/xbrief-projections.js +24 -11
- package/dist/intake/clause-derivation.d.ts +47 -1
- package/dist/intake/clause-derivation.js +238 -9
- package/dist/intake/issue-ingest.d.ts +3 -1
- package/dist/intake/issue-ingest.js +35 -6
- package/dist/lifecycle/completed-tracked-on-delivery.d.ts +5 -0
- package/dist/lifecycle/completed-tracked-on-delivery.js +85 -18
- package/dist/lifecycle-visible/evaluate.d.ts +98 -0
- package/dist/lifecycle-visible/evaluate.js +710 -0
- package/dist/lifecycle-visible/index.d.ts +2 -0
- package/dist/lifecycle-visible/index.js +2 -0
- package/dist/literal-acceptance/capture.d.ts +10 -0
- package/dist/literal-acceptance/capture.js +226 -77
- package/dist/literal-acceptance/evaluate.d.ts +29 -4
- package/dist/literal-acceptance/evaluate.js +70 -2
- package/dist/literal-acceptance/index.d.ts +3 -3
- package/dist/literal-acceptance/index.js +3 -3
- package/dist/literal-acceptance/safety.d.ts +41 -0
- package/dist/literal-acceptance/safety.js +126 -2
- package/dist/literal-acceptance/types.d.ts +8 -0
- package/dist/orchestration/judgment-policy.d.ts +13 -0
- package/dist/orchestration/judgment-policy.js +15 -0
- package/dist/orchestration/probe-session.js +13 -8
- package/dist/orchestration/verify-judgment-gates.d.ts +4 -0
- package/dist/orchestration/verify-judgment-gates.js +43 -11
- package/dist/orphan-active/evaluate.d.ts +7 -1
- package/dist/orphan-active/evaluate.js +102 -23
- package/dist/platform/platform-capabilities.js +2 -3
- package/dist/policy/base-branch.d.ts +37 -0
- package/dist/policy/base-branch.js +114 -0
- package/dist/policy/branch-sync.d.ts +80 -0
- package/dist/policy/branch-sync.js +211 -0
- package/dist/policy/ceremony-dial-escalation.js +1 -0
- package/dist/policy/ceremony-dial.js +11 -21
- package/dist/policy/delivery-branch.d.ts +5 -3
- package/dist/policy/delivery-branch.js +7 -3
- package/dist/policy/file-size-thresholds.d.ts +24 -0
- package/dist/policy/file-size-thresholds.js +23 -0
- package/dist/policy/forge-outage-retry.d.ts +47 -0
- package/dist/policy/forge-outage-retry.js +157 -0
- package/dist/policy/host-hooks.d.ts +20 -0
- package/dist/policy/host-hooks.js +121 -2
- package/dist/policy/index.d.ts +7 -0
- package/dist/policy/index.js +66 -7
- package/dist/policy/org-force-on-migration.js +2 -1
- package/dist/policy/plan-extensions.d.ts +1 -1
- package/dist/policy/plan-extensions.js +2 -0
- package/dist/policy/product-signal.js +6 -7
- package/dist/policy/project-invariants.d.ts +50 -0
- package/dist/policy/project-invariants.js +205 -0
- package/dist/policy/require-human-merge.js +8 -6
- package/dist/policy/resolve.d.ts +7 -2
- package/dist/policy/resolve.js +25 -8
- package/dist/policy/sync-default.d.ts +129 -0
- package/dist/policy/sync-default.js +452 -0
- package/dist/policy/sync-max-files.d.ts +93 -0
- package/dist/policy/sync-max-files.js +192 -0
- package/dist/policy/value-feedback.js +7 -8
- package/dist/pr-monitor/absent-required.d.ts +11 -0
- package/dist/pr-monitor/absent-required.js +61 -0
- package/dist/pr-monitor/constants.d.ts +7 -0
- package/dist/pr-monitor/constants.js +7 -0
- package/dist/pr-monitor/index.d.ts +2 -1
- package/dist/pr-monitor/index.js +2 -1
- package/dist/pr-monitor/main.js +4 -0
- package/dist/pr-monitor/monitor.js +24 -1
- package/dist/pr-wait-mergeable/classify.js +6 -0
- package/dist/preflight/evaluate.d.ts +4 -0
- package/dist/preflight/evaluate.js +35 -1
- package/dist/preflight/index.d.ts +4 -0
- package/dist/preflight/index.js +2 -0
- package/dist/preflight/intended-placement.d.ts +62 -0
- package/dist/preflight/intended-placement.js +275 -0
- package/dist/preflight/project-invariants-gate.d.ts +29 -0
- package/dist/preflight/project-invariants-gate.js +82 -0
- package/dist/preflight-cache/evaluate.d.ts +12 -0
- package/dist/preflight-cache/evaluate.js +15 -1
- package/dist/preflight-cache/index.d.ts +1 -1
- package/dist/preflight-cache/index.js +1 -1
- package/dist/product-first-done-gate/acceptance-resolver.d.ts +128 -0
- package/dist/product-first-done-gate/acceptance-resolver.js +144 -0
- package/dist/product-first-done-gate/acceptance.d.ts +7 -4
- package/dist/product-first-done-gate/acceptance.js +79 -8
- package/dist/product-first-done-gate/empty-resolution.d.ts +4 -1
- package/dist/product-first-done-gate/empty-resolution.js +12 -1
- package/dist/product-first-done-gate/evaluate.d.ts +16 -0
- package/dist/product-first-done-gate/evaluate.js +298 -34
- package/dist/product-first-done-gate/index.d.ts +2 -0
- package/dist/product-first-done-gate/index.js +1 -0
- package/dist/release/build-dist.d.ts +101 -0
- package/dist/release/build-dist.js +428 -46
- package/dist/release/closed-verb-gate.d.ts +12 -0
- package/dist/release/closed-verb-gate.js +51 -0
- package/dist/release/index.d.ts +1 -0
- package/dist/release/index.js +1 -0
- package/dist/release/pipeline.js +7 -0
- package/dist/release/types.d.ts +12 -0
- package/dist/render/constants.d.ts +2 -1
- package/dist/render/constants.js +2 -1
- package/dist/render/framework-commands.js +4 -0
- package/dist/render/spec-validate.js +23 -12
- package/dist/run-summary/emit.d.ts +16 -10
- package/dist/run-summary/emit.js +26 -15
- package/dist/run-summary/index.d.ts +3 -2
- package/dist/run-summary/index.js +2 -1
- package/dist/run-summary/session-id.d.ts +21 -0
- package/dist/run-summary/session-id.js +92 -0
- package/dist/run-summary/types.d.ts +33 -4
- package/dist/run-summary/types.js +1 -0
- package/dist/scope/acceptance-evidence.d.ts +19 -7
- package/dist/scope/acceptance-evidence.js +38 -11
- package/dist/scope/delivery-evidence.d.ts +3 -3
- package/dist/scope/delivery-evidence.js +22 -27
- package/dist/scope/index.d.ts +1 -0
- package/dist/scope/index.js +1 -0
- package/dist/scope/main.js +15 -5
- package/dist/scope/parent-lineage.js +7 -6
- package/dist/scope/project-invariant-coverage.d.ts +54 -0
- package/dist/scope/project-invariant-coverage.js +322 -0
- package/dist/scope/promote-from-issue.js +5 -5
- package/dist/scope/transition.js +1 -1
- package/dist/scope-provenance/compare-intent.d.ts +27 -0
- package/dist/scope-provenance/compare-intent.js +233 -0
- package/dist/scope-provenance/digest.d.ts +18 -1
- package/dist/scope-provenance/digest.js +16 -11
- package/dist/scope-provenance/evaluate.d.ts +5 -1
- package/dist/scope-provenance/evaluate.js +67 -7
- package/dist/scope-provenance/extract-intent.d.ts +43 -0
- package/dist/scope-provenance/extract-intent.js +262 -0
- package/dist/scope-provenance/index.d.ts +7 -1
- package/dist/scope-provenance/index.js +7 -1
- package/dist/scope-provenance/intent-digest.d.ts +12 -0
- package/dist/scope-provenance/intent-digest.js +33 -0
- package/dist/scope-provenance/intent-evaluate.d.ts +43 -0
- package/dist/scope-provenance/intent-evaluate.js +149 -0
- package/dist/scope-provenance/json-tokenizer.d.ts +19 -0
- package/dist/scope-provenance/json-tokenizer.js +225 -0
- package/dist/scope-provenance/known-machine.d.ts +38 -0
- package/dist/scope-provenance/known-machine.js +157 -0
- package/dist/scope-provenance/mint-artifacts.d.ts +102 -0
- package/dist/scope-provenance/mint-artifacts.js +425 -0
- package/dist/session/ac-pass-banking.d.ts +9 -36
- package/dist/session/ac-pass-banking.js +33 -79
- package/dist/session/ac-pass-reuse.d.ts +53 -0
- package/dist/session/ac-pass-reuse.js +95 -0
- package/dist/session/git.js +19 -4
- package/dist/session/index.d.ts +4 -0
- package/dist/session/index.js +4 -0
- package/dist/session/occupancy-stress-worker.d.ts +7 -0
- package/dist/session/occupancy-stress-worker.js +36 -0
- package/dist/session/occupancy.d.ts +97 -0
- package/dist/session/occupancy.js +422 -0
- package/dist/session/openclaw-soft-rebind-deposit.js +6 -6
- package/dist/session/process-cost.d.ts +32 -10
- package/dist/session/process-cost.js +189 -1
- package/dist/session/product-state-hash.d.ts +26 -0
- package/dist/session/product-state-hash.js +700 -0
- package/dist/session/ritual-entrypoint.js +16 -1
- package/dist/session/session-ready.d.ts +2 -0
- package/dist/session/session-ready.js +52 -30
- package/dist/session/session-start.d.ts +13 -0
- package/dist/session/session-start.js +103 -9
- package/dist/session/verify-ac-session-cache.d.ts +42 -0
- package/dist/session/verify-ac-session-cache.js +126 -0
- package/dist/session/verify-session-ritual.d.ts +28 -0
- package/dist/session/verify-session-ritual.js +97 -5
- package/dist/slash/openclaw-deposit.js +13 -9
- package/dist/slice/constants.d.ts +2 -2
- package/dist/slice/constants.js +2 -2
- package/dist/slice/existing.js +9 -3
- package/dist/slice/lock.d.ts +16 -1
- package/dist/slice/lock.js +137 -11
- package/dist/story-ready/evaluate.d.ts +2 -0
- package/dist/story-ready/evaluate.js +13 -0
- package/dist/swarm/complete-cohort.d.ts +3 -0
- package/dist/swarm/complete-cohort.js +59 -11
- package/dist/swarm/finalize-cohort-cli.d.ts +7 -2
- package/dist/swarm/finalize-cohort-cli.js +165 -35
- package/dist/swarm/finalize-cohort.d.ts +1 -0
- package/dist/swarm/finalize-cohort.js +47 -4
- package/dist/swarm/launch.d.ts +22 -0
- package/dist/swarm/launch.js +155 -0
- package/dist/swarm/routing-set-cli.d.ts +11 -1
- package/dist/swarm/routing-set-cli.js +15 -3
- package/dist/swarm/routing-verify.d.ts +13 -1
- package/dist/swarm/routing-verify.js +60 -21
- package/dist/swarm/routing.d.ts +14 -0
- package/dist/swarm/routing.js +53 -3
- package/dist/telemetry-coverage/fake-trial.js +10 -0
- package/dist/telemetry-coverage/kinds.js +2 -0
- package/dist/triage/actions/index.js +4 -1
- package/dist/triage/bootstrap/index.js +6 -6
- package/dist/triage/help/registry-data.d.ts +1 -1
- package/dist/triage/help/registry-data.js +1 -1
- package/dist/triage/welcome/writers.d.ts +1 -1
- package/dist/triage/welcome/writers.js +10 -5
- package/dist/ts-check-lane/index.d.ts +3 -0
- package/dist/ts-check-lane/index.js +2 -0
- package/dist/ts-check-lane/progress-reporter.d.ts +17 -0
- package/dist/ts-check-lane/progress-reporter.js +38 -0
- package/dist/ts-check-lane/progress.d.ts +32 -0
- package/dist/ts-check-lane/progress.js +57 -0
- package/dist/ts-check-lane/run-lane.d.ts +3 -1
- package/dist/ts-check-lane/run-lane.js +8 -6
- package/dist/validate-content/validate-links.d.ts +8 -0
- package/dist/validate-content/validate-links.js +9 -15
- package/dist/value/feedback-file.js +12 -2
- package/dist/value/readback.d.ts +4 -1
- package/dist/value/readback.js +51 -12
- package/dist/vbrief-build/index.d.ts +1 -0
- package/dist/vbrief-build/index.js +1 -0
- package/dist/vbrief-build/project-invariants-io.d.ts +16 -0
- package/dist/vbrief-build/project-invariants-io.js +41 -0
- package/dist/vbrief-reconcile/graph.js +18 -1
- package/dist/vbrief-reconcile/index.d.ts +1 -1
- package/dist/vbrief-reconcile/index.js +1 -1
- package/dist/vbrief-reconcile/types.d.ts +13 -0
- package/dist/vbrief-reconcile/umbrellas.d.ts +14 -0
- package/dist/vbrief-reconcile/umbrellas.js +261 -26
- package/dist/vbrief-validate/index.d.ts +1 -1
- package/dist/vbrief-validate/index.js +1 -1
- package/dist/vbrief-validate/plan-hooks.d.ts +4 -0
- package/dist/vbrief-validate/plan-hooks.js +68 -0
- package/dist/vbrief-validate/schema.d.ts +4 -0
- package/dist/vbrief-validate/schema.js +1 -1
- package/dist/verify-ac/evaluate.d.ts +7 -1
- package/dist/verify-ac/evaluate.js +34 -26
- package/dist/verify-ac/flag.d.ts +4 -0
- package/dist/verify-ac/flag.js +16 -0
- package/dist/verify-ac/index.d.ts +2 -2
- package/dist/verify-ac/index.js +2 -2
- package/dist/verify-env/agent-hook-readiness.d.ts +2 -2
- package/dist/verify-env/agent-hook-readiness.js +12 -8
- package/dist/verify-env/agent-hooks-live-probe.d.ts +5 -1
- package/dist/verify-env/agent-hooks-live-probe.js +26 -5
- package/dist/verify-env/agent-hooks.js +3 -4
- package/dist/verify-source/contained-writes.js +2 -0
- package/dist/verify-source/verify-stubs.d.ts +15 -1
- package/dist/verify-source/verify-stubs.js +20 -16
- package/dist/vitest-runner/win32-coverage-tmp-setup.d.ts +5 -6
- package/dist/vitest-runner/win32-coverage-tmp-setup.js +5 -6
- package/dist/xbrief-migrate/migrate-project.js +2 -5
- package/package.json +7 -3
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Worktree occupancy lease (#3433).
|
|
3
|
+
*
|
|
4
|
+
* Ritual-state is "this session completed ceremony." Occupancy is "who may
|
|
5
|
+
* mutate this tree right now." Those lifetimes differ; do not overload
|
|
6
|
+
* ritual-state.json. Join negotiation (`occupancy:request`) is out of scope.
|
|
7
|
+
*
|
|
8
|
+
* Concurrency model:
|
|
9
|
+
* - Assumptions: local filesystem; cooperating processes on one machine.
|
|
10
|
+
* - Guarantees: mutual exclusion under crash-free operation; detect-and-abort
|
|
11
|
+
* if the sidecar lock is compromised (fence before rename/unlink).
|
|
12
|
+
* - Non-goals: network filesystems; Byzantine processes; perfect off-Linux
|
|
13
|
+
* PID-reuse detection (hard age cap + fence instead).
|
|
14
|
+
*/
|
|
15
|
+
import { randomUUID } from "node:crypto";
|
|
16
|
+
import { existsSync, readFileSync, renameSync, rmSync } from "node:fs";
|
|
17
|
+
import { dirname, join, resolve } from "node:path";
|
|
18
|
+
import { containedRemove, containedWrite } from "../fs/contained-write.js";
|
|
19
|
+
import { assertWriteTargetSafe } from "../fs/projection-containment.js";
|
|
20
|
+
import { assertAppendLockOwned, withAppendLock } from "../slice/lock.js";
|
|
21
|
+
import { stableJson } from "./json.js";
|
|
22
|
+
import { parseTimestamp, timestampIso } from "./time.js";
|
|
23
|
+
export const OCCUPANCY_SCHEMA_VERSION = 1;
|
|
24
|
+
export const OCCUPANCY_RELPATH = [".deft", "occupancy.json"];
|
|
25
|
+
/** Crash recovery TTL: 20 minutes without heartbeat (15–30 window). */
|
|
26
|
+
export const OCCUPANCY_TTL_MS = 20 * 60 * 1000;
|
|
27
|
+
export const OCCUPANCY_INTENTS = ["mutation", "swarm", "review"];
|
|
28
|
+
export const OCCUPANCY_JOIN_PROTOCOLS = ["none", "heartbeat-file", "parent-message"];
|
|
29
|
+
export function occupancyPath(projectRoot) {
|
|
30
|
+
return join(resolve(projectRoot), ...OCCUPANCY_RELPATH);
|
|
31
|
+
}
|
|
32
|
+
export function heartbeatAgeSeconds(record, now = new Date()) {
|
|
33
|
+
return Math.max(0, Math.round((now.getTime() - record.heartbeatAt.getTime()) / 1000));
|
|
34
|
+
}
|
|
35
|
+
export function isOccupancyExpired(record, now = new Date(), ttlMs = OCCUPANCY_TTL_MS) {
|
|
36
|
+
return now.getTime() - record.heartbeatAt.getTime() > ttlMs;
|
|
37
|
+
}
|
|
38
|
+
export function formatOccupancyRemediation(record, now = new Date()) {
|
|
39
|
+
const age = heartbeatAgeSeconds(record, now);
|
|
40
|
+
return (`Worktree occupied by session ${record.sessionId} (intent=${record.intent}, heartbeat ${age}s ago).\n` +
|
|
41
|
+
"Stay read-only (`session:start --read-only`), use another worktree,\n" +
|
|
42
|
+
"queue a join (`occupancy:request`), or steal (`occupancy:steal --confirm`).");
|
|
43
|
+
}
|
|
44
|
+
export function resolveOccupancySessionId(input = {}) {
|
|
45
|
+
const explicit = input.sessionId?.trim();
|
|
46
|
+
if (explicit)
|
|
47
|
+
return explicit;
|
|
48
|
+
const envId = (input.env ?? process.env).DEFT_SESSION_ID?.trim();
|
|
49
|
+
if (envId)
|
|
50
|
+
return envId;
|
|
51
|
+
return (input.newSessionId ?? randomUUID)();
|
|
52
|
+
}
|
|
53
|
+
export function readOccupancy(projectRoot) {
|
|
54
|
+
const path = occupancyPath(projectRoot);
|
|
55
|
+
try {
|
|
56
|
+
if (!existsSync(path))
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
let payload;
|
|
63
|
+
try {
|
|
64
|
+
payload = JSON.parse(readFileSync(path, { encoding: "utf8" }));
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
return parseOccupancy(payload, resolve(projectRoot));
|
|
70
|
+
}
|
|
71
|
+
export function liveOccupant(projectRoot, now = new Date(), ttlMs = OCCUPANCY_TTL_MS) {
|
|
72
|
+
const record = readOccupancy(projectRoot);
|
|
73
|
+
if (record === null || isOccupancyExpired(record, now, ttlMs))
|
|
74
|
+
return null;
|
|
75
|
+
return record;
|
|
76
|
+
}
|
|
77
|
+
export function applyWorktreeOccupancy(projectRoot, input = {}) {
|
|
78
|
+
const now = input.now ?? new Date();
|
|
79
|
+
const path = occupancyPath(projectRoot);
|
|
80
|
+
const incoming = resolveOccupancySessionId(input);
|
|
81
|
+
const existing = readOccupancy(projectRoot);
|
|
82
|
+
const live = existing !== null && !isOccupancyExpired(existing, now) ? existing : null;
|
|
83
|
+
if (input.steal === true) {
|
|
84
|
+
return stealOccupancy(projectRoot, { ...input, sessionId: incoming, now });
|
|
85
|
+
}
|
|
86
|
+
if (live !== null && live.sessionId !== incoming) {
|
|
87
|
+
return {
|
|
88
|
+
action: "denied",
|
|
89
|
+
sessionId: incoming,
|
|
90
|
+
record: live,
|
|
91
|
+
path,
|
|
92
|
+
message: formatOccupancyRemediation(live, now),
|
|
93
|
+
code: 1,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
if (input.write === false) {
|
|
97
|
+
return {
|
|
98
|
+
action: live !== null ? "heartbeat" : "claimed",
|
|
99
|
+
sessionId: incoming,
|
|
100
|
+
record: live,
|
|
101
|
+
path,
|
|
102
|
+
message: live !== null
|
|
103
|
+
? `occupancy heartbeat session ${incoming} (intent=${live.intent})`
|
|
104
|
+
: `occupancy claimed session ${incoming} (intent=${input.intent ?? "mutation"})`,
|
|
105
|
+
code: 0,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
return withOccupancyLock(projectRoot, (fence) => {
|
|
109
|
+
const existingLocked = readOccupancy(projectRoot);
|
|
110
|
+
const liveLocked = existingLocked !== null && !isOccupancyExpired(existingLocked, now) ? existingLocked : null;
|
|
111
|
+
if (liveLocked !== null && liveLocked.sessionId !== incoming) {
|
|
112
|
+
return {
|
|
113
|
+
action: "denied",
|
|
114
|
+
sessionId: incoming,
|
|
115
|
+
record: liveLocked,
|
|
116
|
+
path,
|
|
117
|
+
message: formatOccupancyRemediation(liveLocked, now),
|
|
118
|
+
code: 1,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
const record = writeOccupancyRecord(projectRoot, {
|
|
122
|
+
sessionId: incoming,
|
|
123
|
+
worktreePath: resolve(projectRoot),
|
|
124
|
+
intent: input.intent ?? liveLocked?.intent ?? "mutation",
|
|
125
|
+
claimedAt: liveLocked?.claimedAt ?? now,
|
|
126
|
+
heartbeatAt: now,
|
|
127
|
+
host: input.host ?? liveLocked?.host ?? occupancyHost(input.env),
|
|
128
|
+
address: input.address ?? liveLocked?.address ?? occupancyAddress(input.env),
|
|
129
|
+
retainCapable: input.retainCapable ?? liveLocked?.retainCapable ?? false,
|
|
130
|
+
joinProtocol: input.joinProtocol ?? liveLocked?.joinProtocol ?? "none",
|
|
131
|
+
}, fence);
|
|
132
|
+
const action = liveLocked !== null ? "heartbeat" : "claimed";
|
|
133
|
+
return {
|
|
134
|
+
action,
|
|
135
|
+
sessionId: record.sessionId,
|
|
136
|
+
record,
|
|
137
|
+
path,
|
|
138
|
+
message: action === "heartbeat"
|
|
139
|
+
? `occupancy heartbeat session ${record.sessionId} (intent=${record.intent})`
|
|
140
|
+
: `occupancy claimed session ${record.sessionId} (intent=${record.intent})`,
|
|
141
|
+
code: 0,
|
|
142
|
+
};
|
|
143
|
+
}, input.lockDeps);
|
|
144
|
+
}
|
|
145
|
+
export function stealOccupancy(projectRoot, input = {}) {
|
|
146
|
+
const now = input.now ?? new Date();
|
|
147
|
+
const path = occupancyPath(projectRoot);
|
|
148
|
+
if (input.confirm !== true) {
|
|
149
|
+
return {
|
|
150
|
+
action: "denied",
|
|
151
|
+
sessionId: resolveOccupancySessionId(input),
|
|
152
|
+
record: readOccupancy(projectRoot),
|
|
153
|
+
path,
|
|
154
|
+
message: "occupancy:steal requires --confirm after naming the occupant.",
|
|
155
|
+
code: 2,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
const named = input.occupant?.trim() ?? "";
|
|
159
|
+
if (named.length === 0) {
|
|
160
|
+
return {
|
|
161
|
+
action: "denied",
|
|
162
|
+
sessionId: resolveOccupancySessionId(input),
|
|
163
|
+
record: readOccupancy(projectRoot),
|
|
164
|
+
path,
|
|
165
|
+
message: "occupancy:steal requires --occupant <session-id> to name the current occupant.",
|
|
166
|
+
code: 2,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
const existing = readOccupancy(projectRoot);
|
|
170
|
+
const live = existing !== null && !isOccupancyExpired(existing, now) ? existing : null;
|
|
171
|
+
if (live !== null && live.sessionId !== named) {
|
|
172
|
+
return {
|
|
173
|
+
action: "denied",
|
|
174
|
+
sessionId: resolveOccupancySessionId(input),
|
|
175
|
+
record: live,
|
|
176
|
+
path,
|
|
177
|
+
message: `occupancy:steal named occupant ${named} does not match live occupant ${live.sessionId}.\n` +
|
|
178
|
+
formatOccupancyRemediation(live, now),
|
|
179
|
+
code: 1,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
return withOccupancyLock(projectRoot, (fence) => {
|
|
183
|
+
const existingLocked = readOccupancy(projectRoot);
|
|
184
|
+
const liveLocked = existingLocked !== null && !isOccupancyExpired(existingLocked, now) ? existingLocked : null;
|
|
185
|
+
if (liveLocked !== null && liveLocked.sessionId !== named) {
|
|
186
|
+
return {
|
|
187
|
+
action: "denied",
|
|
188
|
+
sessionId: resolveOccupancySessionId(input),
|
|
189
|
+
record: liveLocked,
|
|
190
|
+
path,
|
|
191
|
+
message: `occupancy:steal named occupant ${named} does not match live occupant ${liveLocked.sessionId}.\n` +
|
|
192
|
+
formatOccupancyRemediation(liveLocked, now),
|
|
193
|
+
code: 1,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
const incoming = resolveOccupancySessionId(input);
|
|
197
|
+
const record = writeOccupancyRecord(projectRoot, {
|
|
198
|
+
sessionId: incoming,
|
|
199
|
+
worktreePath: resolve(projectRoot),
|
|
200
|
+
intent: input.intent ?? "mutation",
|
|
201
|
+
claimedAt: now,
|
|
202
|
+
heartbeatAt: now,
|
|
203
|
+
host: input.host ?? occupancyHost(input.env),
|
|
204
|
+
address: input.address ?? occupancyAddress(input.env),
|
|
205
|
+
retainCapable: input.retainCapable ?? false,
|
|
206
|
+
joinProtocol: input.joinProtocol ?? "none",
|
|
207
|
+
}, fence);
|
|
208
|
+
return {
|
|
209
|
+
action: "stolen",
|
|
210
|
+
sessionId: record.sessionId,
|
|
211
|
+
record,
|
|
212
|
+
path,
|
|
213
|
+
message: `occupancy stolen from ${named}; writer is now session ${record.sessionId}`,
|
|
214
|
+
code: 0,
|
|
215
|
+
};
|
|
216
|
+
}, input.lockDeps);
|
|
217
|
+
}
|
|
218
|
+
export function releaseOccupancy(projectRoot, input = {}) {
|
|
219
|
+
const now = input.now ?? new Date();
|
|
220
|
+
const path = occupancyPath(projectRoot);
|
|
221
|
+
const caller = input.sessionId?.trim() || (input.env ?? process.env).DEFT_SESSION_ID?.trim() || "";
|
|
222
|
+
return withOccupancyLock(projectRoot, (fence) => {
|
|
223
|
+
const existing = readOccupancy(projectRoot);
|
|
224
|
+
if (existing === null) {
|
|
225
|
+
return {
|
|
226
|
+
action: "released",
|
|
227
|
+
sessionId: caller,
|
|
228
|
+
record: null,
|
|
229
|
+
path,
|
|
230
|
+
message: "occupancy already free",
|
|
231
|
+
code: 0,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
const expired = isOccupancyExpired(existing, now);
|
|
235
|
+
const owns = caller.length > 0 && caller === existing.sessionId;
|
|
236
|
+
if (!expired && !owns) {
|
|
237
|
+
return {
|
|
238
|
+
action: "denied",
|
|
239
|
+
sessionId: caller,
|
|
240
|
+
record: existing,
|
|
241
|
+
path,
|
|
242
|
+
message: formatOccupancyRemediation(existing, now),
|
|
243
|
+
code: 1,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
fence();
|
|
247
|
+
const still = readOccupancy(projectRoot);
|
|
248
|
+
if (still === null) {
|
|
249
|
+
return {
|
|
250
|
+
action: "released",
|
|
251
|
+
sessionId: caller,
|
|
252
|
+
record: null,
|
|
253
|
+
path,
|
|
254
|
+
message: "occupancy already free",
|
|
255
|
+
code: 0,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
if (still.sessionId !== existing.sessionId) {
|
|
259
|
+
throw new Error("lock compromised: occupancy session changed before release");
|
|
260
|
+
}
|
|
261
|
+
if (!expired && caller !== still.sessionId) {
|
|
262
|
+
return {
|
|
263
|
+
action: "denied",
|
|
264
|
+
sessionId: caller,
|
|
265
|
+
record: still,
|
|
266
|
+
path,
|
|
267
|
+
message: formatOccupancyRemediation(still, now),
|
|
268
|
+
code: 1,
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
removeOccupancyFile(projectRoot, fence);
|
|
272
|
+
return {
|
|
273
|
+
action: "released",
|
|
274
|
+
sessionId: still.sessionId,
|
|
275
|
+
record: null,
|
|
276
|
+
path,
|
|
277
|
+
message: `occupancy released session ${still.sessionId}`,
|
|
278
|
+
code: 0,
|
|
279
|
+
};
|
|
280
|
+
}, input.lockDeps);
|
|
281
|
+
}
|
|
282
|
+
export function evaluateOccupancyWriteGate(projectRoot, input = {}) {
|
|
283
|
+
const now = input.now ?? new Date();
|
|
284
|
+
const live = liveOccupant(projectRoot, now);
|
|
285
|
+
if (live === null)
|
|
286
|
+
return { allow: true, message: null, occupant: null };
|
|
287
|
+
const incoming = input.sessionId?.trim() || (input.env ?? process.env).DEFT_SESSION_ID?.trim() || "";
|
|
288
|
+
if (incoming.length > 0 && incoming === live.sessionId) {
|
|
289
|
+
return { allow: true, message: null, occupant: live };
|
|
290
|
+
}
|
|
291
|
+
return {
|
|
292
|
+
allow: false,
|
|
293
|
+
message: formatOccupancyRemediation(live, now),
|
|
294
|
+
occupant: live,
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
/** Close-out identity comes from the launch manifest or DEFT_SESSION_ID — never occupancy.json. */
|
|
298
|
+
export function releaseSwarmOccupancy(projectRoot, input = {}) {
|
|
299
|
+
const env = input.env ?? process.env;
|
|
300
|
+
const sessionId = input.sessionId?.trim() || env.DEFT_SESSION_ID?.trim() || "";
|
|
301
|
+
if (sessionId.length === 0) {
|
|
302
|
+
const occupant = readOccupancy(projectRoot);
|
|
303
|
+
return {
|
|
304
|
+
action: "denied",
|
|
305
|
+
sessionId: "",
|
|
306
|
+
record: occupant,
|
|
307
|
+
path: occupancyPath(projectRoot),
|
|
308
|
+
message: "swarm close-out has no occupancy_session_id (manifest missing or predates the field) " +
|
|
309
|
+
"and DEFT_SESSION_ID is unset. Steal with occupancy:steal --confirm --occupant <id>.",
|
|
310
|
+
code: 1,
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
return releaseOccupancy(projectRoot, {
|
|
314
|
+
env,
|
|
315
|
+
now: input.now,
|
|
316
|
+
lockDeps: input.lockDeps,
|
|
317
|
+
sessionId,
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
export function runOccupancySteal(projectRoot, input = {}) {
|
|
321
|
+
return stealOccupancy(projectRoot, { ...input, steal: true });
|
|
322
|
+
}
|
|
323
|
+
function occupancyHost(env) {
|
|
324
|
+
const value = (env ?? process.env).DEFT_AGENT_ID?.trim();
|
|
325
|
+
return value && value.length > 0 ? value : "none";
|
|
326
|
+
}
|
|
327
|
+
function occupancyAddress(env) {
|
|
328
|
+
const value = (env ?? process.env).DEFT_SESSION_NAME?.trim();
|
|
329
|
+
return value && value.length > 0 ? value : "none";
|
|
330
|
+
}
|
|
331
|
+
function parseOccupancy(payload, fallbackWorktree) {
|
|
332
|
+
if (payload === null || typeof payload !== "object" || Array.isArray(payload))
|
|
333
|
+
return null;
|
|
334
|
+
const obj = payload;
|
|
335
|
+
const sessionId = typeof obj.session_id === "string" ? obj.session_id.trim() : "";
|
|
336
|
+
if (sessionId.length === 0)
|
|
337
|
+
return null;
|
|
338
|
+
const intentRaw = typeof obj.intent === "string" ? obj.intent : "mutation";
|
|
339
|
+
const intent = OCCUPANCY_INTENTS.includes(intentRaw)
|
|
340
|
+
? intentRaw
|
|
341
|
+
: "mutation";
|
|
342
|
+
const claimedAt = parseTimestamp(obj.claimed_at) ?? parseTimestamp(obj.heartbeat_at);
|
|
343
|
+
const heartbeatAt = parseTimestamp(obj.heartbeat_at) ?? claimedAt;
|
|
344
|
+
if (claimedAt === null || heartbeatAt === null)
|
|
345
|
+
return null;
|
|
346
|
+
const joinRaw = typeof obj.join_protocol === "string" ? obj.join_protocol : "none";
|
|
347
|
+
const joinProtocol = OCCUPANCY_JOIN_PROTOCOLS.includes(joinRaw)
|
|
348
|
+
? joinRaw
|
|
349
|
+
: "none";
|
|
350
|
+
const worktreePath = typeof obj.worktree_path === "string" && obj.worktree_path.trim().length > 0
|
|
351
|
+
? obj.worktree_path
|
|
352
|
+
: fallbackWorktree;
|
|
353
|
+
return {
|
|
354
|
+
schemaVersion: typeof obj.schemaVersion === "number" ? obj.schemaVersion : OCCUPANCY_SCHEMA_VERSION,
|
|
355
|
+
sessionId,
|
|
356
|
+
worktreePath,
|
|
357
|
+
intent,
|
|
358
|
+
claimedAt,
|
|
359
|
+
heartbeatAt,
|
|
360
|
+
host: typeof obj.host === "string" && obj.host.length > 0 ? obj.host : "none",
|
|
361
|
+
address: typeof obj.address === "string" && obj.address.length > 0 ? obj.address : "none",
|
|
362
|
+
retainCapable: obj.retain_capable === true,
|
|
363
|
+
joinProtocol,
|
|
364
|
+
raw: { ...obj },
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
function occupancyPayload(record) {
|
|
368
|
+
return {
|
|
369
|
+
schemaVersion: OCCUPANCY_SCHEMA_VERSION,
|
|
370
|
+
session_id: record.sessionId,
|
|
371
|
+
worktree_path: record.worktreePath,
|
|
372
|
+
intent: record.intent,
|
|
373
|
+
claimed_at: timestampIso(record.claimedAt),
|
|
374
|
+
heartbeat_at: timestampIso(record.heartbeatAt),
|
|
375
|
+
host: record.host,
|
|
376
|
+
address: record.address,
|
|
377
|
+
retain_capable: record.retainCapable,
|
|
378
|
+
join_protocol: record.joinProtocol,
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
function writeOccupancyRecord(projectRoot, record, fence) {
|
|
382
|
+
const root = resolve(projectRoot);
|
|
383
|
+
const target = occupancyPath(root);
|
|
384
|
+
assertWriteTargetSafe(root, target);
|
|
385
|
+
const dir = dirname(target);
|
|
386
|
+
const tmpName = join(dir, `.occupancy.${process.pid}.occupancy.json.tmp`);
|
|
387
|
+
const payload = occupancyPayload(record);
|
|
388
|
+
const text = `${stableJson(payload, 2)}\n`;
|
|
389
|
+
try {
|
|
390
|
+
containedWrite({ root, target: tmpName, data: text, mode: "create" });
|
|
391
|
+
fence();
|
|
392
|
+
renameSync(tmpName, target);
|
|
393
|
+
}
|
|
394
|
+
catch (err) {
|
|
395
|
+
try {
|
|
396
|
+
rmSync(tmpName, { force: true });
|
|
397
|
+
}
|
|
398
|
+
catch {
|
|
399
|
+
/* best-effort cleanup */
|
|
400
|
+
}
|
|
401
|
+
throw err;
|
|
402
|
+
}
|
|
403
|
+
const parsed = parseOccupancy(payload, record.worktreePath);
|
|
404
|
+
if (parsed === null) {
|
|
405
|
+
throw new Error("occupancy write produced an unreadable record");
|
|
406
|
+
}
|
|
407
|
+
return parsed;
|
|
408
|
+
}
|
|
409
|
+
function removeOccupancyFile(projectRoot, fence) {
|
|
410
|
+
const root = resolve(projectRoot);
|
|
411
|
+
fence();
|
|
412
|
+
containedRemove({ root, target: occupancyPath(root) });
|
|
413
|
+
}
|
|
414
|
+
function withOccupancyLock(projectRoot, fn, deps = {}) {
|
|
415
|
+
return withAppendLock(occupancyPath(projectRoot), (held) => {
|
|
416
|
+
const fence = () => {
|
|
417
|
+
assertAppendLockOwned(held);
|
|
418
|
+
};
|
|
419
|
+
return fn(fence);
|
|
420
|
+
}, deps);
|
|
421
|
+
}
|
|
422
|
+
//# sourceMappingURL=occupancy.js.map
|
|
@@ -5,11 +5,11 @@
|
|
|
5
5
|
* in compact-ritual.ts. Fail-closed when OpenClaw is not detected (unless
|
|
6
6
|
* forceDeposit for tests). Real directory copy — not symlink-escape into npm.
|
|
7
7
|
*/
|
|
8
|
-
import { existsSync, lstatSync,
|
|
8
|
+
import { existsSync, lstatSync, readFileSync, statSync } from "node:fs";
|
|
9
9
|
import { homedir } from "node:os";
|
|
10
10
|
import { join } from "node:path";
|
|
11
11
|
import { detectOpenClaw, isEscapingSkillSymlink, listInScopeSkillsDirs, } from "../doctor/openclaw-skills.js";
|
|
12
|
-
import { containedWrite } from "../fs/contained-write.js";
|
|
12
|
+
import { containedMkdir, containedRemove, containedRename, containedWrite, } from "../fs/contained-write.js";
|
|
13
13
|
import { formatOpenClawSoftRebindSkillMarkdown, isManagedOpenClawSoftRebindSkill, OPENCLAW_SOFT_REBIND_SKILL_ID, } from "./compact-ritual.js";
|
|
14
14
|
function defaultIsDir(path) {
|
|
15
15
|
try {
|
|
@@ -98,7 +98,7 @@ export function depositOpenClawSoftRebindSkill(options = {}) {
|
|
|
98
98
|
if (kind === "symlink" && isEscapingSkillSymlink(skillsDir, skillDir)) {
|
|
99
99
|
// Replace unusable escaping symlink with a real copy.
|
|
100
100
|
try {
|
|
101
|
-
|
|
101
|
+
containedRemove({ root: skillsDir, target: skillDir, recursive: true });
|
|
102
102
|
}
|
|
103
103
|
catch {
|
|
104
104
|
// best-effort
|
|
@@ -121,7 +121,7 @@ export function depositOpenClawSoftRebindSkill(options = {}) {
|
|
|
121
121
|
continue;
|
|
122
122
|
}
|
|
123
123
|
}
|
|
124
|
-
|
|
124
|
+
containedMkdir({ root: skillsDir, target: skillDir });
|
|
125
125
|
const tmpName = `SKILL.md.deft-${process.pid}-${Date.now().toString(36)}.tmp`;
|
|
126
126
|
const staging = join(skillDir, tmpName);
|
|
127
127
|
try {
|
|
@@ -132,11 +132,11 @@ export function depositOpenClawSoftRebindSkill(options = {}) {
|
|
|
132
132
|
data: expected,
|
|
133
133
|
mode: "replace",
|
|
134
134
|
});
|
|
135
|
-
|
|
135
|
+
containedRename({ root: skillsDir, from: staging, to: skillFile, mutation: false });
|
|
136
136
|
}
|
|
137
137
|
catch (err) {
|
|
138
138
|
try {
|
|
139
|
-
|
|
139
|
+
containedRemove({ root: skillsDir, target: staging, mutation: false });
|
|
140
140
|
}
|
|
141
141
|
catch {
|
|
142
142
|
/* best-effort */
|
|
@@ -1,13 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Local process-cost ceremony events (#2994).
|
|
3
|
-
*
|
|
4
|
-
* Always-on, best-effort appends to `.deft-cache/events.jsonl`.
|
|
5
|
-
* Not gated on valueFeedback (distinct from #1709 attribution).
|
|
6
|
-
* No remote / Product Insights upload (#2603 not required).
|
|
7
|
-
*
|
|
8
|
-
* Types are declared locally (not imported from session-start) so this module
|
|
9
|
-
* does not form an import cycle with session-start call sites.
|
|
10
|
-
*/
|
|
11
1
|
import { type BehavioralEventRecord } from "../lifecycle/events.js";
|
|
12
2
|
export { PROCESS_COST_EVENT_NAMES, PROCESS_COST_REQUIRED_PAYLOAD, type ProcessCostEventName, } from "./process-cost-constants.js";
|
|
13
3
|
/** Ceremony tier labels mirror session-start cold|rearm (#2992 / #2994). */
|
|
@@ -50,4 +40,36 @@ export declare function emitSessionStartProcessCost(input: SessionStartProcessCo
|
|
|
50
40
|
* Returns null on any failure (telemetry must not change deny verdict).
|
|
51
41
|
*/
|
|
52
42
|
export declare function emitSessionRitualBlockedProcessCost(input: SessionRitualBlockedProcessCostInput, options: EmitProcessCostOptions): BehavioralEventRecord | null;
|
|
43
|
+
export interface CeremonyCostRollup {
|
|
44
|
+
/** Discriminator: CLI process time, not agent-turn wall clock (#3500 / #3508). */
|
|
45
|
+
readonly kind: "cli_process_time";
|
|
46
|
+
readonly windowLabel: string;
|
|
47
|
+
readonly lastColdDurationMs: number | null;
|
|
48
|
+
readonly lastRearmDurationMs: number | null;
|
|
49
|
+
readonly lastColdSteps: readonly ProcessCostStepTiming[];
|
|
50
|
+
readonly lastRearmSteps: readonly ProcessCostStepTiming[];
|
|
51
|
+
readonly blockedRitualCount: number;
|
|
52
|
+
readonly recoveryTierDistribution: Readonly<Record<string, number>>;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Roll up ceremony cost from `.deft-cache/events.jsonl` (#3508).
|
|
56
|
+
* Measures CLI process time, not agent-turn wall clock (#3500).
|
|
57
|
+
*/
|
|
58
|
+
export declare function computeCeremonyCostRollup(options: {
|
|
59
|
+
projectRoot: string;
|
|
60
|
+
logPath?: string | null;
|
|
61
|
+
windowMs?: number;
|
|
62
|
+
now?: Date;
|
|
63
|
+
windowLabel?: string;
|
|
64
|
+
}): CeremonyCostRollup;
|
|
65
|
+
/**
|
|
66
|
+
* Human rollup for the composed `value:show` reader (#3508).
|
|
67
|
+
* CLI process time, not agent-turn wall clock (#3500).
|
|
68
|
+
*/
|
|
69
|
+
export declare function formatCeremonyCostReport(rollup: CeremonyCostRollup): string;
|
|
70
|
+
/**
|
|
71
|
+
* Operator-visible mutation `session:start` line (#3508).
|
|
72
|
+
* CLI process time only. ⊗ not #3286 Later graduation input.
|
|
73
|
+
*/
|
|
74
|
+
export declare function formatSessionStartCeremonyCostLine(ceremonyTier: ProcessCostCeremonyTier, durationMs: number): string;
|
|
53
75
|
//# sourceMappingURL=process-cost.d.ts.map
|