@deftai/directive-core 0.107.0 → 0.108.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/architecture/sor-preflight.js +2 -0
- package/dist/check/cached-orchestrator.js +28 -3
- package/dist/check/named-cause.js +18 -3
- package/dist/codebase/provider.js +7 -1
- package/dist/delivery-attempt/handoff.js +1 -1
- package/dist/design-critique/citation-grammar.d.ts +53 -0
- package/dist/design-critique/citation-grammar.js +303 -0
- package/dist/design-critique/completed-arc-record.d.ts +62 -0
- package/dist/design-critique/completed-arc-record.js +232 -0
- package/dist/freshness/bind.d.ts +32 -3
- package/dist/freshness/bind.js +58 -12
- package/dist/hooks/classify/host-session-identity.d.ts +76 -0
- package/dist/hooks/classify/host-session-identity.js +470 -0
- package/dist/hooks/classify/index.d.ts +3 -2
- package/dist/hooks/classify/index.js +3 -2
- package/dist/hooks/classify/paths.d.ts +7 -0
- package/dist/hooks/classify/paths.js +35 -0
- package/dist/hooks/classify/stdin.d.ts +5 -0
- package/dist/hooks/classify/stdin.js +27 -0
- package/dist/hooks/dispatcher.d.ts +52 -1
- package/dist/hooks/dispatcher.js +538 -45
- package/dist/hooks/fixtures/cases.d.ts +16 -2
- package/dist/hooks/fixtures/cases.js +175 -0
- package/dist/hooks/scope.js +3 -1
- package/dist/init-deposit/agent-hooks.d.ts +2 -1
- package/dist/init-deposit/agent-hooks.js +8 -1
- package/dist/init-deposit/hook-runtime-travel.d.ts +77 -0
- package/dist/init-deposit/hook-runtime-travel.js +269 -0
- package/dist/intake/clause-derivation.js +9 -3
- package/dist/intake/issue-ingest.js +62 -2
- package/dist/intake/platform-capabilities.d.ts +9 -2
- package/dist/intake/platform-capabilities.js +38 -9
- package/dist/orphan-active/evaluate.d.ts +25 -0
- package/dist/orphan-active/evaluate.js +193 -128
- package/dist/orphan-active/index.d.ts +1 -0
- package/dist/orphan-active/index.js +1 -0
- package/dist/orphan-active/issue-state.d.ts +121 -0
- package/dist/orphan-active/issue-state.js +278 -0
- package/dist/platform/cursor-managed-runtime.d.ts +95 -0
- package/dist/platform/cursor-managed-runtime.js +241 -0
- package/dist/platform/index.d.ts +1 -0
- package/dist/platform/index.js +1 -0
- package/dist/platform/platform-capabilities.d.ts +23 -1
- package/dist/platform/platform-capabilities.js +55 -15
- package/dist/policy/ceremony-dial.js +5 -9
- package/dist/policy/host-hooks.js +4 -9
- package/dist/policy/org-force-on-migration.js +5 -10
- package/dist/policy/plan-extensions.d.ts +14 -2
- package/dist/policy/plan-extensions.js +24 -3
- package/dist/policy/product-signal.js +5 -10
- package/dist/policy/require-human-merge.js +5 -9
- package/dist/policy/resolve.js +77 -16
- package/dist/policy/value-feedback.js +9 -18
- package/dist/pr-closeout-attestable/evaluate.d.ts +80 -0
- package/dist/pr-closeout-attestable/evaluate.js +306 -0
- package/dist/pr-closeout-attestable/index.d.ts +2 -0
- package/dist/pr-closeout-attestable/index.js +2 -0
- package/dist/pr-protected-issues/gh.d.ts +6 -2
- package/dist/pr-protected-issues/gh.js +7 -3
- package/dist/pr-wait-mergeable/cascade.d.ts +9 -1
- package/dist/pr-wait-mergeable/cascade.js +21 -1
- package/dist/pr-wait-mergeable/types.d.ts +2 -0
- package/dist/pr-wait-mergeable/wrappers.d.ts +6 -0
- package/dist/pr-wait-mergeable/wrappers.js +11 -0
- package/dist/preflight/evaluate.d.ts +15 -0
- package/dist/preflight/evaluate.js +29 -3
- package/dist/product-first-done-gate/acceptance-resolver.d.ts +14 -2
- package/dist/product-first-done-gate/acceptance-resolver.js +13 -2
- package/dist/product-first-done-gate/evaluate.js +17 -7
- package/dist/release/cli-drift-report.d.ts +68 -0
- package/dist/release/cli-drift-report.js +189 -0
- package/dist/release/index.d.ts +1 -0
- package/dist/release/index.js +1 -0
- package/dist/release/issue-state-fetch.d.ts +5 -3
- package/dist/release/issue-state-fetch.js +86 -21
- package/dist/release/native-steps.js +1 -0
- package/dist/release/pipeline.js +19 -0
- package/dist/release/types.d.ts +14 -0
- package/dist/render/constants.d.ts +4 -0
- package/dist/render/constants.js +11 -8
- package/dist/render/export-spec.js +31 -5
- package/dist/render/index.d.ts +1 -1
- package/dist/render/index.js +1 -1
- package/dist/render/prd-render.d.ts +4 -1
- package/dist/render/prd-render.js +82 -23
- package/dist/render/project-render.js +14 -7
- package/dist/render/scope-outlook.d.ts +2 -0
- package/dist/render/scope-outlook.js +3 -0
- package/dist/render/spec-render.js +2 -2
- package/dist/resolution/package-manager.d.ts +33 -4
- package/dist/resolution/package-manager.js +210 -9
- package/dist/review-monitor/constants.d.ts +7 -0
- package/dist/review-monitor/constants.js +7 -0
- package/dist/review-monitor/github-lease.d.ts +98 -1
- package/dist/review-monitor/github-lease.js +182 -1
- package/dist/review-monitor/lease-comment.d.ts +36 -0
- package/dist/review-monitor/lease-comment.js +103 -2
- package/dist/scm/call.js +12 -3
- package/dist/scm/design-critique-chip.d.ts +1 -0
- package/dist/scm/design-critique-chip.js +22 -6
- package/dist/scm/gh-rest.d.ts +9 -1
- package/dist/scm/gh-rest.js +103 -0
- package/dist/scm/readiness.d.ts +2 -0
- package/dist/scm/readiness.js +28 -4
- package/dist/scope/project-definition-sync.js +14 -7
- package/dist/session/git.d.ts +24 -0
- package/dist/session/git.js +138 -2
- package/dist/session/occupancy.d.ts +238 -12
- package/dist/session/occupancy.js +815 -29
- package/dist/session/ritual-sentinel.d.ts +5 -0
- package/dist/session/ritual-sentinel.js +5 -0
- package/dist/session/session-ready.d.ts +5 -1
- package/dist/session/session-ready.js +98 -8
- package/dist/session/session-start.d.ts +5 -1
- package/dist/session/session-start.js +32 -17
- package/dist/session/toolchain-preflight.d.ts +13 -3
- package/dist/session/toolchain-preflight.js +93 -18
- package/dist/session/verify-session-ritual.d.ts +11 -0
- package/dist/session/verify-session-ritual.js +60 -15
- package/dist/slice/constants.d.ts +1 -1
- package/dist/spec-authority/constants.d.ts +10 -2
- package/dist/spec-authority/constants.js +53 -8
- package/dist/spec-authority/resolver.d.ts +3 -0
- package/dist/spec-authority/resolver.js +55 -7
- package/dist/subprocess/max-buffer.d.ts +14 -0
- package/dist/subprocess/max-buffer.js +15 -0
- package/dist/swarm/complete-cohort.d.ts +2 -0
- package/dist/swarm/complete-cohort.js +21 -13
- package/dist/swarm/index.d.ts +1 -0
- package/dist/swarm/index.js +1 -0
- package/dist/swarm/launch-cli.js +53 -33
- package/dist/swarm/launch.d.ts +8 -0
- package/dist/swarm/launch.js +63 -43
- package/dist/swarm/pre-dispatch-cli.js +2 -1
- package/dist/swarm/pre-dispatch.js +10 -1
- package/dist/swarm/subagent-status-dir.d.ts +27 -0
- package/dist/swarm/subagent-status-dir.js +42 -0
- package/dist/swarm/worktrees.d.ts +17 -3
- package/dist/swarm/worktrees.js +48 -4
- package/dist/triage/scope/mutations-core.d.ts +2 -6
- package/dist/triage/scope/mutations-core.js +23 -55
- package/dist/triage/scope-drift/add-ignore.js +5 -39
- package/dist/triage/subscribe/index.d.ts +2 -6
- package/dist/triage/subscribe/index.js +15 -65
- package/dist/triage/welcome/writers.js +15 -74
- package/dist/vbrief-build/index.d.ts +2 -1
- package/dist/vbrief-build/index.js +2 -1
- package/dist/vbrief-build/parity-scenarios.js +5 -5
- package/dist/vbrief-build/project-definition-io.d.ts +63 -3
- package/dist/vbrief-build/project-definition-io.js +357 -31
- package/dist/vbrief-build/project-definition-mutation.d.ts +32 -0
- package/dist/vbrief-build/project-definition-mutation.js +43 -0
- package/dist/vbrief-validate/conformance.d.ts +1 -0
- package/dist/vbrief-validate/conformance.js +85 -8
- package/dist/vbrief-validate/main.js +2 -0
- package/dist/vbrief-validate/precutover.js +5 -10
- package/dist/verify-ac/clauses.d.ts +62 -2
- package/dist/verify-ac/clauses.js +183 -111
- package/dist/verify-ac/index.d.ts +1 -1
- package/dist/verify-ac/index.js +1 -1
- package/dist/verify-env/node-runtime.d.ts +8 -4
- package/dist/verify-env/node-runtime.js +9 -6
- package/dist/verify-env/toolchain-check.d.ts +35 -3
- package/dist/verify-env/toolchain-check.js +155 -36
- package/dist/verify-source/project-definition-mutation-boundary.d.ts +104 -0
- package/dist/verify-source/project-definition-mutation-boundary.js +326 -0
- package/package.json +7 -3
package/dist/session/git.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { execFileSync, spawnSync } from "node:child_process";
|
|
2
|
-
import {
|
|
2
|
+
import { existsSync, statSync } from "node:fs";
|
|
3
|
+
import { dirname, isAbsolute, resolve } from "node:path";
|
|
4
|
+
import { resolveCaptureFailureStderr, SUBPROCESS_MAX_BUFFER } from "../subprocess/max-buffer.js";
|
|
3
5
|
function coerceGitBytes(value) {
|
|
4
6
|
if (Buffer.isBuffer(value))
|
|
5
7
|
return value;
|
|
@@ -21,6 +23,9 @@ export const defaultGitRunner = (projectRoot, args) => {
|
|
|
21
23
|
const stdout = execFileSync("git", [...args], {
|
|
22
24
|
cwd: projectRoot,
|
|
23
25
|
stdio: ["ignore", "pipe", "pipe"],
|
|
26
|
+
// `git show <tip>:<path>` streams whole blobs; SPECIFICATION.md alone is
|
|
27
|
+
// past Node's 1 MB default (#3903).
|
|
28
|
+
maxBuffer: SUBPROCESS_MAX_BUFFER,
|
|
24
29
|
});
|
|
25
30
|
return { code: 0, stdout: gitStdoutString(coerceGitBytes(stdout), args), stderr: "" };
|
|
26
31
|
}
|
|
@@ -32,10 +37,96 @@ export const defaultGitRunner = (projectRoot, args) => {
|
|
|
32
37
|
return {
|
|
33
38
|
code: typeof e.status === "number" ? e.status : 2,
|
|
34
39
|
stdout: gitStdoutString(coerceGitBytes(e.stdout), args),
|
|
35
|
-
|
|
40
|
+
// A spawn-level failure (ENOBUFS on an oversized blob, timeout kill)
|
|
41
|
+
// throws with no status and empty stderr (#3903).
|
|
42
|
+
stderr: resolveCaptureFailureStderr({
|
|
43
|
+
captured: coerceGitBytes(e.stderr).toString("utf8").trimEnd(),
|
|
44
|
+
status: e.status,
|
|
45
|
+
message: e.message,
|
|
46
|
+
}),
|
|
36
47
|
};
|
|
37
48
|
}
|
|
38
49
|
};
|
|
50
|
+
/**
|
|
51
|
+
* One `git rev-parse` that answers the three ref reads the mutation gates
|
|
52
|
+
* otherwise make separately. Output is one line per requested value, in
|
|
53
|
+
* argument order: full HEAD, worktree root, branch name.
|
|
54
|
+
*/
|
|
55
|
+
const DISPATCH_GIT_CONTEXT_ARGS = [
|
|
56
|
+
"rev-parse",
|
|
57
|
+
"HEAD",
|
|
58
|
+
"--show-toplevel",
|
|
59
|
+
"--abbrev-ref",
|
|
60
|
+
"HEAD",
|
|
61
|
+
];
|
|
62
|
+
/** The individual reads `DISPATCH_GIT_CONTEXT_ARGS` can answer without a spawn. */
|
|
63
|
+
const COALESCED_GIT_ARGS = [
|
|
64
|
+
["rev-parse", "--verify", "HEAD"],
|
|
65
|
+
["rev-parse", "--show-toplevel"],
|
|
66
|
+
["symbolic-ref", "--short", "HEAD"],
|
|
67
|
+
];
|
|
68
|
+
/**
|
|
69
|
+
* Ref and identity reads whose answer cannot change inside one dispatch.
|
|
70
|
+
* Deliberately excludes content reads (no repeat callers, unbounded memory)
|
|
71
|
+
* and anything that touches the object store or a remote.
|
|
72
|
+
*/
|
|
73
|
+
const CACHEABLE_GIT_VERBS = new Set([
|
|
74
|
+
"rev-parse",
|
|
75
|
+
"symbolic-ref",
|
|
76
|
+
"merge-base",
|
|
77
|
+
"rev-list",
|
|
78
|
+
]);
|
|
79
|
+
function isCoalescedGitArgs(args) {
|
|
80
|
+
return COALESCED_GIT_ARGS.some((candidate) => candidate.length === args.length && candidate.every((arg, i) => arg === args[i]));
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Serve a dispatch's repeated ref reads from one `git` child (#3736).
|
|
84
|
+
*
|
|
85
|
+
* The host mutation gate resolved HEAD, the worktree root, and the branch with
|
|
86
|
+
* a separate spawn each, twice over; on a loaded Windows box a spawn measured a
|
|
87
|
+
* 2.2s p50, so every concurrent agent's hook slowed every other agent's hook.
|
|
88
|
+
* The probe is lazy on purpose: it only fires for a read it can actually
|
|
89
|
+
* answer, so a caller that never asks for context never pays for it.
|
|
90
|
+
*/
|
|
91
|
+
export function memoizeGitRunner(runGit = defaultGitRunner) {
|
|
92
|
+
const cache = new Map();
|
|
93
|
+
const probed = new Set();
|
|
94
|
+
const keyFor = (root, args) => JSON.stringify([root, args]);
|
|
95
|
+
const probeContext = (projectRoot, root) => {
|
|
96
|
+
probed.add(root);
|
|
97
|
+
const context = runGit(projectRoot, DISPATCH_GIT_CONTEXT_ARGS);
|
|
98
|
+
if (context.code !== 0)
|
|
99
|
+
return;
|
|
100
|
+
const [head, worktree, branch, ...extra] = context.stdout.split(/\r?\n/);
|
|
101
|
+
if (!head || !worktree || !branch || extra.length > 0)
|
|
102
|
+
return;
|
|
103
|
+
const ok = (stdout) => ({ code: 0, stdout, stderr: "" });
|
|
104
|
+
cache.set(keyFor(root, ["rev-parse", "--verify", "HEAD"]), ok(head));
|
|
105
|
+
cache.set(keyFor(root, ["rev-parse", "--show-toplevel"]), ok(worktree));
|
|
106
|
+
// `--abbrev-ref` prints the literal "HEAD" on a detached head, where
|
|
107
|
+
// `symbolic-ref` exits non-zero — which is what callers branch on.
|
|
108
|
+
cache.set(keyFor(root, ["symbolic-ref", "--short", "HEAD"]), branch === "HEAD" ? { code: 1, stdout: "", stderr: "" } : ok(branch));
|
|
109
|
+
};
|
|
110
|
+
return (projectRoot, args) => {
|
|
111
|
+
const root = resolve(projectRoot);
|
|
112
|
+
const key = keyFor(root, args);
|
|
113
|
+
const cached = cache.get(key);
|
|
114
|
+
if (cached !== undefined)
|
|
115
|
+
return cached;
|
|
116
|
+
if (!probed.has(root) && isCoalescedGitArgs(args)) {
|
|
117
|
+
probeContext(projectRoot, root);
|
|
118
|
+
const coalesced = cache.get(key);
|
|
119
|
+
if (coalesced !== undefined)
|
|
120
|
+
return coalesced;
|
|
121
|
+
}
|
|
122
|
+
const result = runGit(projectRoot, args);
|
|
123
|
+
const verb = args[0];
|
|
124
|
+
if (verb !== undefined && CACHEABLE_GIT_VERBS.has(verb)) {
|
|
125
|
+
cache.set(key, result);
|
|
126
|
+
}
|
|
127
|
+
return result;
|
|
128
|
+
};
|
|
129
|
+
}
|
|
39
130
|
export function gitHead(projectRoot, runGit = defaultGitRunner) {
|
|
40
131
|
const { code, stdout, stderr } = runGit(projectRoot, ["rev-parse", "--verify", "HEAD"]);
|
|
41
132
|
if (code !== 0 || !stdout) {
|
|
@@ -50,6 +141,51 @@ export function worktreePath(projectRoot, runGit = defaultGitRunner) {
|
|
|
50
141
|
}
|
|
51
142
|
return resolve(projectRoot);
|
|
52
143
|
}
|
|
144
|
+
/**
|
|
145
|
+
* Walk from `targetPath` to the nearest existing directory. A write that
|
|
146
|
+
* creates a missing nested file is the common case; git cannot use a
|
|
147
|
+
* nonexistent cwd.
|
|
148
|
+
*/
|
|
149
|
+
export function existingAncestorDir(targetPath) {
|
|
150
|
+
let current = resolve(targetPath);
|
|
151
|
+
for (;;) {
|
|
152
|
+
if (existsSync(current)) {
|
|
153
|
+
try {
|
|
154
|
+
if (statSync(current).isDirectory())
|
|
155
|
+
return current;
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
// Fall through to dirname when the path is not stat-able.
|
|
159
|
+
}
|
|
160
|
+
const parent = dirname(current);
|
|
161
|
+
return parent === current ? null : parent;
|
|
162
|
+
}
|
|
163
|
+
const parent = dirname(current);
|
|
164
|
+
if (parent === current)
|
|
165
|
+
return null;
|
|
166
|
+
current = parent;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Failure-expressing sibling of {@link worktreePath}. Returns null instead of
|
|
171
|
+
* falling back to `startDir` so a missing git tree is distinguishable from
|
|
172
|
+
* success (#3794).
|
|
173
|
+
*/
|
|
174
|
+
export function worktreePathOrNull(startDir, runGit = defaultGitRunner) {
|
|
175
|
+
const { code, stdout } = runGit(startDir, ["rev-parse", "--show-toplevel"]);
|
|
176
|
+
const trimmed = stdout.trim();
|
|
177
|
+
if (code !== 0 || trimmed.length === 0)
|
|
178
|
+
return null;
|
|
179
|
+
return resolve(trimmed);
|
|
180
|
+
}
|
|
181
|
+
/** Absolute `--git-common-dir` for `projectRoot`, or null on failure. */
|
|
182
|
+
export function gitCommonDir(projectRoot, runGit = defaultGitRunner) {
|
|
183
|
+
const { code, stdout } = runGit(projectRoot, ["rev-parse", "--git-common-dir"]);
|
|
184
|
+
const trimmed = stdout.trim();
|
|
185
|
+
if (code !== 0 || trimmed.length === 0)
|
|
186
|
+
return null;
|
|
187
|
+
return isAbsolute(trimmed) ? resolve(trimmed) : resolve(projectRoot, trimmed);
|
|
188
|
+
}
|
|
53
189
|
/** True when `ancestor` is reachable from `descendant` (same commit counts). */
|
|
54
190
|
export function gitIsAncestor(projectRoot, ancestor, descendant, runGit = defaultGitRunner) {
|
|
55
191
|
if (ancestor === descendant) {
|
|
@@ -4,24 +4,121 @@
|
|
|
4
4
|
* Ritual-state is "this session completed ceremony." Occupancy is "who may
|
|
5
5
|
* mutate this tree right now." Those lifetimes differ; do not overload
|
|
6
6
|
* ritual-state.json. Ordinary end is occupancy:release / session:end (#3604).
|
|
7
|
-
*
|
|
7
|
+
*
|
|
8
|
+
* What this boundary is (#3755): a cooperative bearer-id boundary, not a
|
|
9
|
+
* lineage. The lease admits whoever presents an id the record itself names —
|
|
10
|
+
* the occupant's id, or a child id the occupant granted — so possession of a
|
|
11
|
+
* string is the whole credential. Nothing here observes parentage, so a
|
|
12
|
+
* dispatched child is admitted because a grant records it, never because it
|
|
13
|
+
* inherited the holder's rights. Membership is explicit, attributable and
|
|
14
|
+
* expiring (`grantOccupancyMembership`), and it admits writes only: release,
|
|
15
|
+
* steal, heartbeat and cohort close-out stay owner-only, so a grant cannot be
|
|
16
|
+
* spent on the lease itself. Child-initiated join queuing stays out of scope —
|
|
17
|
+
* the owner issues membership; the child does not request it. Ritual state is
|
|
18
|
+
* still single-owner, so a member writes under the occupant's ceremony: the
|
|
19
|
+
* composite hook write gate measures the tree's verified ritual owner against
|
|
20
|
+
* the occupant that issued the grant, not against the writer.
|
|
8
21
|
*
|
|
9
22
|
* Concurrency model:
|
|
10
23
|
* - Assumptions: local filesystem; cooperating processes on one machine.
|
|
11
24
|
* - Guarantees: mutual exclusion under crash-free operation; detect-and-abort
|
|
12
25
|
* if the sidecar lock is compromised (fence before rename/unlink).
|
|
13
26
|
* - Non-goals: network filesystems; Byzantine processes; perfect off-Linux
|
|
14
|
-
* PID-reuse detection (hard age cap
|
|
27
|
+
* PID-reuse detection (hard age cap — `OCCUPANCY_MAX_LEASE_MS` — plus fence
|
|
28
|
+
* instead).
|
|
29
|
+
* - Residual: the write gate authorizes a write it does not itself perform, so
|
|
30
|
+
* no verdict is atomic with the write. A takeover that publishes after the
|
|
31
|
+
* allow — including one already holding the lock but not yet written — is
|
|
32
|
+
* outside what this gate can see. Closing that would mean denying every
|
|
33
|
+
* owner whose lease file is momentarily locked, which is the load-shedding
|
|
34
|
+
* regression #3736 fixed. The bound is the TTL, not the gate.
|
|
15
35
|
*/
|
|
16
36
|
import { type LockDeps } from "../slice/lock.js";
|
|
37
|
+
import { type SwarmWorkerRole } from "../swarm/routing.js";
|
|
17
38
|
export declare const OCCUPANCY_SCHEMA_VERSION = 1;
|
|
18
39
|
export declare const OCCUPANCY_RELPATH: readonly [".deft", "occupancy.json"];
|
|
19
40
|
/** Crash recovery TTL: 20 minutes without heartbeat (15–30 window). */
|
|
20
41
|
export declare const OCCUPANCY_TTL_MS: number;
|
|
42
|
+
/**
|
|
43
|
+
* Owner-allow re-stamp floor (#3599). The write gate runs on every gated write,
|
|
44
|
+
* so refreshing unconditionally would rewrite the lease file per keystroke-scale
|
|
45
|
+
* event. A quarter of the TTL bounds that without shortening the safe window:
|
|
46
|
+
* a write at any age past this floor resets the clock, so an owner that writes
|
|
47
|
+
* at least once per TTL never expires.
|
|
48
|
+
*/
|
|
49
|
+
export declare const OCCUPANCY_REFRESH_AFTER_MS: number;
|
|
50
|
+
/** Owner-allow staleness warning floor: three quarters of the TTL (#3599). */
|
|
51
|
+
export declare const OCCUPANCY_STALE_WARN_MS: number;
|
|
52
|
+
/**
|
|
53
|
+
* Absolute lease age cap, keyed on `claimedAt` and independent of refresh
|
|
54
|
+
* (#3599). Occupancy admits whoever presents the occupant's session id, so
|
|
55
|
+
* "the owner is still writing" only proves that some process holds that
|
|
56
|
+
* string. Without a bound on claim age, refresh would turn the heartbeat TTL —
|
|
57
|
+
* the sole mechanism that reclaims a worktree from a dead session — into
|
|
58
|
+
* something a writer can extend forever.
|
|
59
|
+
*
|
|
60
|
+
* Thirty-six TTLs is twelve hours, sized by the stalled owner rather than the
|
|
61
|
+
* busy one. Refresh keys on writes, so an agent that finishes overnight and
|
|
62
|
+
* waits for its operator is alive, correct, and silent — it stops refreshing
|
|
63
|
+
* while staying entirely legitimate. Twelve hours spans a 23:00 dispatch to a
|
|
64
|
+
* 09:00 handoff and still bounds reclaim well inside a day. Reaching the cap
|
|
65
|
+
* costs the owner one re-claim, not its work.
|
|
66
|
+
*
|
|
67
|
+
* Known limitation: a pure time cap cannot tell a stalled-but-live owner from a
|
|
68
|
+
* dead one, because the only liveness signal on this path is a write. If that
|
|
69
|
+
* ambiguity starts to bite, the answer is a liveness signal that needs no write
|
|
70
|
+
* — an explicit parked state, or refresh on non-write activity — not a larger
|
|
71
|
+
* number here.
|
|
72
|
+
*/
|
|
73
|
+
export declare const OCCUPANCY_MAX_LEASE_MS: number;
|
|
21
74
|
export declare const OCCUPANCY_INTENTS: readonly ["mutation", "swarm", "review"];
|
|
22
75
|
export type OccupancyIntent = (typeof OCCUPANCY_INTENTS)[number];
|
|
23
76
|
export declare const OCCUPANCY_JOIN_PROTOCOLS: readonly ["none", "heartbeat-file", "parent-message"];
|
|
24
77
|
export type OccupancyJoinProtocol = (typeof OCCUPANCY_JOIN_PROTOCOLS)[number];
|
|
78
|
+
/**
|
|
79
|
+
* Default life of a grant (#3755), sized by one dispatched unit of work:
|
|
80
|
+
* implement, open the PR, run the review cycle. Four hours is a third of the
|
|
81
|
+
* absolute lease cap, so a grant that outlives its child still dies well inside
|
|
82
|
+
* the lease that issued it, and re-granting costs the owner one command.
|
|
83
|
+
*/
|
|
84
|
+
export declare const OCCUPANCY_GRANT_TTL_MS: number;
|
|
85
|
+
/**
|
|
86
|
+
* Grants a single lease may carry (#3755). Bounded because the list is rewritten
|
|
87
|
+
* into the lease file on every touch and the topology it serves is a nuclear
|
|
88
|
+
* family (#3155), not a mesh — a lease needing more than this is a design
|
|
89
|
+
* problem, not a capacity one.
|
|
90
|
+
*/
|
|
91
|
+
export declare const OCCUPANCY_MAX_GRANTS = 32;
|
|
92
|
+
/**
|
|
93
|
+
* A child admitted to the owner's lease (#3755). The five recorded fields are
|
|
94
|
+
* the point: possession of a session string proves nothing about who is behind
|
|
95
|
+
* it, so admission has to name the owner that issued it, the child it admits,
|
|
96
|
+
* the tree it covers, the role it was dispatched for, and when it stops being
|
|
97
|
+
* true. A grant admits writes; it never admits administration.
|
|
98
|
+
*
|
|
99
|
+
* `worktreePath` is recorded rather than assumed. Dispatched children land in
|
|
100
|
+
* their own worktree because the dispatch envelope puts them there, not because
|
|
101
|
+
* anything enforces it, so same-tree dispatch stays reachable and a lease may
|
|
102
|
+
* hold several grants over one path.
|
|
103
|
+
*/
|
|
104
|
+
export interface OccupancyGrant {
|
|
105
|
+
readonly ownerSessionId: string;
|
|
106
|
+
readonly childSessionId: string;
|
|
107
|
+
readonly worktreePath: string;
|
|
108
|
+
readonly role: SwarmWorkerRole;
|
|
109
|
+
readonly expiresAt: Date;
|
|
110
|
+
/** Per-actor host of the granted child, mirroring the occupant's own field. */
|
|
111
|
+
readonly host: string;
|
|
112
|
+
/** Per-actor address of the granted child, mirroring the occupant's own field. */
|
|
113
|
+
readonly address: string;
|
|
114
|
+
/** How this child reports back, from the parked join vocabulary. */
|
|
115
|
+
readonly joinProtocol: OccupancyJoinProtocol;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Who the presented id is to this lease (#3755). `member` is the only thing a
|
|
119
|
+
* grant buys, and it buys it for writes alone.
|
|
120
|
+
*/
|
|
121
|
+
export type OccupancyAdmission = "owner" | "member" | "stranger";
|
|
25
122
|
export interface OccupancyRecord {
|
|
26
123
|
readonly schemaVersion: number;
|
|
27
124
|
readonly sessionId: string;
|
|
@@ -29,13 +126,24 @@ export interface OccupancyRecord {
|
|
|
29
126
|
readonly intent: OccupancyIntent;
|
|
30
127
|
readonly claimedAt: Date;
|
|
31
128
|
readonly heartbeatAt: Date;
|
|
129
|
+
/**
|
|
130
|
+
* Last gated product write under this lease — by the owner or by a granted
|
|
131
|
+
* member (#3599 / #3755) — or null when none is recorded. Distinct from
|
|
132
|
+
* `heartbeatAt`, which any lease touch advances.
|
|
133
|
+
* Coarse to `OCCUPANCY_REFRESH_AFTER_MS`: a write inside that floor does not
|
|
134
|
+
* re-stamp, so the recorded time can trail the true last write by up to the
|
|
135
|
+
* refresh interval.
|
|
136
|
+
*/
|
|
137
|
+
readonly lastWriteAt: Date | null;
|
|
32
138
|
readonly host: string;
|
|
33
139
|
readonly address: string;
|
|
34
140
|
readonly retainCapable: boolean;
|
|
35
141
|
readonly joinProtocol: OccupancyJoinProtocol;
|
|
142
|
+
/** Children the occupant admitted to this lease for writes (#3755). */
|
|
143
|
+
readonly grants: readonly OccupancyGrant[];
|
|
36
144
|
readonly raw: Record<string, unknown>;
|
|
37
145
|
}
|
|
38
|
-
export type OccupancyAction = "claimed" | "heartbeat" | "stolen" | "denied" | "released";
|
|
146
|
+
export type OccupancyAction = "claimed" | "heartbeat" | "stolen" | "denied" | "released" | "granted" | "revoked";
|
|
39
147
|
export interface OccupancyDecision {
|
|
40
148
|
readonly action: OccupancyAction;
|
|
41
149
|
readonly sessionId: string;
|
|
@@ -57,18 +165,70 @@ export interface ApplyOccupancyInput {
|
|
|
57
165
|
readonly address?: string;
|
|
58
166
|
readonly retainCapable?: boolean;
|
|
59
167
|
readonly joinProtocol?: OccupancyJoinProtocol;
|
|
60
|
-
/**
|
|
168
|
+
/** Record this touch as a product write, not only a heartbeat (#3599). */
|
|
169
|
+
readonly markWrite?: boolean;
|
|
170
|
+
/** When false, evaluate only (including confirmed steal) without writing. */
|
|
61
171
|
readonly write?: boolean;
|
|
62
172
|
/** Test seam for lock wait / timeout. */
|
|
63
173
|
readonly lockDeps?: LockDeps;
|
|
64
174
|
}
|
|
65
175
|
export declare function occupancyPath(projectRoot: string): string;
|
|
66
176
|
export declare function heartbeatAgeSeconds(record: OccupancyRecord, now?: Date): number;
|
|
67
|
-
|
|
177
|
+
/** Age of the occupant's last recorded product write, or null when none (#3599). */
|
|
178
|
+
export declare function lastWriteAgeSeconds(record: OccupancyRecord, now?: Date): number | null;
|
|
179
|
+
/**
|
|
180
|
+
* Human phrase for how recently the occupant wrote (#3599). Heartbeat age alone
|
|
181
|
+
* cannot distinguish an occupant mid-edit from one that merely claimed and left.
|
|
182
|
+
*/
|
|
183
|
+
export declare function formatLastWritePhrase(record: OccupancyRecord, now?: Date): string;
|
|
184
|
+
/** Age of the lease itself, measured from the claim that opened it (#3599). */
|
|
185
|
+
export declare function leaseAgeSeconds(record: OccupancyRecord, now?: Date): number;
|
|
186
|
+
/**
|
|
187
|
+
* Why a lease is or is not live (#3599). The two dead states are not the same
|
|
188
|
+
* operator problem: `heartbeat-stale` says nobody has touched the lease, while
|
|
189
|
+
* `age-capped` says the holder may well be active but has held the tree past
|
|
190
|
+
* the bound that keeps crash recovery possible.
|
|
191
|
+
*/
|
|
192
|
+
export type OccupancyLiveness = "live" | "heartbeat-stale" | "age-capped";
|
|
193
|
+
export declare function occupancyLiveness(record: OccupancyRecord, now?: Date, ttlMs?: number, maxLeaseMs?: number): OccupancyLiveness;
|
|
194
|
+
export declare function isOccupancyExpired(record: OccupancyRecord, now?: Date, ttlMs?: number, maxLeaseMs?: number): boolean;
|
|
195
|
+
/** Grants that still admit somebody (#3755). An expired grant admits nobody. */
|
|
196
|
+
export declare function liveOccupancyGrants(record: OccupancyRecord, now?: Date): readonly OccupancyGrant[];
|
|
197
|
+
/**
|
|
198
|
+
* The grant admitting `sessionId`, or null (#3755). Expiry is refused here, on
|
|
199
|
+
* read, rather than trusted to a sweep: nothing guarantees a lease is ever
|
|
200
|
+
* touched again after the grant is written, so a grant that outlived its clock
|
|
201
|
+
* must stop admitting the moment it is read, not the next time it is rewritten.
|
|
202
|
+
*/
|
|
203
|
+
export declare function occupancyGrantFor(record: OccupancyRecord, sessionId: string, now?: Date): OccupancyGrant | null;
|
|
204
|
+
/**
|
|
205
|
+
* What the presented id is to this lease (#3755). Deliberately not a liveness
|
|
206
|
+
* question: it answers who, and callers pair it with `occupancyLiveness` to
|
|
207
|
+
* answer whether the lease is still worth anything.
|
|
208
|
+
*/
|
|
209
|
+
export declare function occupancyAdmission(record: OccupancyRecord, sessionId: string, now?: Date): OccupancyAdmission;
|
|
210
|
+
/**
|
|
211
|
+
* Warn the holder that its own lease is inside the staleness window (#3599).
|
|
212
|
+
* Without this the owner learns it went stale only when a peer steals the lease.
|
|
213
|
+
*/
|
|
214
|
+
export declare function formatOccupancyStaleWarning(record: OccupancyRecord, now?: Date, ttlMs?: number): string;
|
|
215
|
+
/**
|
|
216
|
+
* Tell the holder its lease aged out of the absolute cap (#3599). Distinct
|
|
217
|
+
* remediation from a stale heartbeat: beating harder cannot help, because the
|
|
218
|
+
* lease is gone rather than merely quiet, so the answer is to re-claim.
|
|
219
|
+
*/
|
|
220
|
+
export declare function formatOccupancyAgeCapRemediation(record: OccupancyRecord, now?: Date, maxLeaseMs?: number): string;
|
|
68
221
|
export declare function formatOccupancyRemediation(record: OccupancyRecord, now?: Date): string;
|
|
222
|
+
/**
|
|
223
|
+
* Refuse an administrative verb to a granted child (#3755). Named apart from
|
|
224
|
+
* the stranger refusal because the answer differs: this caller is admitted, and
|
|
225
|
+
* telling it to steal or wait would send it to take the very lease its grant
|
|
226
|
+
* derives from. A grant admits writes; the lease has one owner.
|
|
227
|
+
*/
|
|
228
|
+
export declare function formatOccupancyMemberAdministrationRefusal(record: OccupancyRecord, grant: OccupancyGrant, verb: string): string;
|
|
69
229
|
export declare function resolveOccupancySessionId(input?: ApplyOccupancyInput): string;
|
|
70
230
|
export declare function readOccupancy(projectRoot: string): OccupancyRecord | null;
|
|
71
|
-
export declare function liveOccupant(projectRoot: string, now?: Date, ttlMs?: number): OccupancyRecord | null;
|
|
231
|
+
export declare function liveOccupant(projectRoot: string, now?: Date, ttlMs?: number, maxLeaseMs?: number): OccupancyRecord | null;
|
|
72
232
|
export declare function applyWorktreeOccupancy(projectRoot: string, input?: ApplyOccupancyInput): OccupancyDecision;
|
|
73
233
|
export declare function stealOccupancy(projectRoot: string, input?: ApplyOccupancyInput): OccupancyDecision;
|
|
74
234
|
export declare function releaseOccupancy(projectRoot: string, input?: {
|
|
@@ -78,15 +238,81 @@ export declare function releaseOccupancy(projectRoot: string, input?: {
|
|
|
78
238
|
readonly swarmCloseout?: boolean;
|
|
79
239
|
readonly lockDeps?: LockDeps;
|
|
80
240
|
}): OccupancyDecision;
|
|
81
|
-
export
|
|
241
|
+
export interface OccupancyMembershipInput {
|
|
242
|
+
/** The owner issuing or withdrawing the grant; never the child. */
|
|
243
|
+
readonly sessionId?: string;
|
|
244
|
+
readonly childSessionId?: string;
|
|
245
|
+
readonly role?: string;
|
|
246
|
+
readonly worktreePath?: string;
|
|
247
|
+
/** Explicit end of the grant; clamped to the owner's own lease cap. */
|
|
248
|
+
readonly expiresAt?: Date;
|
|
249
|
+
readonly ttlMs?: number;
|
|
250
|
+
readonly host?: string;
|
|
251
|
+
readonly address?: string;
|
|
252
|
+
readonly joinProtocol?: OccupancyJoinProtocol;
|
|
253
|
+
readonly env?: NodeJS.ProcessEnv;
|
|
254
|
+
readonly now?: Date;
|
|
255
|
+
readonly lockDeps?: LockDeps;
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Admit a dispatched child to this lease for writes (#3755).
|
|
259
|
+
*
|
|
260
|
+
* Owner-only, and the record is the point: a child that writes here is named
|
|
261
|
+
* on the lease it writes under, so an unexpected edit resolves to a session, a
|
|
262
|
+
* role and a tree instead of to "somebody who had the string". The grant cannot
|
|
263
|
+
* outlive the lease that issued it — expiry is clamped to the absolute lease
|
|
264
|
+
* cap — and it buys writes alone.
|
|
265
|
+
*/
|
|
266
|
+
export declare function grantOccupancyMembership(projectRoot: string, input?: OccupancyMembershipInput): OccupancyDecision;
|
|
267
|
+
/**
|
|
268
|
+
* Withdraw a child's grant early (#3755). Expiry already bounds every grant, so
|
|
269
|
+
* this exists for the case expiry cannot serve: the child finished, or should
|
|
270
|
+
* never have been admitted, and the owner wants that true now.
|
|
271
|
+
*/
|
|
272
|
+
export declare function revokeOccupancyMembership(projectRoot: string, input?: OccupancyMembershipInput): OccupancyDecision;
|
|
273
|
+
export interface OccupancyWriteGateResult {
|
|
274
|
+
readonly allow: boolean;
|
|
275
|
+
readonly message: string | null;
|
|
276
|
+
readonly occupant: OccupancyRecord | null;
|
|
277
|
+
/** True when this call re-stamped the owner's lease (#3599). */
|
|
278
|
+
readonly refreshed: boolean;
|
|
279
|
+
/** Set when the owner's own lease was inside the staleness window (#3599). */
|
|
280
|
+
readonly warning: string | null;
|
|
281
|
+
/** Why the write was admitted, or null on a refusal (#3755). */
|
|
282
|
+
readonly admitted: Exclude<OccupancyAdmission, "stranger"> | null;
|
|
283
|
+
/** The grant that admitted a member, or null (#3755). */
|
|
284
|
+
readonly grant: OccupancyGrant | null;
|
|
285
|
+
}
|
|
286
|
+
export interface OccupancyWriteGateInput {
|
|
82
287
|
readonly sessionId?: string;
|
|
83
288
|
readonly env?: NodeJS.ProcessEnv;
|
|
84
289
|
readonly now?: Date;
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
290
|
+
/**
|
|
291
|
+
* Re-stamp the owner's lease on the same-session allow (#3599). Off by
|
|
292
|
+
* default so the gate stays a pure read for callers that only probe; the
|
|
293
|
+
* dispatcher turns it on for the evaluation that immediately precedes an
|
|
294
|
+
* allowed write, so the stamp records a write that actually happened.
|
|
295
|
+
*/
|
|
296
|
+
readonly refresh?: boolean;
|
|
297
|
+
readonly lockDeps?: LockDeps;
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Decide whether the presented session may write, and — on the owner-allow
|
|
301
|
+
* path — keep the owner's lease alive (#3599).
|
|
302
|
+
*
|
|
303
|
+
* Before this, the gate was read-only on owner-allow, so the one event that
|
|
304
|
+
* proves the owner is alive did not extend its lease: the live window was
|
|
305
|
+
* twenty minutes from claim, once, regardless of how long the session worked.
|
|
306
|
+
*/
|
|
307
|
+
export declare function evaluateOccupancyWriteGate(projectRoot: string, input?: OccupancyWriteGateInput): OccupancyWriteGateResult;
|
|
308
|
+
/**
|
|
309
|
+
* Refresh the caller's own live lease (#3599). Discoverable counterpart to the
|
|
310
|
+
* automatic write-gate refresh, for sessions whose work is long and quiet:
|
|
311
|
+
* reading, building, or waiting produces no gated write to ride on.
|
|
312
|
+
*
|
|
313
|
+
* Never claims and never mints an owner — an unheld or foreign lease is denied.
|
|
314
|
+
*/
|
|
315
|
+
export declare function heartbeatOccupancy(projectRoot: string, input?: ApplyOccupancyInput): OccupancyDecision;
|
|
90
316
|
/** Close-out identity comes from the launch manifest or DEFT_SESSION_ID — never occupancy.json. */
|
|
91
317
|
export declare function releaseSwarmOccupancy(projectRoot: string, input?: {
|
|
92
318
|
readonly sessionId?: string;
|