@deftai/directive-core 0.88.0 → 0.90.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 (202) hide show
  1. package/dist/authz/actions.d.ts +55 -0
  2. package/dist/authz/actions.js +125 -0
  3. package/dist/authz/classify.d.ts +23 -0
  4. package/dist/authz/classify.js +217 -0
  5. package/dist/authz/closed-verb.d.ts +42 -0
  6. package/dist/authz/closed-verb.js +279 -0
  7. package/dist/authz/evaluate.d.ts +41 -0
  8. package/dist/authz/evaluate.js +298 -0
  9. package/dist/authz/index.d.ts +15 -0
  10. package/dist/authz/index.js +15 -0
  11. package/dist/authz/origin.d.ts +28 -0
  12. package/dist/authz/origin.js +64 -0
  13. package/dist/authz/paths.d.ts +6 -0
  14. package/dist/authz/paths.js +19 -0
  15. package/dist/authz/store.d.ts +40 -0
  16. package/dist/authz/store.js +317 -0
  17. package/dist/authz/templates.d.ts +139 -0
  18. package/dist/authz/templates.js +241 -0
  19. package/dist/authz/types.d.ts +111 -0
  20. package/dist/authz/types.js +41 -0
  21. package/dist/authz/verb-classification.d.ts +44 -0
  22. package/dist/authz/verb-classification.js +237 -0
  23. package/dist/branch/evaluate.js +6 -1
  24. package/dist/cache/fetch.js +10 -5
  25. package/dist/cache/io.d.ts +9 -2
  26. package/dist/cache/io.js +30 -10
  27. package/dist/cache/task-cache/store.js +11 -7
  28. package/dist/capacity/backfill.js +10 -4
  29. package/dist/category-b-namespace/index.js +10 -4
  30. package/dist/codebase/default-extractor.js +3 -0
  31. package/dist/codebase/map.js +11 -4
  32. package/dist/doctor/constants.d.ts +1 -1
  33. package/dist/doctor/constants.js +2 -0
  34. package/dist/doctor/doctor-state.js +28 -4
  35. package/dist/doctor/flags.js +21 -1
  36. package/dist/doctor/index.d.ts +1 -0
  37. package/dist/doctor/index.js +1 -0
  38. package/dist/doctor/main.d.ts +10 -0
  39. package/dist/doctor/main.js +182 -0
  40. package/dist/doctor/openclaw-skills.d.ts +109 -0
  41. package/dist/doctor/openclaw-skills.js +463 -0
  42. package/dist/doctor/types.d.ts +22 -0
  43. package/dist/escalation/actions.d.ts +63 -0
  44. package/dist/escalation/actions.js +137 -0
  45. package/dist/escalation/index.d.ts +8 -0
  46. package/dist/escalation/index.js +8 -0
  47. package/dist/escalation/paths.d.ts +3 -0
  48. package/dist/escalation/paths.js +10 -0
  49. package/dist/escalation/store.d.ts +17 -0
  50. package/dist/escalation/store.js +172 -0
  51. package/dist/escalation/types.d.ts +73 -0
  52. package/dist/escalation/types.js +43 -0
  53. package/dist/eval/crud-telemetry.js +30 -10
  54. package/dist/eval/health.js +22 -7
  55. package/dist/eval/readback.js +11 -10
  56. package/dist/eval/run.js +32 -16
  57. package/dist/events/attribution-enrichment.js +10 -4
  58. package/dist/finish-loop/directive-finish-loop.d.ts +37 -0
  59. package/dist/finish-loop/directive-finish-loop.js +239 -0
  60. package/dist/finish-loop/grant-gate.d.ts +31 -0
  61. package/dist/finish-loop/grant-gate.js +169 -0
  62. package/dist/finish-loop/index.d.ts +11 -0
  63. package/dist/finish-loop/index.js +11 -0
  64. package/dist/finish-loop/main.d.ts +35 -0
  65. package/dist/finish-loop/main.js +365 -0
  66. package/dist/finish-loop/pr-finish-loop.d.ts +34 -0
  67. package/dist/finish-loop/pr-finish-loop.js +217 -0
  68. package/dist/finish-loop/progress.d.ts +17 -0
  69. package/dist/finish-loop/progress.js +45 -0
  70. package/dist/finish-loop/queue.d.ts +16 -0
  71. package/dist/finish-loop/queue.js +65 -0
  72. package/dist/finish-loop/types.d.ts +52 -0
  73. package/dist/finish-loop/types.js +10 -0
  74. package/dist/fs/contained-write.d.ts +101 -0
  75. package/dist/fs/contained-write.js +281 -0
  76. package/dist/hooks/classify/classify.d.ts +10 -0
  77. package/dist/hooks/classify/classify.js +37 -0
  78. package/dist/hooks/classify/index.d.ts +14 -0
  79. package/dist/hooks/classify/index.js +13 -0
  80. package/dist/hooks/classify/paths.d.ts +22 -0
  81. package/dist/hooks/classify/paths.js +72 -0
  82. package/dist/hooks/classify/payload.d.ts +16 -0
  83. package/dist/hooks/classify/payload.js +45 -0
  84. package/dist/hooks/classify/stdin.d.ts +12 -0
  85. package/dist/hooks/classify/stdin.js +63 -0
  86. package/dist/hooks/classify/tool-name.d.ts +18 -0
  87. package/dist/hooks/classify/tool-name.js +85 -0
  88. package/dist/hooks/classify/types.d.ts +41 -0
  89. package/dist/hooks/classify/types.js +7 -0
  90. package/dist/hooks/dispatcher.d.ts +21 -18
  91. package/dist/hooks/dispatcher.js +232 -168
  92. package/dist/hooks/fixtures/cases.d.ts +44 -0
  93. package/dist/hooks/fixtures/cases.js +595 -0
  94. package/dist/hooks/fixtures/index.d.ts +2 -0
  95. package/dist/hooks/fixtures/index.js +2 -0
  96. package/dist/hooks/index.d.ts +2 -0
  97. package/dist/hooks/index.js +2 -0
  98. package/dist/index.d.ts +5 -0
  99. package/dist/index.js +6 -0
  100. package/dist/init-deposit/agent-hooks.js +27 -8
  101. package/dist/init-deposit/gitignore.js +11 -3
  102. package/dist/init-deposit/headless-manifest.js +12 -4
  103. package/dist/init-deposit/migrate.d.ts +1 -1
  104. package/dist/init-deposit/migrate.js +13 -3
  105. package/dist/init-deposit/prettierignore.js +10 -3
  106. package/dist/init-deposit/scaffold.js +35 -26
  107. package/dist/init-deposit/xbrief-projections.js +11 -4
  108. package/dist/intake/candidates-log.js +46 -35
  109. package/dist/intake/github-body-cli.d.ts +6 -0
  110. package/dist/intake/github-body-cli.js +17 -0
  111. package/dist/intake/github-body.d.ts +46 -3
  112. package/dist/intake/github-body.js +191 -20
  113. package/dist/intake/issue-emit.d.ts +1 -0
  114. package/dist/intake/issue-emit.js +41 -23
  115. package/dist/lifecycle/event-detect.js +12 -4
  116. package/dist/lifecycle/events.js +31 -20
  117. package/dist/lifecycle/index.d.ts +1 -0
  118. package/dist/lifecycle/index.js +1 -0
  119. package/dist/lifecycle/lifecycle-hygiene.js +9 -3
  120. package/dist/lifecycle/stats.d.ts +53 -0
  121. package/dist/lifecycle/stats.js +286 -0
  122. package/dist/packs/pack-render.js +19 -6
  123. package/dist/plan-sequence/store.js +11 -4
  124. package/dist/policy/hotfix-criteria.d.ts +79 -0
  125. package/dist/policy/hotfix-criteria.js +197 -0
  126. package/dist/policy/index.d.ts +4 -0
  127. package/dist/policy/index.js +31 -1
  128. package/dist/policy/intent-ceiling.d.ts +79 -0
  129. package/dist/policy/intent-ceiling.js +181 -0
  130. package/dist/policy/no-deft-directive.js +10 -3
  131. package/dist/policy/org-force-on-migration.js +9 -5
  132. package/dist/policy/require-human-merge.d.ts +75 -0
  133. package/dist/policy/require-human-merge.js +324 -0
  134. package/dist/policy/resolve.js +17 -6
  135. package/dist/policy/runtime-authority.d.ts +15 -2
  136. package/dist/policy/runtime-authority.js +23 -3
  137. package/dist/policy/write-fence.d.ts +78 -0
  138. package/dist/policy/write-fence.js +164 -0
  139. package/dist/pr-wait-mergeable/cascade.d.ts +10 -0
  140. package/dist/pr-wait-mergeable/cascade.js +28 -0
  141. package/dist/preflight/evaluate.js +10 -0
  142. package/dist/product-signal/consent.js +21 -5
  143. package/dist/product-signal/submit.js +22 -12
  144. package/dist/release/build-dist-runner.js +5 -2
  145. package/dist/release/build-dist.d.ts +19 -1
  146. package/dist/release/build-dist.js +50 -2
  147. package/dist/release/native-steps.js +7 -2
  148. package/dist/release/spawn.js +13 -0
  149. package/dist/release-e2e/git-ops.d.ts +7 -0
  150. package/dist/release-e2e/git-ops.js +35 -2
  151. package/dist/release-publish/pipeline.d.ts +13 -0
  152. package/dist/release-publish/pipeline.js +46 -1
  153. package/dist/scope/audit-log.js +19 -24
  154. package/dist/scope/decompose.js +10 -5
  155. package/dist/scope/decomposed-refs.js +18 -3
  156. package/dist/scope/demote.js +9 -2
  157. package/dist/scope/undo.js +9 -2
  158. package/dist/session/index.d.ts +2 -0
  159. package/dist/session/index.js +2 -0
  160. package/dist/session/process-cost-constants.d.ts +9 -0
  161. package/dist/session/process-cost-constants.js +11 -0
  162. package/dist/session/process-cost.d.ts +53 -0
  163. package/dist/session/process-cost.js +82 -0
  164. package/dist/session/release-availability.js +26 -6
  165. package/dist/session/ritual-sentinel.d.ts +5 -0
  166. package/dist/session/ritual-sentinel.js +31 -13
  167. package/dist/session/session-ready.d.ts +67 -0
  168. package/dist/session/session-ready.js +264 -0
  169. package/dist/session/session-start.d.ts +56 -1
  170. package/dist/session/session-start.js +384 -23
  171. package/dist/session/verify-session-ritual.d.ts +8 -0
  172. package/dist/session/verify-session-ritual.js +95 -35
  173. package/dist/staleness-tickler/state.js +26 -6
  174. package/dist/swarm/launch.js +13 -3
  175. package/dist/swarm/routing.js +9 -3
  176. package/dist/task-surface/index.js +24 -9
  177. package/dist/tool-events/classify.d.ts +20 -0
  178. package/dist/tool-events/classify.js +634 -0
  179. package/dist/tool-events/index.d.ts +10 -0
  180. package/dist/tool-events/index.js +10 -0
  181. package/dist/tool-events/summarize.d.ts +34 -0
  182. package/dist/tool-events/summarize.js +93 -0
  183. package/dist/tool-events/types.d.ts +52 -0
  184. package/dist/tool-events/types.js +13 -0
  185. package/dist/triage/bootstrap/gitignore.js +53 -10
  186. package/dist/triage/cache-path.js +10 -3
  187. package/dist/triage/welcome/summary.js +11 -4
  188. package/dist/triage/welcome/writers.js +45 -16
  189. package/dist/validate-content/validate-links.js +5 -0
  190. package/dist/value/readback.js +11 -6
  191. package/dist/vbrief-activate/activate.js +12 -4
  192. package/dist/vbrief-build/project-definition-io.js +14 -6
  193. package/dist/verify-source/contained-writes.d.ts +59 -0
  194. package/dist/verify-source/contained-writes.js +272 -0
  195. package/dist/verify-source/index.d.ts +1 -0
  196. package/dist/verify-source/index.js +1 -0
  197. package/dist/verify-source/openclaw-tier1.js +14 -1
  198. package/dist/verify-source/verify-stubs.js +3 -0
  199. package/dist/xbrief-migrate/migrate-project.js +26 -12
  200. package/dist/xbrief-migrate/transforms.d.ts +4 -2
  201. package/dist/xbrief-migrate/transforms.js +37 -14
  202. package/package.json +19 -3
@@ -0,0 +1,164 @@
1
+ /**
2
+ * Unified path write fence (#516 / #2443 / #2948 Wave 3).
3
+ *
4
+ * Single evaluation SoT: path checks always go through
5
+ * `evaluateRuntimeAuthorityPath` / `evaluateRuntimeAuthorityDirectWrite`.
6
+ * This module only **resolves** a RuntimeAuthorityPolicy by intersecting
7
+ * project `runtimeAuthority.allowPaths` / `denyPaths` with optional story
8
+ * `plan.metadata.swarm.file_scope` (and a read-time `writeScope` alias).
9
+ *
10
+ * There is no second independent writeScope evaluation engine.
11
+ */
12
+ import { readFileSync } from "node:fs";
13
+ import { DEFAULT_RUNTIME_AUTHORITY_POLICY, DEFAULT_RUNTIME_AUTHORITY_SCOPES, } from "./runtime-authority.js";
14
+ /** Clean non-empty path glob strings. */
15
+ function cleanGlobs(raw) {
16
+ if (raw === null || raw === undefined)
17
+ return [];
18
+ return raw.filter((s) => typeof s === "string" && s.trim().length > 0);
19
+ }
20
+ function cleanGlobArray(raw) {
21
+ if (!Array.isArray(raw))
22
+ return [];
23
+ return raw.filter((s) => typeof s === "string" && s.trim().length > 0);
24
+ }
25
+ /**
26
+ * Normalize a legacy #2443 `writeScope` alias into `file_scope` + deny globs.
27
+ * Accepts:
28
+ * - `string[]` → allow list
29
+ * - `{ allow?: string[], deny?: string[] }`
30
+ * - `{ file_scope?: string[], denyPaths?: string[] }` (already-normalized shape)
31
+ *
32
+ * Callers MUST feed the result into {@link resolveWriteFence}; do not evaluate
33
+ * writeScope with a separate matcher.
34
+ */
35
+ export function normalizeStoryWriteScope(raw) {
36
+ if (raw === null || raw === undefined) {
37
+ return { fileScope: [], denyPaths: [] };
38
+ }
39
+ if (Array.isArray(raw)) {
40
+ return { fileScope: cleanGlobArray(raw), denyPaths: [] };
41
+ }
42
+ if (typeof raw !== "object") {
43
+ return { fileScope: [], denyPaths: [] };
44
+ }
45
+ const rec = raw;
46
+ const allow = cleanGlobArray(rec.allow ?? rec.file_scope ?? rec.fileScope);
47
+ const deny = cleanGlobArray(rec.deny ?? rec.denyPaths);
48
+ return { fileScope: allow, denyPaths: deny };
49
+ }
50
+ /**
51
+ * Extract story write fence from an xBRIEF/vBRIEF document.
52
+ * Prefer `plan.metadata.swarm.file_scope`; fall back to normalized `writeScope`
53
+ * on `plan.metadata.swarm` or `plan.metadata` (read-time alias only).
54
+ */
55
+ export function extractStoryFileScope(storyData) {
56
+ if (typeof storyData !== "object" || storyData === null || Array.isArray(storyData)) {
57
+ return { fileScope: [], denyPaths: [] };
58
+ }
59
+ const plan = storyData.plan;
60
+ if (typeof plan !== "object" || plan === null || Array.isArray(plan)) {
61
+ return { fileScope: [], denyPaths: [] };
62
+ }
63
+ const metadata = plan.metadata;
64
+ if (typeof metadata !== "object" || metadata === null || Array.isArray(metadata)) {
65
+ return { fileScope: [], denyPaths: [] };
66
+ }
67
+ const meta = metadata;
68
+ const swarmRaw = meta.swarm;
69
+ const swarm = typeof swarmRaw === "object" && swarmRaw !== null && !Array.isArray(swarmRaw)
70
+ ? swarmRaw
71
+ : null;
72
+ const fromFileScope = cleanGlobArray(swarm?.file_scope ?? swarm?.fileScope);
73
+ const fromWriteScope = normalizeStoryWriteScope(swarm?.writeScope ?? swarm?.write_scope ?? meta.writeScope ?? meta.write_scope);
74
+ // file_scope is SoT when present; writeScope alias only when file_scope empty.
75
+ if (fromFileScope.length > 0) {
76
+ // Still merge writeScope.deny so legacy deny lists are not lost.
77
+ return {
78
+ fileScope: fromFileScope,
79
+ denyPaths: fromWriteScope.denyPaths,
80
+ };
81
+ }
82
+ return fromWriteScope;
83
+ }
84
+ /** Load story fence from an active xBRIEF path (fail-open on IO/parse errors). */
85
+ export function loadStoryWriteFenceFromPath(scopePath) {
86
+ if (scopePath === null || scopePath === undefined || scopePath.trim().length === 0) {
87
+ return { fileScope: [], denyPaths: [] };
88
+ }
89
+ try {
90
+ const text = readFileSync(scopePath, "utf8");
91
+ const data = JSON.parse(text);
92
+ return extractStoryFileScope(data);
93
+ }
94
+ catch {
95
+ // Residual: host/session may not surface a readable active story.
96
+ // Fail open at story layer (project policy still applies if enabled).
97
+ return { fileScope: [], denyPaths: [] };
98
+ }
99
+ }
100
+ /**
101
+ * Intersect project `runtimeAuthority` with optional story `file_scope`.
102
+ *
103
+ * Rules:
104
+ * - Empty project `allowPaths` when enabled = all paths until story narrows.
105
+ * - Empty story scope = project policy only.
106
+ * - Story `file_scope` alone enables a fence even when project policy is disabled.
107
+ * - `denyPaths` always win (project + story-normalized denys).
108
+ * - When both project allowPaths and story file_scope are non-empty, a path must
109
+ * match **both** layers (AND / intersection semantics via dual allow layers).
110
+ */
111
+ export function resolveWriteFence(projectPolicy, storyFileScope, options) {
112
+ const storyAllow = cleanGlobs(storyFileScope);
113
+ const storyDeny = cleanGlobs(options?.storyDenyPaths);
114
+ const projectActive = projectPolicy.enabled;
115
+ const storyActive = storyAllow.length > 0 || storyDeny.length > 0;
116
+ if (!projectActive && !storyActive) {
117
+ return {
118
+ policy: DEFAULT_RUNTIME_AUTHORITY_POLICY,
119
+ sources: [],
120
+ fenceActive: false,
121
+ storyAllowPaths: [],
122
+ storyDenyPaths: [],
123
+ };
124
+ }
125
+ const sources = [];
126
+ if (projectActive)
127
+ sources.push("project");
128
+ if (storyActive)
129
+ sources.push("story");
130
+ // Project denyPaths + story denyPaths; deny always wins at evaluation time.
131
+ const denyPaths = [...projectPolicy.denyPaths, ...storyDeny];
132
+ // Project allow layer only when project policy is enabled (empty = unrestricted until story).
133
+ const allowPaths = projectActive ? [...projectPolicy.allowPaths] : [];
134
+ // Story-only: enable fence with default scopes (edits true) so direct writes are gated.
135
+ const scopes = projectActive
136
+ ? projectPolicy.scopes
137
+ : { ...DEFAULT_RUNTIME_AUTHORITY_SCOPES, edits: true };
138
+ const policy = {
139
+ enabled: true,
140
+ allowPaths,
141
+ denyPaths,
142
+ scopes,
143
+ storyAllowPaths: storyAllow,
144
+ fenceSources: sources,
145
+ };
146
+ return {
147
+ policy,
148
+ sources,
149
+ fenceActive: true,
150
+ storyAllowPaths: storyAllow,
151
+ storyDenyPaths: storyDeny,
152
+ };
153
+ }
154
+ /**
155
+ * Resolve the write fence for PreToolUse / product sinks.
156
+ * Composes project policy with story fence extracted from an active xBRIEF path.
157
+ */
158
+ export function resolveWriteFenceForScope(projectPolicy, scopePath, loadStory = loadStoryWriteFenceFromPath) {
159
+ const story = loadStory(scopePath);
160
+ return resolveWriteFence(projectPolicy, story.fileScope, {
161
+ storyDenyPaths: story.denyPaths,
162
+ });
163
+ }
164
+ //# sourceMappingURL=write-fence.js.map
@@ -1,3 +1,4 @@
1
+ import { type AgentMergeEvaluateResult } from "../policy/require-human-merge.js";
1
2
  import { type SemanticGreenFn } from "./semantic-green.js";
2
3
  import type { MergeFn, MonitorFn, ProtectedCheckFn, WaitMergeableResult } from "./types.js";
3
4
  export interface WaitMergeableOptions {
@@ -11,6 +12,15 @@ export interface WaitMergeableOptions {
11
12
  readonly requireMasterCiGreen?: boolean;
12
13
  /** Target branch override for stale-base comparison (defaults to PR base.ref). */
13
14
  readonly baseBranch?: string | null;
15
+ /**
16
+ * Project root for human-merge + intent-ceiling preflight (#1193).
17
+ * Defaults to process.cwd(). Pass explicitly in tests.
18
+ */
19
+ readonly projectRoot?: string;
20
+ /** Inject agent-merge evaluator (unit tests). */
21
+ readonly agentMergeFn?: (projectRoot: string) => AgentMergeEvaluateResult;
22
+ /** When true, skip human-merge / intent preflight (tests only). */
23
+ readonly skipHumanMergeGate?: boolean;
14
24
  }
15
25
  /** Run protected-check -> wait -> merge cascade (#1369). */
16
26
  export declare function waitMergeableAndMerge(prNumber: number, repo: string, options: {
@@ -1,3 +1,5 @@
1
+ import { evaluateIntentCeilingFromEnv } from "../policy/intent-ceiling.js";
2
+ import { evaluateAgentMerge, } from "../policy/require-human-merge.js";
1
3
  import { classifyMonitorOutcome, parseMonitorPayload } from "./classify.js";
2
4
  import { EXIT_CONFIG_ERROR, EXIT_MERGED, EXIT_TIMEOUT_OR_ESCALATION } from "./constants.js";
3
5
  import { makeResult } from "./result.js";
@@ -17,6 +19,32 @@ export function waitMergeableAndMerge(prNumber, repo, options) {
17
19
  const mergeFn = options.mergeFn ?? runGhMerge;
18
20
  const semanticGreenFn = options.semanticGreenFn ?? evaluateSemanticGreen;
19
21
  const protectedIssues = options.protected;
22
+ const projectRoot = options.projectRoot ?? process.cwd();
23
+ // #1193 surface (1): refuse agent merge when requireHumanMerge is true,
24
+ // and refuse when slash-command intent ceiling does not authorize merge.
25
+ if (options.skipHumanMergeGate !== true) {
26
+ const intent = evaluateIntentCeilingFromEnv("merge");
27
+ if (!intent.allowed) {
28
+ return makeResult({
29
+ prNumber,
30
+ repo,
31
+ outcome: "config-error",
32
+ exitCode: EXIT_CONFIG_ERROR,
33
+ error: intent.reason,
34
+ });
35
+ }
36
+ const agentMergeFn = options.agentMergeFn ?? evaluateAgentMerge;
37
+ const hm = agentMergeFn(projectRoot);
38
+ if (!hm.allowed) {
39
+ return makeResult({
40
+ prNumber,
41
+ repo,
42
+ outcome: "config-error",
43
+ exitCode: EXIT_CONFIG_ERROR,
44
+ error: hm.message,
45
+ });
46
+ }
47
+ }
20
48
  let protectedCheckPayload = {};
21
49
  if (protectedIssues.length > 0) {
22
50
  const [prcRc, prcStdout, prcStderr] = protectedFn(prNumber, repo, protectedIssues);
@@ -1,5 +1,6 @@
1
1
  import { readFileSync, statSync } from "node:fs";
2
2
  import { basename, dirname } from "node:path";
3
+ import { evaluateIntentCeilingFromEnv } from "../policy/intent-ceiling.js";
3
4
  /** Canonical eligibility folder — only vbrief/active/ may spawn implementation. */
4
5
  export const ACTIVE_FOLDER = "active";
5
6
  /** Canonical eligibility status — only `running` signals an active handoff. */
@@ -124,6 +125,15 @@ export function evaluate(vbriefPath) {
124
125
  message: buildReject(path, `plan.status is '${status}' -- only '${ELIGIBLE_STATUS}' is eligible for implementation.`),
125
126
  };
126
127
  }
128
+ // Slash-command intent containment (#1193 / extends #810): non-implement session
129
+ // verbs must not authorize implementation preflight even when the xBRIEF is active.
130
+ const intent = evaluateIntentCeilingFromEnv("implement");
131
+ if (!intent.allowed) {
132
+ return {
133
+ exitCode: 1,
134
+ message: buildReject(path, intent.reason),
135
+ };
136
+ }
127
137
  return {
128
138
  exitCode: 0,
129
139
  message: `OK ${path} -- ready for implementation.`,
@@ -1,6 +1,7 @@
1
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
1
+ import { existsSync, mkdirSync, readFileSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
- import { dirname, join } from "node:path";
3
+ import { basename, dirname, join, resolve } from "node:path";
4
+ import { containedWrite } from "../fs/contained-write.js";
4
5
  import { DEFAULT_PRODUCT_SIGNAL_SINK_REPO } from "../policy/product-signal.js";
5
6
  import { platformUserConfigDir } from "../user-config/resolve-user-md.js";
6
7
  export const PRODUCT_SIGNAL_CONSENT_FILENAME = "product-signal-consent.json";
@@ -142,8 +143,15 @@ export function grantProductSignalConsent(options = {}) {
142
143
  sinkRepo,
143
144
  };
144
145
  const path = resolveProductSignalConsentPath(options);
145
- mkdirSync(dirname(path), { recursive: true });
146
- writeFileSync(path, `${JSON.stringify(record, null, 2)}\n`, "utf8");
146
+ // #2980 wave D: product write sink routes through containedWrite.
147
+ const dir = dirname(path);
148
+ mkdirSync(dir, { recursive: true });
149
+ containedWrite({
150
+ root: resolve(dir),
151
+ target: basename(path),
152
+ data: `${JSON.stringify(record, null, 2)}\n`,
153
+ mode: "replace",
154
+ });
147
155
  return record;
148
156
  }
149
157
  /** Revoke consent by setting revokedAt (#2693 D2). */
@@ -167,7 +175,15 @@ export function revokeProductSignalConsent(options = {}) {
167
175
  ...existing,
168
176
  revokedAt: now.toISOString().replace(/\.\d{3}Z$/, "Z"),
169
177
  };
170
- writeFileSync(path, `${JSON.stringify(revoked, null, 2)}\n`, "utf8");
178
+ // #2980 wave D: product write sink routes through containedWrite.
179
+ const dir = dirname(path);
180
+ mkdirSync(dir, { recursive: true });
181
+ containedWrite({
182
+ root: resolve(dir),
183
+ target: basename(path),
184
+ data: `${JSON.stringify(revoked, null, 2)}\n`,
185
+ mode: "replace",
186
+ });
171
187
  return true;
172
188
  }
173
189
  //# sourceMappingURL=consent.js.map
@@ -1,6 +1,7 @@
1
- import { appendFileSync, existsSync, mkdirSync, readFileSync } from "node:fs";
1
+ import { existsSync, readFileSync } from "node:fs";
2
2
  import { join, resolve } from "node:path";
3
- import { assertWriteTargetSafe, ProjectionContainmentError } from "../fs/projection-containment.js";
3
+ import { ContainedWriteError, containedWrite } from "../fs/contained-write.js";
4
+ import { ProjectionContainmentError } from "../fs/projection-containment.js";
4
5
  import { enableProductSignal, formatProductSignalStatusLine, resolveProductSignal, } from "../policy/product-signal.js";
5
6
  import { resolveProjectRoot } from "../scope/project-context.js";
6
7
  import { resolveActorName } from "./actor-name.js";
@@ -15,17 +16,26 @@ import { bootstrapProductSignalSink } from "./sink-bootstrap.js";
15
16
  export const PRODUCT_SIGNAL_LAST_SUBMIT_REL = join(".deft-cache", "product-signal-last-submit.json");
16
17
  function recordLastSubmit(projectRoot, outcome, url) {
17
18
  const path = resolve(projectRoot, PRODUCT_SIGNAL_LAST_SUBMIT_REL);
18
- assertWriteTargetSafe(projectRoot, path);
19
19
  try {
20
- mkdirSync(join(path, ".."), { recursive: true });
21
- appendFileSync(path, `${JSON.stringify({
22
- outcome,
23
- issueUrl: url,
24
- at: new Date().toISOString().replace(/\.\d{3}Z$/, "Z"),
25
- })}\n`, "utf8");
20
+ // #2980 wave D: product write sink routes through containedWrite.
21
+ const root = resolve(projectRoot);
22
+ containedWrite({
23
+ root,
24
+ target: path,
25
+ data: `${JSON.stringify({
26
+ outcome,
27
+ issueUrl: url,
28
+ at: new Date().toISOString().replace(/\.\d{3}Z$/, "Z"),
29
+ })}\n`,
30
+ mode: "append",
31
+ });
26
32
  }
27
- catch {
28
- // observability only
33
+ catch (err) {
34
+ // Containment refusals must fail closed (tests + product path) (#2807 / #2980).
35
+ if (err instanceof ContainedWriteError || err instanceof ProjectionContainmentError) {
36
+ throw err;
37
+ }
38
+ // other observability I/O failures only
29
39
  }
30
40
  }
31
41
  function readLastSubmitSummary(projectRoot) {
@@ -134,7 +144,7 @@ export async function submitProductSignal(options) {
134
144
  recordLastSubmit(root, result.outcome, result.issueUrl ?? null);
135
145
  }
136
146
  catch (err) {
137
- if (err instanceof ProjectionContainmentError) {
147
+ if (err instanceof ProjectionContainmentError || err instanceof ContainedWriteError) {
138
148
  return {
139
149
  outcome: "error-config",
140
150
  exitCode: 2,
@@ -1,8 +1,10 @@
1
1
  /**
2
2
  * Subprocess entry for sync callers (release pipeline, task build).
3
3
  * Top-level await keeps the process alive until the archive is written.
4
+ * Progress ticks go to stderr so pipeline callers can still capture the
5
+ * archive path on stdout (#2953).
4
6
  */
5
- import { buildArchive, selectFormat } from "./build-dist.js";
7
+ import { buildArchive, emitBuildProgress, selectFormat } from "./build-dist.js";
6
8
  const version = process.argv[2];
7
9
  const root = process.argv[3];
8
10
  if (!version || !root) {
@@ -11,7 +13,8 @@ if (!version || !root) {
11
13
  }
12
14
  const fmt = selectFormat(process.env.DEFT_BUILD_FORMAT);
13
15
  try {
14
- const out = await buildArchive(root, version, fmt);
16
+ process.stderr.write(`build-dist-runner: start version=${version} format=${fmt}\n`);
17
+ const out = await buildArchive(root, version, fmt, { onProgress: emitBuildProgress });
15
18
  process.stdout.write(`${out}\n`);
16
19
  }
17
20
  catch (err) {
@@ -8,7 +8,25 @@ export declare function iterSourceFiles(root: string, excludes?: ReadonlySet<str
8
8
  }>;
9
9
  export declare function selectFormat(arg: string | null | undefined): "tar" | "zip";
10
10
  export declare function outputPath(root: string, version: string, fmt: "tar" | "zip"): string;
11
- export declare function buildArchive(root: string, version: string, fmt: "tar" | "zip", extraExcludes?: readonly string[]): Promise<string>;
11
+ export type BuildProgress = {
12
+ readonly stage: string;
13
+ readonly detail?: string;
14
+ readonly current?: number;
15
+ readonly total?: number;
16
+ };
17
+ export type BuildArchiveOptions = {
18
+ readonly extraExcludes?: readonly string[];
19
+ /** Progress sink; default silent so unit tests stay quiet (#2953). */
20
+ readonly onProgress?: (p: BuildProgress) => void;
21
+ };
22
+ /**
23
+ * Emit human-readable stage progress for long release packaging runs (#2953).
24
+ * Writes to stderr so stdout stays the archive path for pipeline capture.
25
+ */
26
+ export declare function emitBuildProgress(progress: BuildProgress, stream?: {
27
+ write: (chunk: string) => unknown;
28
+ }): void;
29
+ export declare function buildArchive(root: string, version: string, fmt: "tar" | "zip", options?: readonly string[] | BuildArchiveOptions): Promise<string>;
12
30
  export declare function parseExtraExcludes(raw: string): string[];
13
31
  export declare function main(argv: readonly string[]): Promise<number>;
14
32
  //# sourceMappingURL=build-dist.d.ts.map
@@ -16,6 +16,9 @@ export const DEFAULT_EXCLUDES = new Set([
16
16
  ".ruff_cache",
17
17
  ".coverage",
18
18
  "coverage",
19
+ // Scratch worktrees are never shippable framework content (#2953).
20
+ ".deft-scratch",
21
+ "swarm-worktrees",
19
22
  ]);
20
23
  export const DEFAULT_EXCLUDED_PATH_PREFIXES = [
21
24
  "history/archive",
@@ -97,14 +100,46 @@ export function outputPath(root, version, fmt) {
97
100
  const suffix = fmt === "zip" ? "zip" : "tar.gz";
98
101
  return join(root, "dist", `deft-${version}.${suffix}`);
99
102
  }
100
- export async function buildArchive(root, version, fmt, extraExcludes = []) {
103
+ /**
104
+ * Emit human-readable stage progress for long release packaging runs (#2953).
105
+ * Writes to stderr so stdout stays the archive path for pipeline capture.
106
+ */
107
+ export function emitBuildProgress(progress, stream = process.stderr) {
108
+ const { stage, detail, current, total } = progress;
109
+ let line = `build-dist: ${stage}`;
110
+ if (typeof current === "number" && typeof total === "number" && total > 0) {
111
+ const pct = Math.min(100, Math.floor((current / total) * 100));
112
+ line += ` ${current}/${total} (${pct}%)`;
113
+ }
114
+ if (detail)
115
+ line += ` - ${detail}`;
116
+ stream.write(`${line}\n`);
117
+ }
118
+ function isExtraExcludesList(options) {
119
+ return Array.isArray(options);
120
+ }
121
+ function resolveBuildOptions(options = {}) {
122
+ // Backward-compatible: 4th arg may be a bare extra-excludes array (pre-#2953).
123
+ if (isExtraExcludesList(options)) {
124
+ return { extraExcludes: options, onProgress: () => { } };
125
+ }
126
+ return {
127
+ extraExcludes: options.extraExcludes ?? [],
128
+ onProgress: options.onProgress ?? (() => { }),
129
+ };
130
+ }
131
+ export async function buildArchive(root, version, fmt, options = {}) {
132
+ const { extraExcludes, onProgress } = resolveBuildOptions(options);
101
133
  const excludes = new Set([...DEFAULT_EXCLUDES, ...extraExcludes]);
102
134
  const output = outputPath(root, version, fmt);
103
135
  mkdirSync(dirname(output), { recursive: true });
104
136
  if (existsSync(output)) {
105
137
  unlinkSync(output);
106
138
  }
139
+ onProgress({ stage: "scan", detail: "enumerating source files" });
107
140
  const entries = iterSourceFiles(root, excludes);
141
+ const total = entries.length;
142
+ onProgress({ stage: "scan", detail: `found ${total} files`, current: total, total });
108
143
  await new Promise((resolvePromise, reject) => {
109
144
  const out = createWriteStream(output);
110
145
  const archive = fmt === "zip"
@@ -114,11 +149,21 @@ export async function buildArchive(root, version, fmt, extraExcludes = []) {
114
149
  out.on("error", (err) => reject(err));
115
150
  archive.on("error", (err) => reject(err));
116
151
  archive.pipe(out);
152
+ onProgress({ stage: "pack", detail: `format=${fmt}`, current: 0, total });
153
+ // Progress ticks every ~5% (or every 250 files) so long packs do not look hung.
154
+ const tickEvery = Math.max(1, Math.min(250, Math.ceil(total / 20)));
155
+ let i = 0;
117
156
  for (const { absPath, archiveRel } of entries) {
118
157
  archive.file(absPath, { name: `${ARCHIVE_ROOT}/${archiveRel}` });
158
+ i += 1;
159
+ if (i === total || i % tickEvery === 0) {
160
+ onProgress({ stage: "pack", current: i, total });
161
+ }
119
162
  }
163
+ onProgress({ stage: "finalize", detail: output });
120
164
  void archive.finalize();
121
165
  });
166
+ onProgress({ stage: "done", detail: output, current: total, total });
122
167
  return output;
123
168
  }
124
169
  export function parseExtraExcludes(raw) {
@@ -159,7 +204,10 @@ export async function main(argv) {
159
204
  }
160
205
  const fmt = selectFormat(fmtArg);
161
206
  try {
162
- const out = await buildArchive(projectRoot, version, fmt, parseExtraExcludes(extra));
207
+ const out = await buildArchive(projectRoot, version, fmt, {
208
+ extraExcludes: parseExtraExcludes(extra),
209
+ onProgress: emitBuildProgress,
210
+ });
163
211
  const printable = relative(projectRoot, out) || out;
164
212
  process.stdout.write(`Created ${printable}\n`);
165
213
  return 0;
@@ -79,16 +79,21 @@ export function runBuildNative(projectRoot, version, _seams = {}) {
79
79
  return [false, "build requires a release version"];
80
80
  }
81
81
  const env = { ...process.env, DEFT_RELEASE_VERSION: version };
82
+ // Inherit stderr so build-dist-runner progress ticks stream live during
83
+ // release Step 8; capture stdout only for the archive path (#2953).
82
84
  const result = spawnSync(process.execPath, [BUILD_DIST_RUNNER, version, projectRoot], {
83
85
  cwd: projectRoot,
84
86
  encoding: "utf8",
85
87
  env,
88
+ stdio: ["ignore", "pipe", "inherit"],
86
89
  });
87
90
  if (result.status !== 0) {
88
- const msg = (result.stderr ?? result.stdout ?? "").trim() || `exit ${result.status ?? 1}`;
91
+ const captured = (typeof result.stderr === "string" ? result.stderr : "") ||
92
+ (typeof result.stdout === "string" ? result.stdout : "");
93
+ const msg = captured.trim() || `exit ${result.status ?? 1}`;
89
94
  return [false, `build failed: ${msg}`];
90
95
  }
91
- const out = (result.stdout ?? "").trim();
96
+ const out = (typeof result.stdout === "string" ? result.stdout : "").trim();
92
97
  return [true, `build ran clean (DEFT_RELEASE_VERSION=${version}) -> ${out}`];
93
98
  }
94
99
  //# sourceMappingURL=native-steps.js.map
@@ -24,6 +24,19 @@ export function spawnText(cmd, args, options = {}) {
24
24
  if (status === null) {
25
25
  if (result.signal !== null && result.signal !== undefined) {
26
26
  status = 128;
27
+ // Timeout kills leave stderr empty; surface timeout so e2e never reports
28
+ // "failed: " with a blank reason (#3004 / #1867).
29
+ if (stderr.trim().length === 0) {
30
+ if (result.error) {
31
+ stderr = result.error.message;
32
+ }
33
+ else if (options.timeoutMs !== undefined) {
34
+ stderr = `process terminated by signal ${result.signal} (timeout ${options.timeoutMs}ms)`;
35
+ }
36
+ else {
37
+ stderr = `process terminated by signal ${result.signal}`;
38
+ }
39
+ }
27
40
  }
28
41
  else if (result.error) {
29
42
  status = 2;
@@ -1,4 +1,11 @@
1
+ import type { SpawnResult } from "../release/types.js";
1
2
  import type { E2ESeams } from "./types.js";
3
+ /** Mirror push can exceed the generic release `runGit` 30s cap (#3004). Align with clone. */
4
+ export declare const PUSH_MIRROR_TIMEOUT_MS = 300000;
5
+ /** Prefer stderr; never leave a blank reason after "failed:" (#3004). */
6
+ export declare function formatGitOpFailure(op: string, result: SpawnResult, options?: {
7
+ timeoutMs?: number;
8
+ }): string;
2
9
  export declare function cloneRepoToTemp(projectRoot: string, targetDir: string, seams?: E2ESeams): [boolean, string];
3
10
  export declare function setOriginToTempRepo(cloneDir: string, owner: string, slug: string, seams?: E2ESeams): [boolean, string];
4
11
  export declare function pushMirror(cloneDir: string, seams?: E2ESeams): [boolean, string];
@@ -1,11 +1,22 @@
1
1
  import { runGit as releaseRunGit } from "../release/git.js";
2
2
  import { spawnText } from "../release/spawn.js";
3
+ /** Mirror push can exceed the generic release `runGit` 30s cap (#3004). Align with clone. */
4
+ export const PUSH_MIRROR_TIMEOUT_MS = 300_000;
3
5
  function defaultRunGit(projectRoot, args, env, seams = {}) {
4
6
  if (seams.runGit) {
5
7
  return seams.runGit(projectRoot, args, env);
6
8
  }
7
9
  return releaseRunGit(projectRoot, args, { spawnText: seams.spawnText ?? spawnText }, env);
8
10
  }
11
+ /** Prefer stderr; never leave a blank reason after "failed:" (#3004). */
12
+ export function formatGitOpFailure(op, result, options = {}) {
13
+ const stderr = result.stderr.trim();
14
+ if (stderr.length > 0) {
15
+ return `${op} failed: ${stderr}`;
16
+ }
17
+ const timeoutHint = options.timeoutMs !== undefined ? `; possible timeout after ${options.timeoutMs}ms` : "";
18
+ return `${op} failed: no stderr (exit ${result.status ?? "null"}${timeoutHint})`;
19
+ }
9
20
  export function cloneRepoToTemp(projectRoot, targetDir, seams = {}) {
10
21
  const env = { ...process.env, DEFT_PROJECT_ROOT: targetDir };
11
22
  const spawn = seams.spawnText ?? spawnText;
@@ -27,9 +38,31 @@ export function setOriginToTempRepo(cloneDir, owner, slug, seams = {}) {
27
38
  return [true, `origin -> ${url}`];
28
39
  }
29
40
  export function pushMirror(cloneDir, seams = {}) {
30
- const result = defaultRunGit(cloneDir, ["push", "origin", "refs/heads/*:refs/heads/*", "refs/tags/*:refs/tags/*"], undefined, seams);
41
+ // Do not use default release runGit (30s) full heads+tags mirror of directive
42
+ // routinely exceeds that on first push to an empty temp repo (#3004).
43
+ const pushArgs = [
44
+ "push",
45
+ "origin",
46
+ "refs/heads/*:refs/heads/*",
47
+ "refs/tags/*:refs/tags/*",
48
+ ];
49
+ let result;
50
+ if (seams.runGit) {
51
+ result = seams.runGit(cloneDir, [...pushArgs], undefined);
52
+ }
53
+ else {
54
+ const spawn = seams.spawnText ?? spawnText;
55
+ result = spawn("git", ["-C", cloneDir, ...pushArgs], {
56
+ timeoutMs: PUSH_MIRROR_TIMEOUT_MS,
57
+ });
58
+ }
31
59
  if (result.status !== 0) {
32
- return [false, `git push (heads+tags refspecs) failed: ${result.stderr.trim()}`];
60
+ return [
61
+ false,
62
+ formatGitOpFailure("git push (heads+tags refspecs)", result, {
63
+ timeoutMs: PUSH_MIRROR_TIMEOUT_MS,
64
+ }),
65
+ ];
33
66
  }
34
67
  return [true, "pushed heads + tags to temp origin"];
35
68
  }
@@ -1,4 +1,17 @@
1
+ import { type EnvMap } from "../authz/closed-verb.js";
2
+ import type { ClosedVerbDecision, HumanOriginGrant } from "../authz/types.js";
1
3
  import type { PublishConfig, ReleasePublishSeams } from "./types.js";
2
4
  export declare function emit(label: string, status: string): void;
5
+ /**
6
+ * Fail-closed gate before draft→public (#1095 Wave 4).
7
+ * Allow only DEFT_ALLOW_RELEASE_PUBLISH=1 or a matching human-origin grant.
8
+ * Loads conventions/verb-classification.json from projectRoot when present.
9
+ * Test seam: pass grants / env via optional overrides on seams when present.
10
+ */
11
+ export declare function evaluateReleasePublishGate(version: string, projectRoot: string, options?: {
12
+ readonly grants?: readonly HumanOriginGrant[];
13
+ readonly env?: EnvMap;
14
+ readonly repo?: string | null;
15
+ }): ClosedVerbDecision;
3
16
  export declare function runPublish(config: PublishConfig, seams?: ReleasePublishSeams): number;
4
17
  //# sourceMappingURL=pipeline.d.ts.map