@deftai/directive-core 0.90.0 → 0.92.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.
Files changed (101) hide show
  1. package/dist/cache/fetch.js +3 -2
  2. package/dist/cache/io.d.ts +13 -2
  3. package/dist/cache/io.js +36 -7
  4. package/dist/cache/operations.js +5 -3
  5. package/dist/doctor/main.js +37 -0
  6. package/dist/doctor/openclaw-skills.d.ts +42 -8
  7. package/dist/doctor/openclaw-skills.js +281 -38
  8. package/dist/eval/health.d.ts +10 -1
  9. package/dist/eval/health.js +16 -16
  10. package/dist/hooks/dispatcher.d.ts +6 -1
  11. package/dist/hooks/dispatcher.js +30 -0
  12. package/dist/init-deposit/gitignore.js +3 -0
  13. package/dist/init-deposit/hygiene.d.ts +43 -1
  14. package/dist/init-deposit/hygiene.js +112 -7
  15. package/dist/init-deposit/init-deposit.d.ts +5 -0
  16. package/dist/init-deposit/init-deposit.js +37 -2
  17. package/dist/init-deposit/scaffold.js +3 -1
  18. package/dist/intake/github-auth-modes.d.ts +1 -1
  19. package/dist/intake/github-auth-modes.js +25 -3
  20. package/dist/intake/issue-ingest-cli.js +13 -0
  21. package/dist/intake/reconcile-issues-cli.js +13 -0
  22. package/dist/orchestration/probe-session.d.ts +5 -1
  23. package/dist/orchestration/probe-session.js +24 -13
  24. package/dist/platform/agents-md.js +1 -1
  25. package/dist/policy/deft-directive-disable.d.ts +86 -0
  26. package/dist/policy/deft-directive-disable.js +167 -0
  27. package/dist/policy/delivery-branch.d.ts +33 -0
  28. package/dist/policy/delivery-branch.js +124 -0
  29. package/dist/policy/index.d.ts +2 -0
  30. package/dist/policy/index.js +17 -1
  31. package/dist/policy/plan-extensions.d.ts +10 -0
  32. package/dist/policy/plan-extensions.js +16 -0
  33. package/dist/pr-closing-keywords/detect.d.ts +13 -0
  34. package/dist/pr-closing-keywords/detect.js +73 -8
  35. package/dist/pr-closing-keywords/index.d.ts +2 -2
  36. package/dist/pr-closing-keywords/index.js +1 -1
  37. package/dist/pr-closing-keywords/main.js +119 -90
  38. package/dist/pr-closing-keywords/types.d.ts +10 -0
  39. package/dist/render/constants.d.ts +16 -0
  40. package/dist/render/constants.js +16 -1
  41. package/dist/render/export-spec.d.ts +7 -1
  42. package/dist/render/export-spec.js +51 -5
  43. package/dist/render/index.d.ts +3 -2
  44. package/dist/render/index.js +2 -2
  45. package/dist/render/roadmap-render.js +164 -60
  46. package/dist/render/scope-outlook.d.ts +5 -0
  47. package/dist/render/scope-outlook.js +3 -0
  48. package/dist/render/spec-render.d.ts +28 -2
  49. package/dist/render/spec-render.js +111 -14
  50. package/dist/render/text-utils.d.ts +6 -0
  51. package/dist/render/text-utils.js +23 -0
  52. package/dist/scm/binary.d.ts +3 -0
  53. package/dist/scm/binary.js +10 -2
  54. package/dist/scm/call.d.ts +5 -0
  55. package/dist/scm/call.js +5 -0
  56. package/dist/scm/index.d.ts +1 -0
  57. package/dist/scm/index.js +1 -0
  58. package/dist/scm/main.d.ts +5 -0
  59. package/dist/scm/main.js +27 -0
  60. package/dist/scm/readiness-cli.d.ts +24 -0
  61. package/dist/scm/readiness-cli.js +85 -0
  62. package/dist/scm/readiness.d.ts +104 -0
  63. package/dist/scm/readiness.js +421 -0
  64. package/dist/scope/batch-promote.d.ts +26 -0
  65. package/dist/scope/batch-promote.js +167 -0
  66. package/dist/scope/brief-io.d.ts +3 -1
  67. package/dist/scope/brief-io.js +5 -3
  68. package/dist/scope/delivery-evidence.d.ts +112 -0
  69. package/dist/scope/delivery-evidence.js +419 -0
  70. package/dist/scope/index.d.ts +2 -0
  71. package/dist/scope/index.js +2 -0
  72. package/dist/scope/main.d.ts +7 -0
  73. package/dist/scope/main.js +155 -9
  74. package/dist/scope/registry-artifact-sync.js +4 -1
  75. package/dist/scope/transition.d.ts +11 -1
  76. package/dist/scope/transition.js +30 -4
  77. package/dist/scope/wip-cap-check.d.ts +6 -0
  78. package/dist/scope/wip-cap-check.js +14 -2
  79. package/dist/session/ritual-sentinel.js +21 -12
  80. package/dist/session/session-start-hook.d.ts +3 -0
  81. package/dist/session/session-start-hook.js +21 -0
  82. package/dist/session/session-start.d.ts +7 -0
  83. package/dist/session/session-start.js +85 -0
  84. package/dist/swarm/complete-cohort.d.ts +20 -1
  85. package/dist/swarm/complete-cohort.js +56 -9
  86. package/dist/swarm/finalize-cohort-cli.js +9 -0
  87. package/dist/swarm/finalize-cohort.d.ts +8 -0
  88. package/dist/swarm/finalize-cohort.js +217 -21
  89. package/dist/triage/welcome/index.d.ts +1 -1
  90. package/dist/triage/welcome/index.js +1 -1
  91. package/dist/triage/welcome/onboard.js +7 -1
  92. package/dist/triage/welcome/prior-state.d.ts +7 -0
  93. package/dist/triage/welcome/prior-state.js +16 -2
  94. package/dist/triage/welcome/writers.d.ts +10 -0
  95. package/dist/triage/welcome/writers.js +72 -2
  96. package/dist/user-config/experimental-rules.d.ts +43 -0
  97. package/dist/user-config/experimental-rules.js +162 -0
  98. package/dist/user-config/index.d.ts +1 -0
  99. package/dist/user-config/index.js +1 -0
  100. package/dist/verify-source/contained-writes.js +1 -1
  101. package/package.json +3 -3
@@ -3,19 +3,22 @@ import { dirname, join } from "node:path";
3
3
  import { maybeRunStalenessTickler } from "../staleness-tickler/run.js";
4
4
  import { reconcileUmbrellas, renderUmbrellasReport } from "../vbrief-reconcile/umbrellas.js";
5
5
  import { canonicalLogPath, readAll } from "./audit-log.js";
6
+ import { batchPromote } from "./batch-promote.js";
6
7
  import { TRANSITIONS } from "./constants.js";
8
+ import { isNonDeliveryDisposition, } from "./delivery-evidence.js";
7
9
  import { batchDemote, DEFAULT_OLDER_THAN_DAYS, demoteOne, resolveDemoteFilePath, resolveFilePath, resolveProjectRootStrict, } from "./demote.js";
8
10
  import { completedPathForScopeMove, findOpenUmbrellaReferences, renderOpenUmbrellaWarning, } from "./open-umbrella-warning.js";
9
11
  import { resolveProjectRoot } from "./project-context.js";
10
12
  import { recordWipCapOverride, runTransition } from "./transition.js";
11
13
  import { findByDecisionId, isAlreadyUndone, REVERSIBLE_ACTIONS, undoBatch, undoOne, } from "./undo.js";
12
14
  import { checkWipCap, formatWipCapRefusal } from "./wip-cap-check.js";
13
- const LIFECYCLE_USAGE_STDERR = "usage: scope_lifecycle.py [-h] [--project-root PROJECT_ROOT] [--force]\n" +
15
+ const LIFECYCLE_USAGE_STDERR = "usage: scope_lifecycle.py [-h] [--project-root PROJECT_ROOT] [--force] [--batch]\n" +
14
16
  " {activate,block,cancel,complete,fail,promote,restore,unblock}\n" +
15
- " file\n" +
16
- "scope_lifecycle.py: error: the following arguments are required: action, file\n";
17
+ " [file ...]\n" +
18
+ "scope_lifecycle.py: error: the following arguments are required: action, file\n" +
19
+ "(promote --batch may omit file and promotes all proposed/ scopes; #3011)\n";
17
20
  function parseLifecycleArgv(argv) {
18
- if (argv.length < 2) {
21
+ if (argv.length < 1) {
19
22
  return { args: null, error: "usage" };
20
23
  }
21
24
  const action = argv[0] ?? "";
@@ -25,6 +28,15 @@ function parseLifecycleArgv(argv) {
25
28
  let file = "";
26
29
  let projectRoot;
27
30
  let force = false;
31
+ let batch = false;
32
+ const batchFiles = [];
33
+ let nonDeliveryDisposition;
34
+ let prNumber;
35
+ let mergeCommit;
36
+ let prBase;
37
+ let deliveryBranch;
38
+ let repo;
39
+ let mergedAt;
28
40
  for (let i = 1; i < argv.length; i += 1) {
29
41
  const arg = argv[i];
30
42
  if (arg === undefined) {
@@ -33,6 +45,9 @@ function parseLifecycleArgv(argv) {
33
45
  if (arg === "--force") {
34
46
  force = true;
35
47
  }
48
+ else if (arg === "--batch") {
49
+ batch = true;
50
+ }
36
51
  else if (arg === "--project-root") {
37
52
  projectRoot = argv[i + 1];
38
53
  i += 1;
@@ -40,17 +55,122 @@ function parseLifecycleArgv(argv) {
40
55
  else if (arg?.startsWith("--project-root=")) {
41
56
  projectRoot = arg.slice("--project-root=".length);
42
57
  }
43
- else if (!arg.startsWith("-") && file.length === 0) {
44
- file = arg;
58
+ else if (arg === "--non-delivery" || arg === "--non-delivery-disposition") {
59
+ const raw = argv[i + 1];
60
+ i += 1;
61
+ if (raw === undefined || !isNonDeliveryDisposition(raw)) {
62
+ return { args: null, error: "usage" };
63
+ }
64
+ nonDeliveryDisposition = raw;
65
+ }
66
+ else if (arg?.startsWith("--non-delivery=")) {
67
+ const raw = arg.slice("--non-delivery=".length);
68
+ if (!isNonDeliveryDisposition(raw)) {
69
+ return { args: null, error: "usage" };
70
+ }
71
+ nonDeliveryDisposition = raw;
72
+ }
73
+ else if (arg === "--pr" && argv[i + 1] !== undefined) {
74
+ prNumber = Number.parseInt(argv[i + 1] ?? "", 10);
75
+ i += 1;
76
+ }
77
+ else if (arg?.startsWith("--pr=")) {
78
+ prNumber = Number.parseInt(arg.slice("--pr=".length), 10);
79
+ }
80
+ else if (arg === "--merge-commit" && argv[i + 1] !== undefined) {
81
+ mergeCommit = argv[i + 1];
82
+ i += 1;
83
+ }
84
+ else if (arg?.startsWith("--merge-commit=")) {
85
+ mergeCommit = arg.slice("--merge-commit=".length);
86
+ }
87
+ else if (arg === "--pr-base" && argv[i + 1] !== undefined) {
88
+ prBase = argv[i + 1];
89
+ i += 1;
90
+ }
91
+ else if (arg?.startsWith("--pr-base=")) {
92
+ prBase = arg.slice("--pr-base=".length);
93
+ }
94
+ else if (arg === "--delivery-branch" && argv[i + 1] !== undefined) {
95
+ deliveryBranch = argv[i + 1];
96
+ i += 1;
97
+ }
98
+ else if (arg?.startsWith("--delivery-branch=")) {
99
+ deliveryBranch = arg.slice("--delivery-branch=".length);
100
+ }
101
+ else if (arg === "--repo" && argv[i + 1] !== undefined) {
102
+ repo = argv[i + 1];
103
+ i += 1;
104
+ }
105
+ else if (arg?.startsWith("--repo=")) {
106
+ repo = arg.slice("--repo=".length);
107
+ }
108
+ else if (arg === "--merged-at" && argv[i + 1] !== undefined) {
109
+ mergedAt = argv[i + 1];
110
+ i += 1;
111
+ }
112
+ else if (arg?.startsWith("--merged-at=")) {
113
+ mergedAt = arg.slice("--merged-at=".length);
114
+ }
115
+ else if (!arg.startsWith("-")) {
116
+ if (batch) {
117
+ batchFiles.push(arg);
118
+ }
119
+ else if (file.length === 0) {
120
+ file = arg;
121
+ }
122
+ else {
123
+ return { args: null, error: "usage" };
124
+ }
45
125
  }
46
126
  else {
47
127
  return { args: null, error: "usage" };
48
128
  }
49
129
  }
130
+ const deliveryEvidence = prNumber !== undefined ||
131
+ mergeCommit !== undefined ||
132
+ prBase !== undefined ||
133
+ deliveryBranch !== undefined ||
134
+ repo !== undefined ||
135
+ mergedAt !== undefined
136
+ ? {
137
+ prNumber: prNumber !== undefined && Number.isFinite(prNumber) ? prNumber : null,
138
+ mergeCommit: mergeCommit ?? null,
139
+ prBase: prBase ?? null,
140
+ deliveryBranch: deliveryBranch ?? null,
141
+ repository: repo ?? null,
142
+ mergedAt: mergedAt ?? (mergeCommit !== undefined ? "supplied" : null),
143
+ verifier: "scope:complete",
144
+ }
145
+ : undefined;
146
+ if (batch) {
147
+ if (action !== "promote") {
148
+ return { args: null, error: "usage" };
149
+ }
150
+ return {
151
+ args: {
152
+ action,
153
+ file: file.length > 0 ? file : "",
154
+ projectRoot,
155
+ force,
156
+ batch: true,
157
+ batchFiles: batchFiles.length > 0 ? batchFiles : file.length > 0 ? [file] : [],
158
+ },
159
+ };
160
+ }
50
161
  if (file.length === 0) {
51
162
  return { args: null, error: "usage" };
52
163
  }
53
- return { args: { action, file, projectRoot, force } };
164
+ return {
165
+ args: {
166
+ action,
167
+ file,
168
+ projectRoot,
169
+ force,
170
+ nonDeliveryDisposition,
171
+ deliveryEvidence,
172
+ },
173
+ };
54
174
  }
55
175
  /** Main entry for scope_lifecycle.py parity. */
56
176
  export function lifecycleMain(argv) {
@@ -61,7 +181,28 @@ export function lifecycleMain(argv) {
61
181
  }
62
182
  return 2;
63
183
  }
64
- const { action, file, projectRoot, force } = parsed.args;
184
+ const { action, file, projectRoot, force, batch, batchFiles, nonDeliveryDisposition, deliveryEvidence, } = parsed.args;
185
+ if (batch === true && action === "promote") {
186
+ const result = batchPromote({
187
+ files: batchFiles && batchFiles.length > 0 ? batchFiles : undefined,
188
+ force: force === true,
189
+ projectRoot,
190
+ });
191
+ for (const line of result.messages) {
192
+ process.stdout.write(`${line}\n`);
193
+ }
194
+ for (const line of result.skipped) {
195
+ process.stdout.write(` skipped: ${line}\n`);
196
+ }
197
+ if (!result.ok && result.exitCode !== 0) {
198
+ for (const line of result.messages) {
199
+ if (line.startsWith("ERROR:") || line.includes("WIP cap")) {
200
+ process.stderr.write(`${line}\n`);
201
+ }
202
+ }
203
+ }
204
+ return result.exitCode;
205
+ }
65
206
  const [filePath, error] = resolveFilePath(file, projectRoot);
66
207
  if (error !== null || filePath === null) {
67
208
  process.stderr.write(`Error: ${error}\n`);
@@ -78,7 +219,12 @@ export function lifecycleMain(argv) {
78
219
  }
79
220
  }
80
221
  }
81
- const result = runTransition(action, filePath);
222
+ const transitionOptions = {
223
+ nonDeliveryDisposition,
224
+ deliveryEvidence,
225
+ verifier: "scope:complete",
226
+ };
227
+ const result = runTransition(action, filePath, new Date(), transitionOptions);
82
228
  if (result.ok) {
83
229
  if (action === "promote" && capCheck !== null && capCheck.forceOverride) {
84
230
  const rootForAudit = resolveProjectRoot(projectRoot);
@@ -164,7 +164,10 @@ export function syncRegistryArtifactAfterScopeMove(registryPath, scopeData, oldP
164
164
  hooks.persist(registryPath, registry);
165
165
  }
166
166
  else {
167
- atomicWriteText(registryPath, formatBriefJson(registry));
167
+ // #3042: contain registry stay-path write against project root (parent of xbrief/).
168
+ atomicWriteText(registryPath, formatBriefJson(registry), {
169
+ projectRoot: dirname(resolve(vbriefRoot)),
170
+ });
168
171
  }
169
172
  }
170
173
  }
@@ -1,10 +1,20 @@
1
+ import type { GitRunner } from "../session/git.js";
1
2
  import { detectLifecycleFolder } from "./decomposed-refs.js";
3
+ import { type DeliveryEvidenceInput, type NonDeliveryDisposition } from "./delivery-evidence.js";
2
4
  import type { WipCapCheck } from "./wip-cap-check.js";
3
5
  export interface TransitionResult {
4
6
  readonly ok: boolean;
5
7
  readonly message: string;
6
8
  }
7
- export declare function runTransition(action: string, filePath: string, now?: Date): TransitionResult;
9
+ /** Optional completion evidence / disposition for the delivery gate (#3041). */
10
+ export interface TransitionOptions {
11
+ readonly deliveryEvidence?: DeliveryEvidenceInput | null;
12
+ readonly nonDeliveryDisposition?: NonDeliveryDisposition | null;
13
+ readonly runGit?: GitRunner;
14
+ readonly verifier?: string;
15
+ readonly assumeEvidenceValidated?: boolean;
16
+ }
17
+ export declare function runTransition(action: string, filePath: string, now?: Date, options?: TransitionOptions): TransitionResult;
8
18
  export declare function recordWipCapOverride(filePath: string, projectRoot: string, check: WipCapCheck, now?: Date): void;
9
19
  export { detectLifecycleFolder };
10
20
  //# sourceMappingURL=transition.d.ts.map
@@ -8,6 +8,7 @@ import { atomicWriteBrief, formatBriefJson, readBriefForMutation } from "./brief
8
8
  import { stampCompletionMetadata } from "./capacity-stamp.js";
9
9
  import { LIFECYCLE_FOLDERS, MOVE_LABELS, STATUS_PRECONDITIONS, STAY_LABELS, TRANSITIONS, } from "./constants.js";
10
10
  import { detectLifecycleFolder, updateDecomposedChildBackReferences, updateDecomposedParentBackReferences, } from "./decomposed-refs.js";
11
+ import { classifyStoredDeliveryDisposition, evaluateDeliveryGate, stampDeliveryProvenance, } from "./delivery-evidence.js";
11
12
  import { syncProjectDefinitionAfterScopeMove } from "./project-definition-sync.js";
12
13
  import { syncSpecificationAfterScopeMove } from "./specification-sync.js";
13
14
  import { utcNowIso } from "./vbrief-json.js";
@@ -48,7 +49,7 @@ function stampEnvelopeUpdated(data, nowIso) {
48
49
  }
49
50
  }
50
51
  }
51
- export function runTransition(action, filePath, now = new Date()) {
52
+ export function runTransition(action, filePath, now = new Date(), options = {}) {
52
53
  if (!(action in TRANSITIONS)) {
53
54
  const valid = Object.keys(TRANSITIONS).sort().join(", ");
54
55
  return { ok: false, message: `Unknown action '${action}'. Valid actions: ${valid}` };
@@ -95,9 +96,15 @@ export function runTransition(action, filePath, now = new Date()) {
95
96
  const requiredStatus = STATUS_PRECONDITIONS[act];
96
97
  if (requiredStatus !== undefined) {
97
98
  if (currentStatus === targetStatus) {
99
+ // Surface legacy delivery disposition on already-completed briefs (#3041).
100
+ let dispositionSuffix = "";
101
+ if (act === "complete" && currentFolder === "completed") {
102
+ const disposition = classifyStoredDeliveryDisposition(planObj);
103
+ dispositionSuffix = ` (deliveryDisposition=${disposition})`;
104
+ }
98
105
  return {
99
106
  ok: true,
100
- message: `No-op: ${basename} is already ${targetStatus} in ${currentFolder}/`,
107
+ message: `No-op: ${basename} is already ${targetStatus} in ${currentFolder}/${dispositionSuffix}`,
101
108
  };
102
109
  }
103
110
  if (currentStatus !== requiredStatus) {
@@ -131,6 +138,25 @@ export function runTransition(action, filePath, now = new Date()) {
131
138
  }
132
139
  }
133
140
  const nowIso = utcNowIso(now);
141
+ // #3041: fail closed before mutating a code-bearing complete without delivery evidence.
142
+ if (act === "complete") {
143
+ const gate = evaluateDeliveryGate({
144
+ projectRoot,
145
+ plan: planObj,
146
+ nowIso,
147
+ evidence: options.deliveryEvidence,
148
+ nonDeliveryDisposition: options.nonDeliveryDisposition,
149
+ runGit: options.runGit,
150
+ verifier: options.verifier ?? "scope:complete",
151
+ assumeEvidenceValidated: options.assumeEvidenceValidated,
152
+ });
153
+ if (!gate.ok) {
154
+ return { ok: false, message: gate.message };
155
+ }
156
+ if (gate.provenance !== null) {
157
+ stampDeliveryProvenance(planObj, gate.provenance);
158
+ }
159
+ }
134
160
  planObj.status = targetStatus;
135
161
  planObj.updated = nowIso;
136
162
  // Keep the envelope clock aligned with plan.updated on every mutating transition (#2862).
@@ -153,7 +179,7 @@ export function runTransition(action, filePath, now = new Date()) {
153
179
  }
154
180
  // #2578: stamp terminal status at the destination path in the same write as
155
181
  // folder placement — never leave a non-terminal status under completed/.
156
- const writeResult = atomicWriteBrief(destPath, data, vbriefRoot);
182
+ const writeResult = atomicWriteBrief(destPath, data, vbriefRoot, { projectRoot });
157
183
  if (!writeResult.ok) {
158
184
  return { ok: false, message: writeResult.message };
159
185
  }
@@ -193,7 +219,7 @@ export function runTransition(action, filePath, now = new Date()) {
193
219
  message: `${actionLabel} ${basename}: ${currentFolder}/ -> ${targetFolder}/ (status: ${targetStatus})`,
194
220
  };
195
221
  }
196
- const writeResult = atomicWriteBrief(resolvedPath, data, vbriefRoot);
222
+ const writeResult = atomicWriteBrief(resolvedPath, data, vbriefRoot, { projectRoot });
197
223
  if (!writeResult.ok) {
198
224
  return { ok: false, message: writeResult.message };
199
225
  }
@@ -7,5 +7,11 @@ export interface WipCapCheck {
7
7
  }
8
8
  /** Resolve WIP cap and decide if promotion is allowed (#1124). */
9
9
  export declare function checkWipCap(projectRoot: string, force?: boolean): WipCapCheck;
10
+ /**
11
+ * WIP check for promoting `additional` scopes in one batch (#3011).
12
+ * Single promote is `additional = 1` (same as historic count >= cap refuse).
13
+ * Batch of N refuses when `count + N > cap` unless `--force`.
14
+ */
15
+ export declare function checkWipCapForAdditional(projectRoot: string, additional: number, force?: boolean): WipCapCheck;
10
16
  export declare function formatWipCapRefusal(check: WipCapCheck): string;
11
17
  //# sourceMappingURL=wip-cap-check.d.ts.map
@@ -1,10 +1,21 @@
1
1
  import { countVbriefWip, resolveWipCap } from "../policy/wip.js";
2
2
  /** Resolve WIP cap and decide if promotion is allowed (#1124). */
3
3
  export function checkWipCap(projectRoot, force = false) {
4
+ return checkWipCapForAdditional(projectRoot, 1, force);
5
+ }
6
+ /**
7
+ * WIP check for promoting `additional` scopes in one batch (#3011).
8
+ * Single promote is `additional = 1` (same as historic count >= cap refuse).
9
+ * Batch of N refuses when `count + N > cap` unless `--force`.
10
+ */
11
+ export function checkWipCapForAdditional(projectRoot, additional, force = false) {
4
12
  const capResult = resolveWipCap(projectRoot);
5
13
  const cap = capResult.cap;
6
14
  const count = countVbriefWip(projectRoot);
7
- const overCap = count >= cap;
15
+ const add = Math.max(0, Math.floor(additional));
16
+ // Single-promote parity: refuse when already at/over cap before adding one.
17
+ // Batch: refuse when current + additional would exceed the cap.
18
+ const overCap = add <= 1 ? count >= cap : count + add > cap;
8
19
  if (!overCap) {
9
20
  return { allowed: true, cap, count, source: capResult.source, forceOverride: false };
10
21
  }
@@ -18,6 +29,7 @@ export function formatWipCapRefusal(check) {
18
29
  "Either:\n" +
19
30
  " task scope:demote <existing> # return one to proposed/\n" +
20
31
  " task scope:demote --batch --older-than-days 30 # bulk relief (D9 folded into D1)\n" +
21
- " task scope:promote <file> --force # override (logged)");
32
+ " task scope:promote <file> --force # override (logged)\n" +
33
+ " task scope:promote --batch --force # batch override (#3011)");
22
34
  }
23
35
  //# sourceMappingURL=wip-cap-check.js.map
@@ -1,6 +1,7 @@
1
- import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, statSync, } from "node:fs";
2
- import { join, relative, resolve } from "node:path";
1
+ import { existsSync, readdirSync, readFileSync, renameSync, rmSync, statSync } from "node:fs";
2
+ import { basename, dirname, join, relative, resolve } from "node:path";
3
3
  import { containedWrite } from "../fs/contained-write.js";
4
+ import { assertWriteTargetSafe } from "../fs/projection-containment.js";
4
5
  import { hasArtifactSuffix, LEGACY_ARTIFACT_DIR, MIGRATED_ARTIFACT_DIR, } from "../layout/resolve.js";
5
6
  import { stableJson } from "./json.js";
6
7
  import { RITUAL_STATE_CONTRACT } from "./posture.js";
@@ -95,21 +96,29 @@ function validateSteps(raw, key) {
95
96
  }
96
97
  return [steps, null];
97
98
  }
98
- function atomicWriteJson(targetPath, payload, prefix) {
99
- const dir = join(targetPath, "..");
100
- mkdirSync(dir, { recursive: true });
101
- const tmpBase = `${prefix}${process.pid}.json.tmp`;
99
+ /**
100
+ * Contained atomic JSON write for ritual/sentinel state (#3042).
101
+ * Containment root is projectRoot (not dirname(target)) so a force-added `.deft`
102
+ * directory symlink fails closed before temp+rename.
103
+ */
104
+ function atomicWriteJson(projectRoot, targetPath, payload, prefix) {
105
+ const root = resolve(projectRoot);
106
+ const abs = resolve(targetPath);
107
+ // Refuse leaf/parent symlinks on the final path before temp+rename publish.
108
+ assertWriteTargetSafe(root, abs);
109
+ const dir = dirname(abs);
110
+ const tmpBase = `${prefix}${process.pid}.${basename(abs)}.tmp`;
102
111
  const tmpName = join(dir, tmpBase);
103
112
  const text = `${stableJson(payload, 2)}\n`;
104
113
  try {
105
- // #2980 wave D: ritual-state product write routes through containedWrite.
114
+ // #2980 wave D / #3042: product write routes through containedWrite under projectRoot.
106
115
  containedWrite({
107
- root: resolve(dir),
108
- target: tmpBase,
116
+ root,
117
+ target: tmpName,
109
118
  data: text,
110
119
  mode: "create",
111
120
  });
112
- renameSync(tmpName, targetPath);
121
+ renameSync(tmpName, abs);
113
122
  }
114
123
  catch (err) {
115
124
  try {
@@ -191,7 +200,7 @@ export function readRitualState(projectRoot) {
191
200
  }
192
201
  export function writeRitualState(projectRoot, payload) {
193
202
  const stateFile = ritualStatePath(projectRoot);
194
- atomicWriteJson(stateFile, payload, ".ritual-state.");
203
+ atomicWriteJson(projectRoot, stateFile, payload, ".ritual-state.");
195
204
  return stateFile;
196
205
  }
197
206
  /** Instant guaranteed to fail `evaluateLoadedState` age checks on any policy horizon. */
@@ -303,7 +312,7 @@ export function writeSentinel(projectRoot, input) {
303
312
  .replace(`${LEGACY_ARTIFACT_DIR}/active/`, `${MIGRATED_ARTIFACT_DIR}/active/`),
304
313
  lastBranch: input.lastBranch,
305
314
  };
306
- atomicWriteJson(sentinelFile, payload, ".last-session.");
315
+ atomicWriteJson(projectRoot, sentinelFile, payload, ".last-session.");
307
316
  return sentinelFile;
308
317
  }
309
318
  function formatElapsed(deltaMs) {
@@ -1,3 +1,4 @@
1
+ import { detectDeftDirectiveDisable } from "../policy/deft-directive-disable.js";
1
2
  import { detectNoDeftDirective } from "../policy/no-deft-directive.js";
2
3
  import { writeSentinel } from "./ritual-sentinel.js";
3
4
  export interface SessionStartHookOptions {
@@ -7,6 +8,8 @@ export interface SessionStartHookOptions {
7
8
  readonly writeSentinelFn?: typeof writeSentinel;
8
9
  /** Test seam for #2926 opt-out detection. */
9
10
  readonly detectNoDeftDirectiveFn?: typeof detectNoDeftDirective;
11
+ /** Test seam for #3039 test kill-switch detection. */
12
+ readonly detectDeftDirectiveDisableFn?: typeof detectDeftDirectiveDisable;
10
13
  }
11
14
  /** Write ``.deft/last-session.json`` from current git state (#1269). */
12
15
  export declare function runSessionStartHookWrite(projectRoot: string, options?: SessionStartHookOptions): {
@@ -1,9 +1,30 @@
1
1
  import { resolveVersion } from "../doctor/paths.js";
2
+ import { detectDeftDirectiveDisable, formatDeftDirectiveDisableMessage, isDeftDirectiveDisableActive, } from "../policy/deft-directive-disable.js";
2
3
  import { detectNoDeftDirective, NO_DEFT_DIRECTIVE_DISABLED_MESSAGE, NO_DEFT_DIRECTIVE_INCONSISTENT_MESSAGE, } from "../policy/no-deft-directive.js";
3
4
  import { detectBranch } from "./git.js";
4
5
  import { detectLatestActiveVbrief, writeSentinel } from "./ritual-sentinel.js";
5
6
  /** Write ``.deft/last-session.json`` from current git state (#1269). */
6
7
  export function runSessionStartHookWrite(projectRoot, options = {}) {
8
+ const detectKill = options.detectDeftDirectiveDisableFn ?? detectDeftDirectiveDisable;
9
+ // #3039: local (untracked) kill-switch — skip ritual bookkeeping (deposit OK).
10
+ // Tracked flags do not short-circuit (enforcement stays on).
11
+ const kill = detectKill(projectRoot);
12
+ const killActive = options.detectDeftDirectiveDisableFn !== undefined
13
+ ? kill.active
14
+ : isDeftDirectiveDisableActive(projectRoot);
15
+ if (killActive) {
16
+ const detectOptOut = options.detectNoDeftDirectiveFn ?? detectNoDeftDirective;
17
+ const optOut = detectOptOut(projectRoot);
18
+ const message = formatDeftDirectiveDisableMessage({
19
+ permanentOptOutAlsoPresent: optOut.present,
20
+ trackedByGit: false,
21
+ });
22
+ return {
23
+ code: 0,
24
+ stdout: `${message}\n`,
25
+ stderr: "",
26
+ };
27
+ }
7
28
  const detectOptOut = options.detectNoDeftDirectiveFn ?? detectNoDeftDirective;
8
29
  // #2926: root opt-out wins — host SessionStart must not write ritual bookkeeping.
9
30
  const optOut = detectOptOut(projectRoot);
@@ -1,4 +1,5 @@
1
1
  import { type EnvironmentContext } from "../platform/shell-context.js";
2
+ import { type ProbeScmReadinessOptions, type ScmReadinessReport } from "../scm/readiness.js";
2
3
  import { type ResolveUserMdResult } from "../user-config/resolve-user-md.js";
3
4
  import type { GitRunner } from "./git.js";
4
5
  import { type ReleaseAvailabilityProbeOptions } from "./release-availability.js";
@@ -86,6 +87,12 @@ export interface SessionStartOptions {
86
87
  readonly allowOptionalNetwork?: boolean;
87
88
  /** Process env for network opt-in resolution (tests inject). */
88
89
  readonly env?: NodeJS.ProcessEnv;
90
+ /**
91
+ * #2275: SCM tooling + auth readiness probe for mismatched/headless envs.
92
+ * Defaults to probeScmReadiness; shallow on hot path, deep when optional
93
+ * network is enabled. Inject in tests.
94
+ */
95
+ readonly probeScm?: (options: ProbeScmReadinessOptions) => ScmReadinessReport;
89
96
  }
90
97
  /** Format preferred `session:start` recovery command for cold vs re-arm (#2992). */
91
98
  export declare function formatSessionStartRecoveryCommand(tier?: SessionCeremonyTier): string;