@deftai/directive-core 0.108.0 → 0.109.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (157) hide show
  1. package/dist/cache/operations.js +1 -1
  2. package/dist/check/gate-lists.d.ts +14 -0
  3. package/dist/check/gate-lists.js +26 -3
  4. package/dist/check/session-completed-ac.d.ts +1 -1
  5. package/dist/check/session-completed-ac.js +1 -1
  6. package/dist/consumer-check-contract/evaluate.d.ts +47 -0
  7. package/dist/consumer-check-contract/evaluate.js +181 -15
  8. package/dist/delivery-attempt/evaluate.d.ts +9 -1
  9. package/dist/delivery-attempt/evaluate.js +69 -0
  10. package/dist/delivery-attempt/index.d.ts +1 -1
  11. package/dist/delivery-attempt/index.js +1 -1
  12. package/dist/deposit/live-procedure-exclusions.d.ts +18 -0
  13. package/dist/deposit/live-procedure-exclusions.js +110 -0
  14. package/dist/deposit/live-procedure-targets.d.ts +45 -0
  15. package/dist/deposit/live-procedure-targets.js +274 -0
  16. package/dist/deposit/python-free.d.ts +6 -0
  17. package/dist/deposit/python-free.js +15 -0
  18. package/dist/deposit/rewrite-deposit-links.d.ts +42 -0
  19. package/dist/deposit/rewrite-deposit-links.js +148 -0
  20. package/dist/deposit/run-stage-content-pack.d.ts +2 -0
  21. package/dist/deposit/run-stage-content-pack.js +3 -0
  22. package/dist/deposit/stage-content-pack.d.ts +17 -0
  23. package/dist/deposit/stage-content-pack.js +91 -0
  24. package/dist/design-critique/citation-grammar.d.ts +7 -0
  25. package/dist/design-critique/citation-grammar.js +1 -1
  26. package/dist/design-critique/completed-arc-record.d.ts +8 -1
  27. package/dist/design-critique/completed-arc-record.js +98 -14
  28. package/dist/hooks/classify/host-session-identity.d.ts +25 -12
  29. package/dist/hooks/classify/host-session-identity.js +64 -49
  30. package/dist/hooks/classify/index.d.ts +2 -2
  31. package/dist/hooks/classify/index.js +2 -2
  32. package/dist/hooks/classify/paths.d.ts +2 -0
  33. package/dist/hooks/classify/paths.js +8 -4
  34. package/dist/hooks/classify/stdin.js +69 -3
  35. package/dist/hooks/dest-form.d.ts +20 -1
  36. package/dist/hooks/dest-form.js +158 -21
  37. package/dist/hooks/dispatcher.d.ts +22 -2
  38. package/dist/hooks/dispatcher.js +338 -66
  39. package/dist/hooks/fixtures/cases.d.ts +6 -0
  40. package/dist/hooks/fixtures/cases.js +53 -0
  41. package/dist/hooks/git-destructive-log.d.ts +32 -0
  42. package/dist/hooks/git-destructive-log.js +46 -0
  43. package/dist/hooks/index.d.ts +3 -0
  44. package/dist/hooks/index.js +3 -0
  45. package/dist/hooks/owner-liveness.d.ts +92 -0
  46. package/dist/hooks/owner-liveness.js +103 -0
  47. package/dist/hooks/shell-write-targets.d.ts +10 -0
  48. package/dist/hooks/shell-write-targets.js +274 -0
  49. package/dist/hooks/tools.d.ts +49 -2
  50. package/dist/hooks/tools.js +96 -1
  51. package/dist/init-deposit/agent-hooks.d.ts +10 -0
  52. package/dist/init-deposit/agent-hooks.js +39 -0
  53. package/dist/init-deposit/gitignore.d.ts +7 -0
  54. package/dist/init-deposit/gitignore.js +24 -0
  55. package/dist/init-deposit/host-tool-coverage.d.ts +53 -0
  56. package/dist/init-deposit/host-tool-coverage.js +150 -0
  57. package/dist/init-deposit/index.d.ts +1 -0
  58. package/dist/init-deposit/index.js +1 -0
  59. package/dist/init-deposit/init-deposit.js +3 -0
  60. package/dist/init-deposit/refresh.js +7 -0
  61. package/dist/init-deposit/runtime-writers.d.ts +14 -0
  62. package/dist/init-deposit/runtime-writers.js +33 -0
  63. package/dist/intake/issue-ingest.d.ts +6 -1
  64. package/dist/intake/issue-ingest.js +21 -2
  65. package/dist/intake/reconcile-issues.js +13 -13
  66. package/dist/lifecycle/brief-envelope.d.ts +25 -0
  67. package/dist/lifecycle/brief-envelope.js +42 -0
  68. package/dist/lifecycle/index.d.ts +1 -0
  69. package/dist/lifecycle/index.js +1 -0
  70. package/dist/literal-acceptance/evaluate.js +14 -5
  71. package/dist/literal-acceptance/index.d.ts +1 -1
  72. package/dist/literal-acceptance/index.js +1 -1
  73. package/dist/literal-acceptance/run.d.ts +2 -0
  74. package/dist/literal-acceptance/run.js +19 -1
  75. package/dist/orchestration/subagent-monitor.d.ts +6 -0
  76. package/dist/orchestration/subagent-monitor.js +23 -1
  77. package/dist/orphan-active/candidate-scope.d.ts +53 -0
  78. package/dist/orphan-active/candidate-scope.js +157 -0
  79. package/dist/orphan-active/evaluate.d.ts +27 -0
  80. package/dist/orphan-active/evaluate.js +63 -6
  81. package/dist/orphan-active/index.d.ts +1 -0
  82. package/dist/orphan-active/index.js +1 -0
  83. package/dist/policy/merge-approval-head.js +7 -6
  84. package/dist/pr-closing-keywords/gh.js +32 -9
  85. package/dist/pr-closing-keywords/main.d.ts +1 -0
  86. package/dist/pr-closing-keywords/main.js +68 -3
  87. package/dist/pr-closing-keywords/types.d.ts +2 -0
  88. package/dist/pr-merge-readiness/gh.js +32 -9
  89. package/dist/pr-protected-issues/gh.js +36 -9
  90. package/dist/pr-wait-mergeable/wrappers.js +3 -3
  91. package/dist/product-first-done-gate/evaluate.js +6 -2
  92. package/dist/product-first-done-gate/types.js +2 -0
  93. package/dist/release/consumer-hard-stops.d.ts +48 -0
  94. package/dist/release/consumer-hard-stops.js +140 -0
  95. package/dist/release/consumer-readiness-disclosure.d.ts +18 -0
  96. package/dist/release/consumer-readiness-disclosure.js +51 -0
  97. package/dist/release/index.d.ts +2 -0
  98. package/dist/release/index.js +2 -0
  99. package/dist/release/pipeline.js +55 -0
  100. package/dist/release/run-consumer-readiness.d.ts +15 -0
  101. package/dist/release/run-consumer-readiness.js +24 -0
  102. package/dist/release/types.d.ts +6 -0
  103. package/dist/review-monitor/github-lease.js +4 -3
  104. package/dist/run-summary/types.d.ts +2 -2
  105. package/dist/scm/build-command.d.ts +2 -2
  106. package/dist/scm/build-command.js +2 -2
  107. package/dist/scm/call-shape.d.ts +25 -0
  108. package/dist/scm/call-shape.js +59 -0
  109. package/dist/scm/call.d.ts +7 -4
  110. package/dist/scm/call.js +41 -11
  111. package/dist/scm/gh-rest.d.ts +25 -12
  112. package/dist/scm/gh-rest.js +65 -15
  113. package/dist/scm/index.d.ts +2 -0
  114. package/dist/scm/index.js +2 -0
  115. package/dist/scm/spawn-status.d.ts +33 -0
  116. package/dist/scm/spawn-status.js +53 -0
  117. package/dist/scope/acceptance-evidence.d.ts +1 -1
  118. package/dist/scope/transition.js +4 -14
  119. package/dist/session/ac-pass-banking.d.ts +2 -2
  120. package/dist/session/ac-pass-banking.js +2 -2
  121. package/dist/session/child-occupancy.d.ts +72 -0
  122. package/dist/session/child-occupancy.js +209 -0
  123. package/dist/session/host-session-owner.d.ts +93 -0
  124. package/dist/session/host-session-owner.js +148 -0
  125. package/dist/session/index.d.ts +2 -0
  126. package/dist/session/index.js +2 -0
  127. package/dist/session/occupancy.d.ts +107 -2
  128. package/dist/session/occupancy.js +272 -34
  129. package/dist/session/verify-ac-session-cache.d.ts +2 -2
  130. package/dist/session/verify-ac-session-cache.js +2 -2
  131. package/dist/swarm/complete-cohort.js +2 -0
  132. package/dist/swarm/pre-dispatch.js +2 -0
  133. package/dist/swarm/subagent-status-dir.d.ts +2 -1
  134. package/dist/swarm/subagent-status-dir.js +11 -2
  135. package/dist/swarm/worktrees.js +2 -0
  136. package/dist/triage/evaluate/worktrees.js +153 -6
  137. package/dist/umbrella-current-shape/index.d.ts +51 -3
  138. package/dist/umbrella-current-shape/index.js +106 -18
  139. package/dist/validate-content/deposit-required.d.ts +39 -0
  140. package/dist/validate-content/deposit-required.js +147 -0
  141. package/dist/validate-content/index.d.ts +1 -0
  142. package/dist/validate-content/index.js +1 -0
  143. package/dist/validate-content/validate-links.d.ts +2 -3
  144. package/dist/validate-content/validate-links.js +29 -2
  145. package/dist/vbrief-activate/activate.d.ts +7 -2
  146. package/dist/vbrief-activate/activate.js +26 -13
  147. package/dist/verify-ac/evaluate.d.ts +9 -0
  148. package/dist/verify-ac/evaluate.js +32 -9
  149. package/dist/verify-env/agent-hooks.d.ts +6 -1
  150. package/dist/verify-env/agent-hooks.js +28 -2
  151. package/dist/verify-source/deposit-closure.d.ts +23 -0
  152. package/dist/verify-source/deposit-closure.js +162 -0
  153. package/dist/verify-source/index.d.ts +2 -0
  154. package/dist/verify-source/index.js +2 -0
  155. package/dist/verify-source/semantic-single-source.d.ts +36 -0
  156. package/dist/verify-source/semantic-single-source.js +349 -0
  157. package/package.json +3 -3
@@ -702,6 +702,59 @@ export const HOOK_FIXTURE_CASES = [
702
702
  hostIdentity: { status: "invalid", sessionId: null },
703
703
  },
704
704
  },
705
+ // --- Host-env session identity (#3873) ---
706
+ {
707
+ // The claim side of the loop: without this rewrite the lease is claimed
708
+ // under a minted UUID that the later write gate can never present.
709
+ id: "grok-posix-shell-host-env-lifecycle-rewrite",
710
+ host: "grok",
711
+ os: "posix",
712
+ tool: "Shell",
713
+ regression: ["#3873", "#3611"],
714
+ payload: {
715
+ tool_name: "Bash",
716
+ tool_input: { command: "deft session:start" },
717
+ },
718
+ environ: { GROK_SESSION_ID: "grok-session-a" },
719
+ expected: {
720
+ toolName: "Bash",
721
+ writeIntent: "shell",
722
+ writeTargetPath: null,
723
+ hostIdentity: {
724
+ status: "ok",
725
+ sessionId: "host:grok:v1:Z3Jvay1zZXNzaW9uLWE",
726
+ },
727
+ lifecycle: {
728
+ verb: "session:start",
729
+ requestedSessionId: "host:grok:v1:Z3Jvay1zZXNzaW9uLWE",
730
+ resultKind: "rewrite",
731
+ rewrittenCommand: "deft session:start --session-id=host:grok:v1:Z3Jvay1zZXNzaW9uLWE",
732
+ updatedInput: {
733
+ command: "deft session:start --session-id=host:grok:v1:Z3Jvay1zZXNzaW9uLWE",
734
+ },
735
+ },
736
+ },
737
+ },
738
+ {
739
+ id: "grok-posix-write-payload-session-id-untrusted",
740
+ host: "grok",
741
+ os: "posix",
742
+ tool: "Write",
743
+ regression: ["#3873"],
744
+ payload: {
745
+ tool_name: "Write",
746
+ session_id: "payload-owner",
747
+ tool_input: { content: "x", file_path: "src/app.ts" },
748
+ },
749
+ environ: {},
750
+ expected: {
751
+ toolName: "Write",
752
+ writeIntent: "direct-write",
753
+ writeTargetPath: "src/app.ts",
754
+ hostIdentity: { status: "missing", sessionId: null },
755
+ },
756
+ notes: "Grok's payload session_id contract is unverified; only the host variable is read",
757
+ },
705
758
  // --- Regression: empty / missing tool name ---
706
759
  {
707
760
  id: "cursor-posix-stdin-empty",
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Durable attribution log for tree-wide destructive git at the write fence (#3917).
3
+ *
4
+ * Lives outside the repository so a `reset --hard` or a worktree prune cannot
5
+ * erase it. This is detection, not a root-cause claim.
6
+ */
7
+ import type { GitDestructiveKind } from "./dest-form.js";
8
+ export declare const GIT_DESTRUCTIVE_LOG_ENV = "DEFT_GIT_DESTRUCTIVE_LOG";
9
+ export type GitDestructiveDisposition = "deny" | "allow-fixture";
10
+ export interface GitDestructiveRecord {
11
+ readonly ts: string;
12
+ readonly kind: GitDestructiveKind;
13
+ readonly disposition: GitDestructiveDisposition;
14
+ readonly command: string;
15
+ readonly projectRoot: string;
16
+ readonly host: string;
17
+ readonly toolName: string | null;
18
+ readonly actor: string;
19
+ readonly pid: number;
20
+ readonly relocators: readonly string[];
21
+ readonly unprovable: boolean;
22
+ }
23
+ export declare function resolveGitDestructiveLogPath(env?: NodeJS.ProcessEnv, platform?: NodeJS.Platform, homeDir?: string): string;
24
+ /**
25
+ * Best-effort append. A log failure must not block the deny.
26
+ */
27
+ export declare function appendGitDestructiveRecord(record: GitDestructiveRecord, options?: {
28
+ readonly logPath?: string;
29
+ readonly env?: NodeJS.ProcessEnv;
30
+ readonly append?: (path: string, line: string) => void;
31
+ }): void;
32
+ //# sourceMappingURL=git-destructive-log.d.ts.map
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Durable attribution log for tree-wide destructive git at the write fence (#3917).
3
+ *
4
+ * Lives outside the repository so a `reset --hard` or a worktree prune cannot
5
+ * erase it. This is detection, not a root-cause claim.
6
+ */
7
+ import { mkdirSync } from "node:fs";
8
+ import { homedir } from "node:os";
9
+ import { dirname, join, resolve } from "node:path";
10
+ import { containedWrite } from "../fs/contained-write.js";
11
+ import { platformUserConfigDir } from "../user-config/resolve-user-md.js";
12
+ export const GIT_DESTRUCTIVE_LOG_ENV = "DEFT_GIT_DESTRUCTIVE_LOG";
13
+ export function resolveGitDestructiveLogPath(env = process.env, platform = process.platform, homeDir = homedir()) {
14
+ const override = env[GIT_DESTRUCTIVE_LOG_ENV]?.trim();
15
+ if (override !== undefined && override.length > 0) {
16
+ return resolve(override);
17
+ }
18
+ return join(platformUserConfigDir(platform, env, homeDir), "logs", "git-destructive.jsonl");
19
+ }
20
+ function defaultAppend(path, line) {
21
+ const targetAbs = resolve(path);
22
+ const parent = dirname(targetAbs);
23
+ mkdirSync(parent, { recursive: true });
24
+ containedWrite({
25
+ root: parent,
26
+ target: targetAbs,
27
+ data: line,
28
+ mode: "append",
29
+ mkdir: false,
30
+ mutation: false,
31
+ });
32
+ }
33
+ /**
34
+ * Best-effort append. A log failure must not block the deny.
35
+ */
36
+ export function appendGitDestructiveRecord(record, options = {}) {
37
+ try {
38
+ const path = options.logPath ?? resolveGitDestructiveLogPath(options.env ?? process.env);
39
+ const line = `${JSON.stringify(record)}\n`;
40
+ (options.append ?? defaultAppend)(path, line);
41
+ }
42
+ catch {
43
+ // Attribution is best-effort; the fail-closed deny still stands.
44
+ }
45
+ }
46
+ //# sourceMappingURL=git-destructive-log.js.map
@@ -3,5 +3,8 @@ export * from "./cursor-hooks.js";
3
3
  export * from "./dest-form.js";
4
4
  export * from "./dispatcher.js";
5
5
  export * from "./fixtures/index.js";
6
+ export * from "./git-destructive-log.js";
7
+ export * from "./owner-liveness.js";
6
8
  export * from "./scope.js";
9
+ export * from "./shell-write-targets.js";
7
10
  //# sourceMappingURL=index.d.ts.map
@@ -3,5 +3,8 @@ export * from "./cursor-hooks.js";
3
3
  export * from "./dest-form.js";
4
4
  export * from "./dispatcher.js";
5
5
  export * from "./fixtures/index.js";
6
+ export * from "./git-destructive-log.js";
7
+ export * from "./owner-liveness.js";
6
8
  export * from "./scope.js";
9
+ export * from "./shell-write-targets.js";
7
10
  //# sourceMappingURL=index.js.map
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Owner liveness on non-write hook activity (#3987).
3
+ *
4
+ * The occupancy lease renews on one signal: a gated product write
5
+ * (`evaluateOccupancyWriteGate`, refresh path, #3599). After #3990 widened the
6
+ * PreToolUse matchers, coverage is necessary and still not sufficient, because
7
+ * eligibility — not coverage — is what keeps shell traffic off that path. Only
8
+ * five write verbs are recognized, and a dest carrying `$` / `*` / `?`, a temp
9
+ * path, an out-of-root target, or any compound command is refused before the
10
+ * gate re-stamps. `cd <root>; <command>` is compound, and the mandated Windows
11
+ * body-file flow (#2646 / #2744) is compound, `$`-bearing and temp-targeted at
12
+ * once. So an owner can commit and push all session and still starve its own
13
+ * lease, which is the data-loss half of #3987.
14
+ *
15
+ * The remedy is the one `occupancy.ts` already names in the
16
+ * `OCCUPANCY_MAX_LEASE_MS` docblock: "refresh on non-write activity". The hook
17
+ * fires on every matched tool call and resolves the actor from the host
18
+ * payload, so the owner's presence is already proven there — no new identity
19
+ * has to travel anywhere.
20
+ *
21
+ * Bounds, each one load-bearing (5471374558 F4):
22
+ *
23
+ * - Host-authoritative actor only. An ambient inherited `DEFT_SESSION_ID` is
24
+ * never enough. On Grok the owner id is a hook-sibling variable, and
25
+ * publishing it so an agent shell could present it would hand lease renewal
26
+ * to every descendant process — the widening the arc withdrew.
27
+ * - Owner only, via `heartbeatOccupancy`: it never claims, never mints, never
28
+ * resurrects, and refuses a foreign or expired lease. Reusing it keeps one
29
+ * renewal path with one set of refusal semantics.
30
+ * - `markWrite` stays false, so `last_write_at` still means "a product write
31
+ * was recorded" and `no recorded write` stays honest for a would-be stealer.
32
+ * - Keyed on the lease's own worktree path, so a record describing another tree
33
+ * is not renewed from this one. The caller supplies the tree the mutation
34
+ * gates authorized against — a linked worktree, not the payload root — so a
35
+ * worktree write renews the worktree's lease rather than the primary's.
36
+ * - `claimed_at` is untouched, so `OCCUPANCY_MAX_LEASE_MS` — 12 hours — is
37
+ * unmoved. Liveness renewal cannot outlive the absolute cap.
38
+ * - Runs after the decision, never as an input to it. A liveness re-stamp must
39
+ * not change any verdict, and re-stamping before the mutation gates would
40
+ * suppress their own `markWrite = true` refresh by resetting the age floor.
41
+ *
42
+ * Bound this deliberately does not cross: a call with no write target proves
43
+ * only the tree the host named. `projectRootFromHookPayload` takes that from
44
+ * the payload's own `cwd`-class fields, so a session working inside a linked
45
+ * worktree names the worktree and renews it; a session whose host reports the
46
+ * primary checkout renews the primary. When those differ and nothing in the
47
+ * payload says which tree the work is in, this renews neither by guessing —
48
+ * keeping a lease alive for a tree nobody occupies is a widening, not a fix,
49
+ * and the TTL reclaiming an unused tree is the behaviour the lease exists for.
50
+ * `deft occupancy:heartbeat --session-id <owner>` stays the explicit path for
51
+ * a session whose tree the host does not report.
52
+ */
53
+ import type { LockDeps } from "../slice/lock.js";
54
+ export type OwnerLivenessSkipReason =
55
+ /** No host-authoritative owner resolved; ambient identity is not accepted. */
56
+ "no-host-authoritative-owner"
57
+ /** Nothing to renew: no lease, or the lease is expired or age-capped. */
58
+ | "no-live-lease"
59
+ /** A lease exists but this actor is not its owner. */
60
+ | "not-owner"
61
+ /** The lease record describes a different worktree than the one hooked. */
62
+ | "foreign-worktree"
63
+ /** Younger than the shared re-stamp floor; renewing would rewrite per keystroke. */
64
+ | "within-refresh-floor"
65
+ /** The lock was busy or the lease changed under us; the lease is left alone. */
66
+ | "refresh-unavailable";
67
+ export type OwnerLivenessOutcome = {
68
+ readonly restamped: true;
69
+ readonly sessionId: string;
70
+ readonly heartbeatAt: Date;
71
+ } | {
72
+ readonly restamped: false;
73
+ readonly reason: OwnerLivenessSkipReason;
74
+ };
75
+ export interface OwnerLivenessInput {
76
+ /** Tree the hook fired against; also the tree whose lease may be renewed. */
77
+ readonly projectRoot: string;
78
+ /** Owner id the host payload resolved to, or undefined when none did. */
79
+ readonly ownerSessionId: string | undefined;
80
+ /** False for ambient/environment identity — such an actor never renews. */
81
+ readonly hostAuthoritative: boolean;
82
+ readonly now?: Date;
83
+ readonly lockDeps?: LockDeps;
84
+ }
85
+ /**
86
+ * Renew the owner's lease from a hook event that proved the owner is alive.
87
+ *
88
+ * Pure side effect on `occupancy.json`: the caller's decision is already made
89
+ * and this never alters it. Every refusal path leaves the lease untouched.
90
+ */
91
+ export declare function restampOwnerLivenessOnHookEvent(input: OwnerLivenessInput): OwnerLivenessOutcome;
92
+ //# sourceMappingURL=owner-liveness.d.ts.map
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Owner liveness on non-write hook activity (#3987).
3
+ *
4
+ * The occupancy lease renews on one signal: a gated product write
5
+ * (`evaluateOccupancyWriteGate`, refresh path, #3599). After #3990 widened the
6
+ * PreToolUse matchers, coverage is necessary and still not sufficient, because
7
+ * eligibility — not coverage — is what keeps shell traffic off that path. Only
8
+ * five write verbs are recognized, and a dest carrying `$` / `*` / `?`, a temp
9
+ * path, an out-of-root target, or any compound command is refused before the
10
+ * gate re-stamps. `cd <root>; <command>` is compound, and the mandated Windows
11
+ * body-file flow (#2646 / #2744) is compound, `$`-bearing and temp-targeted at
12
+ * once. So an owner can commit and push all session and still starve its own
13
+ * lease, which is the data-loss half of #3987.
14
+ *
15
+ * The remedy is the one `occupancy.ts` already names in the
16
+ * `OCCUPANCY_MAX_LEASE_MS` docblock: "refresh on non-write activity". The hook
17
+ * fires on every matched tool call and resolves the actor from the host
18
+ * payload, so the owner's presence is already proven there — no new identity
19
+ * has to travel anywhere.
20
+ *
21
+ * Bounds, each one load-bearing (5471374558 F4):
22
+ *
23
+ * - Host-authoritative actor only. An ambient inherited `DEFT_SESSION_ID` is
24
+ * never enough. On Grok the owner id is a hook-sibling variable, and
25
+ * publishing it so an agent shell could present it would hand lease renewal
26
+ * to every descendant process — the widening the arc withdrew.
27
+ * - Owner only, via `heartbeatOccupancy`: it never claims, never mints, never
28
+ * resurrects, and refuses a foreign or expired lease. Reusing it keeps one
29
+ * renewal path with one set of refusal semantics.
30
+ * - `markWrite` stays false, so `last_write_at` still means "a product write
31
+ * was recorded" and `no recorded write` stays honest for a would-be stealer.
32
+ * - Keyed on the lease's own worktree path, so a record describing another tree
33
+ * is not renewed from this one. The caller supplies the tree the mutation
34
+ * gates authorized against — a linked worktree, not the payload root — so a
35
+ * worktree write renews the worktree's lease rather than the primary's.
36
+ * - `claimed_at` is untouched, so `OCCUPANCY_MAX_LEASE_MS` — 12 hours — is
37
+ * unmoved. Liveness renewal cannot outlive the absolute cap.
38
+ * - Runs after the decision, never as an input to it. A liveness re-stamp must
39
+ * not change any verdict, and re-stamping before the mutation gates would
40
+ * suppress their own `markWrite = true` refresh by resetting the age floor.
41
+ *
42
+ * Bound this deliberately does not cross: a call with no write target proves
43
+ * only the tree the host named. `projectRootFromHookPayload` takes that from
44
+ * the payload's own `cwd`-class fields, so a session working inside a linked
45
+ * worktree names the worktree and renews it; a session whose host reports the
46
+ * primary checkout renews the primary. When those differ and nothing in the
47
+ * payload says which tree the work is in, this renews neither by guessing —
48
+ * keeping a lease alive for a tree nobody occupies is a widening, not a fix,
49
+ * and the TTL reclaiming an unused tree is the behaviour the lease exists for.
50
+ * `deft occupancy:heartbeat --session-id <owner>` stays the explicit path for
51
+ * a session whose tree the host does not report.
52
+ */
53
+ import { resolve } from "node:path";
54
+ import { heartbeatOccupancy, isOccupancyExpired, OCCUPANCY_REFRESH_AFTER_MS, readOccupancy, } from "../session/occupancy.js";
55
+ function skip(reason) {
56
+ return { restamped: false, reason };
57
+ }
58
+ /**
59
+ * Renew the owner's lease from a hook event that proved the owner is alive.
60
+ *
61
+ * Pure side effect on `occupancy.json`: the caller's decision is already made
62
+ * and this never alters it. Every refusal path leaves the lease untouched.
63
+ */
64
+ export function restampOwnerLivenessOnHookEvent(input) {
65
+ if (!input.hostAuthoritative)
66
+ return skip("no-host-authoritative-owner");
67
+ const owner = input.ownerSessionId?.trim() ?? "";
68
+ if (owner.length === 0)
69
+ return skip("no-host-authoritative-owner");
70
+ const root = resolve(input.projectRoot);
71
+ const now = input.now ?? new Date();
72
+ const record = readOccupancy(root);
73
+ if (record === null || isOccupancyExpired(record, now))
74
+ return skip("no-live-lease");
75
+ // Owner only. A granted member's presence is not the owner's presence, and
76
+ // `heartbeatOccupancy` refuses it anyway — checking here keeps the reason honest.
77
+ if (record.sessionId !== owner)
78
+ return skip("not-owner");
79
+ if (resolve(record.worktreePath) !== root)
80
+ return skip("foreign-worktree");
81
+ // Same floor the write gate uses, for the same reason: the hook runs on a
82
+ // large fraction of tool calls, so an unconditional renew would rewrite the
83
+ // lease file continuously without lengthening the safe window at all.
84
+ if (now.getTime() - record.heartbeatAt.getTime() < OCCUPANCY_REFRESH_AFTER_MS) {
85
+ return skip("within-refresh-floor");
86
+ }
87
+ const beat = heartbeatOccupancy(root, {
88
+ sessionId: owner,
89
+ // Never fall back to an ambient owner: the resolved host identity is the
90
+ // only thing that proves who is here.
91
+ env: {},
92
+ now,
93
+ lockDeps: input.lockDeps,
94
+ });
95
+ if (beat.code !== 0 || beat.record === null)
96
+ return skip("refresh-unavailable");
97
+ return {
98
+ restamped: true,
99
+ sessionId: beat.record.sessionId,
100
+ heartbeatAt: beat.record.heartbeatAt,
101
+ };
102
+ }
103
+ //# sourceMappingURL=owner-liveness.js.map
@@ -0,0 +1,10 @@
1
+ export declare const SHELL_WRITE_KINDS: readonly ["set-content", "out-file", "add-content", "python-pathlib", "io-writealltext"];
2
+ export type ShellWriteKind = (typeof SHELL_WRITE_KINDS)[number];
3
+ export interface ShellWriteTarget {
4
+ readonly kind: ShellWriteKind;
5
+ readonly path: string;
6
+ readonly unprovable?: boolean;
7
+ }
8
+ export declare function classifyShellWriteTargets(command: string): ShellWriteTarget[];
9
+ export declare function isInRepoShellWritePath(projectRoot: string, dest: string): boolean;
10
+ //# sourceMappingURL=shell-write-targets.d.ts.map
@@ -0,0 +1,274 @@
1
+ /** Recognized in-repo Shell file-write dests (#3983 / #3987). */
2
+ import { isAbsolute, relative, resolve } from "node:path";
3
+ export const SHELL_WRITE_KINDS = [
4
+ "set-content",
5
+ "out-file",
6
+ "add-content",
7
+ "python-pathlib",
8
+ "io-writealltext",
9
+ ];
10
+ const DQ = String.fromCharCode(34);
11
+ const AQ = String.fromCharCode(39);
12
+ function stripQuotes(token) {
13
+ const t = token.trim();
14
+ if (t.length < 2)
15
+ return t;
16
+ const a = t[0];
17
+ const b = t[t.length - 1];
18
+ if ((a === DQ && b === DQ) || (a === AQ && b === AQ))
19
+ return t.slice(1, -1);
20
+ return t;
21
+ }
22
+ function pushUnique(out, kind, raw) {
23
+ const path = stripQuotes(raw).trim();
24
+ if (path.length === 0)
25
+ return;
26
+ if (out.some((d) => d.kind === kind && d.path === path))
27
+ return;
28
+ out.push({ kind, path });
29
+ }
30
+ function extractQuoted(command, start) {
31
+ if (start >= command.length)
32
+ return { value: "", next: start };
33
+ const q = command[start];
34
+ if (q !== DQ && q !== AQ) {
35
+ let i = start;
36
+ while (i < command.length) {
37
+ const c = command[i];
38
+ if (c === undefined)
39
+ break;
40
+ if (c === " " || c === " " || c === "," || c === ")")
41
+ break;
42
+ i += 1;
43
+ }
44
+ return { value: command.slice(start, i), next: i };
45
+ }
46
+ let i = start + 1;
47
+ while (i < command.length && command[i] !== q)
48
+ i += 1;
49
+ return { value: command.slice(start + 1, i), next: i + 1 };
50
+ }
51
+ function skipWs(command, start) {
52
+ let i = start;
53
+ while (i < command.length) {
54
+ const code = command.charCodeAt(i);
55
+ if (code !== 32 && code !== 9 && code !== 10 && code !== 13)
56
+ break;
57
+ i += 1;
58
+ }
59
+ return i;
60
+ }
61
+ function startsWithVerb(segment, verb) {
62
+ const i = skipWs(segment, 0);
63
+ if (segment.slice(i, i + verb.length).toLowerCase() !== verb.toLowerCase())
64
+ return false;
65
+ const next = segment[i + verb.length];
66
+ return next === undefined || next === " " || next === " " || next === "-";
67
+ }
68
+ function splitWriteSegments(command) {
69
+ const segs = [];
70
+ let cur = "";
71
+ let quote = null;
72
+ for (let i = 0; i < command.length; i++) {
73
+ const c = command[i];
74
+ if (c === undefined)
75
+ break;
76
+ if (quote !== null) {
77
+ if (c === quote)
78
+ quote = null;
79
+ cur += c;
80
+ continue;
81
+ }
82
+ if (c === DQ || c === AQ) {
83
+ quote = c;
84
+ cur += c;
85
+ continue;
86
+ }
87
+ const code = command.charCodeAt(i);
88
+ const next = command.charCodeAt(i + 1);
89
+ if ((code === 38 && next === 38) || (code === 124 && next === 124)) {
90
+ segs.push(cur);
91
+ cur = "";
92
+ i += 1;
93
+ continue;
94
+ }
95
+ if (code === 124 || code === 38 || code === 59 || code === 10 || code === 13) {
96
+ segs.push(cur);
97
+ cur = "";
98
+ continue;
99
+ }
100
+ cur += c;
101
+ }
102
+ segs.push(cur);
103
+ return segs.map((seg) => seg.trim()).filter((seg) => seg.length > 0);
104
+ }
105
+ const PATH_PARAM_NAMES = ["path", "literalpath", "filepath"];
106
+ function uniqueNamedParam(flag, names) {
107
+ const f = flag.replace(/^-+/, "").toLowerCase();
108
+ if (f.length === 0)
109
+ return null;
110
+ const hits = names.filter((n) => n.startsWith(f));
111
+ return hits.length === 1 ? (hits[0] ?? null) : null;
112
+ }
113
+ function parseFlag(token) {
114
+ if (!token.startsWith("-"))
115
+ return null;
116
+ const colon = token.indexOf(":");
117
+ if (colon <= 0)
118
+ return { flag: token, attached: null };
119
+ return { flag: token.slice(0, colon), attached: token.slice(colon + 1) };
120
+ }
121
+ function extractAfterVerb(command, verb, kind, out) {
122
+ if (!startsWithVerb(command, verb))
123
+ return;
124
+ const i = skipWs(command, 0);
125
+ let j = skipWs(command, i + verb.length);
126
+ let named = null;
127
+ let positional = null;
128
+ while (j < command.length) {
129
+ const extracted = extractQuoted(command, j);
130
+ if (extracted.value.length === 0)
131
+ break;
132
+ j = skipWs(command, extracted.next);
133
+ const parsed = parseFlag(extracted.value);
134
+ if (parsed === null) {
135
+ if (positional === null)
136
+ positional = extracted.value;
137
+ continue;
138
+ }
139
+ let arg = parsed.attached;
140
+ if (arg === null || arg.length === 0) {
141
+ const next = extractQuoted(command, j);
142
+ if (next.value.length > 0 && !next.value.startsWith("-")) {
143
+ arg = next.value;
144
+ j = skipWs(command, next.next);
145
+ }
146
+ else {
147
+ arg = null;
148
+ }
149
+ }
150
+ if (uniqueNamedParam(parsed.flag, PATH_PARAM_NAMES) !== null) {
151
+ if (arg !== null && arg.length > 0 && !arg.startsWith("-"))
152
+ named = arg;
153
+ }
154
+ }
155
+ const dest = named ?? positional;
156
+ if (dest !== null && !dest.startsWith("-"))
157
+ pushUnique(out, kind, dest);
158
+ }
159
+ function pathlibWriteFollows(command, afterPathArgs) {
160
+ let i = skipWs(command, afterPathArgs);
161
+ if (command[i] === ")")
162
+ i = skipWs(command, i + 1);
163
+ while (i < command.length) {
164
+ if (command[i] === ";")
165
+ return false;
166
+ i = skipWs(command, i);
167
+ if (command[i] !== ".")
168
+ return false;
169
+ i += 1;
170
+ const start = i;
171
+ while (i < command.length) {
172
+ const c = command.charCodeAt(i);
173
+ if ((c >= 65 && c <= 90) || (c >= 97 && c <= 122) || c === 95) {
174
+ i += 1;
175
+ continue;
176
+ }
177
+ break;
178
+ }
179
+ const name = command.slice(start, i);
180
+ i = skipWs(command, i);
181
+ if (command[i] !== "(")
182
+ return false;
183
+ if (name === "write_text")
184
+ return true;
185
+ if (name === "write_bytes")
186
+ return true;
187
+ i += 1;
188
+ let depth = 1;
189
+ while (i < command.length && depth > 0) {
190
+ const ch = command[i];
191
+ if (ch === "(")
192
+ depth += 1;
193
+ else if (ch === ")")
194
+ depth -= 1;
195
+ i += 1;
196
+ }
197
+ }
198
+ return false;
199
+ }
200
+ function extractPythonPathlib(command, out) {
201
+ const i = skipWs(command, 0);
202
+ const head = command.slice(i, i + 10).toLowerCase();
203
+ if (!head.startsWith("python") && !head.startsWith("py ") && !head.startsWith("py.exe"))
204
+ return;
205
+ const needle = "Path(";
206
+ let from = i;
207
+ while (from < command.length) {
208
+ const at = command.indexOf(needle, from);
209
+ if (at < 0)
210
+ return;
211
+ let j = skipWs(command, at + needle.length);
212
+ const ch = command[j];
213
+ if (ch === "r" || ch === "R")
214
+ j = skipWs(command, j + 1);
215
+ const extracted = extractQuoted(command, j);
216
+ if (pathlibWriteFollows(command, extracted.next)) {
217
+ pushUnique(out, "python-pathlib", extracted.value);
218
+ }
219
+ from = at + needle.length;
220
+ }
221
+ }
222
+ function extractIoWrite(command, out) {
223
+ const i = skipWs(command, 0);
224
+ if (command[i] !== "[")
225
+ return;
226
+ const needles = ["WriteAllText(", "WriteAllBytes("];
227
+ for (const needle of needles) {
228
+ const at = command.indexOf(needle, i);
229
+ if (at < 0)
230
+ continue;
231
+ const j = skipWs(command, at + needle.length);
232
+ const extracted = extractQuoted(command, j);
233
+ pushUnique(out, "io-writealltext", extracted.value);
234
+ }
235
+ }
236
+ function classifySimpleWrite(command) {
237
+ const out = [];
238
+ extractAfterVerb(command, "Set-Content", "set-content", out);
239
+ extractAfterVerb(command, "Add-Content", "add-content", out);
240
+ extractAfterVerb(command, "Out-File", "out-file", out);
241
+ extractPythonPathlib(command, out);
242
+ extractIoWrite(command, out);
243
+ return out;
244
+ }
245
+ export function classifyShellWriteTargets(command) {
246
+ const cmd = command.trim();
247
+ if (cmd.length === 0)
248
+ return [];
249
+ const segments = splitWriteSegments(cmd);
250
+ if (segments.length === 1)
251
+ return classifySimpleWrite(segments[0] ?? cmd);
252
+ const found = [];
253
+ for (const seg of segments)
254
+ found.push(...classifySimpleWrite(seg));
255
+ if (found.length === 0)
256
+ return [];
257
+ return found.map((d) => ({ ...d, unprovable: true }));
258
+ }
259
+ export function isInRepoShellWritePath(projectRoot, dest) {
260
+ const path = dest.trim();
261
+ if (path.length === 0)
262
+ return false;
263
+ if (path.includes("*") || path.includes("?") || path.includes("$"))
264
+ return false;
265
+ const root = resolve(projectRoot);
266
+ const abs = isAbsolute(path) ? resolve(path) : resolve(root, path);
267
+ const rel = relative(root, abs);
268
+ if (rel.length === 0)
269
+ return false;
270
+ if (rel.startsWith("..") || isAbsolute(rel))
271
+ return false;
272
+ return true;
273
+ }
274
+ //# sourceMappingURL=shell-write-targets.js.map