@deftai/directive-core 0.88.0 → 0.89.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 (168) 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/doctor-state.js +28 -4
  33. package/dist/doctor/main.d.ts +10 -0
  34. package/dist/doctor/main.js +160 -0
  35. package/dist/doctor/types.d.ts +11 -0
  36. package/dist/escalation/actions.d.ts +63 -0
  37. package/dist/escalation/actions.js +137 -0
  38. package/dist/escalation/index.d.ts +8 -0
  39. package/dist/escalation/index.js +8 -0
  40. package/dist/escalation/paths.d.ts +3 -0
  41. package/dist/escalation/paths.js +10 -0
  42. package/dist/escalation/store.d.ts +17 -0
  43. package/dist/escalation/store.js +172 -0
  44. package/dist/escalation/types.d.ts +73 -0
  45. package/dist/escalation/types.js +43 -0
  46. package/dist/eval/crud-telemetry.js +30 -10
  47. package/dist/eval/health.js +22 -7
  48. package/dist/eval/readback.js +11 -10
  49. package/dist/eval/run.js +32 -16
  50. package/dist/events/attribution-enrichment.js +10 -4
  51. package/dist/finish-loop/directive-finish-loop.d.ts +37 -0
  52. package/dist/finish-loop/directive-finish-loop.js +239 -0
  53. package/dist/finish-loop/grant-gate.d.ts +31 -0
  54. package/dist/finish-loop/grant-gate.js +169 -0
  55. package/dist/finish-loop/index.d.ts +11 -0
  56. package/dist/finish-loop/index.js +11 -0
  57. package/dist/finish-loop/main.d.ts +35 -0
  58. package/dist/finish-loop/main.js +365 -0
  59. package/dist/finish-loop/pr-finish-loop.d.ts +34 -0
  60. package/dist/finish-loop/pr-finish-loop.js +217 -0
  61. package/dist/finish-loop/progress.d.ts +17 -0
  62. package/dist/finish-loop/progress.js +45 -0
  63. package/dist/finish-loop/queue.d.ts +16 -0
  64. package/dist/finish-loop/queue.js +65 -0
  65. package/dist/finish-loop/types.d.ts +52 -0
  66. package/dist/finish-loop/types.js +10 -0
  67. package/dist/fs/contained-write.d.ts +101 -0
  68. package/dist/fs/contained-write.js +281 -0
  69. package/dist/hooks/classify/classify.d.ts +10 -0
  70. package/dist/hooks/classify/classify.js +37 -0
  71. package/dist/hooks/classify/index.d.ts +14 -0
  72. package/dist/hooks/classify/index.js +13 -0
  73. package/dist/hooks/classify/paths.d.ts +22 -0
  74. package/dist/hooks/classify/paths.js +72 -0
  75. package/dist/hooks/classify/payload.d.ts +16 -0
  76. package/dist/hooks/classify/payload.js +45 -0
  77. package/dist/hooks/classify/stdin.d.ts +12 -0
  78. package/dist/hooks/classify/stdin.js +63 -0
  79. package/dist/hooks/classify/tool-name.d.ts +18 -0
  80. package/dist/hooks/classify/tool-name.js +85 -0
  81. package/dist/hooks/classify/types.d.ts +41 -0
  82. package/dist/hooks/classify/types.js +7 -0
  83. package/dist/hooks/dispatcher.d.ts +21 -18
  84. package/dist/hooks/dispatcher.js +212 -163
  85. package/dist/hooks/fixtures/cases.d.ts +44 -0
  86. package/dist/hooks/fixtures/cases.js +595 -0
  87. package/dist/hooks/fixtures/index.d.ts +2 -0
  88. package/dist/hooks/fixtures/index.js +2 -0
  89. package/dist/hooks/index.d.ts +2 -0
  90. package/dist/hooks/index.js +2 -0
  91. package/dist/index.d.ts +4 -0
  92. package/dist/index.js +5 -0
  93. package/dist/init-deposit/agent-hooks.js +27 -8
  94. package/dist/init-deposit/gitignore.js +11 -3
  95. package/dist/init-deposit/headless-manifest.js +12 -4
  96. package/dist/init-deposit/migrate.d.ts +1 -1
  97. package/dist/init-deposit/migrate.js +13 -3
  98. package/dist/init-deposit/prettierignore.js +10 -3
  99. package/dist/init-deposit/scaffold.js +35 -26
  100. package/dist/init-deposit/xbrief-projections.js +11 -4
  101. package/dist/intake/candidates-log.js +46 -35
  102. package/dist/intake/github-body-cli.d.ts +6 -0
  103. package/dist/intake/github-body-cli.js +17 -0
  104. package/dist/intake/github-body.d.ts +46 -3
  105. package/dist/intake/github-body.js +191 -20
  106. package/dist/intake/issue-emit.d.ts +1 -0
  107. package/dist/intake/issue-emit.js +41 -23
  108. package/dist/lifecycle/event-detect.js +12 -4
  109. package/dist/lifecycle/events.js +29 -20
  110. package/dist/lifecycle/lifecycle-hygiene.js +9 -3
  111. package/dist/packs/pack-render.js +19 -6
  112. package/dist/plan-sequence/store.js +11 -4
  113. package/dist/policy/hotfix-criteria.d.ts +79 -0
  114. package/dist/policy/hotfix-criteria.js +197 -0
  115. package/dist/policy/index.d.ts +4 -0
  116. package/dist/policy/index.js +31 -1
  117. package/dist/policy/intent-ceiling.d.ts +79 -0
  118. package/dist/policy/intent-ceiling.js +181 -0
  119. package/dist/policy/no-deft-directive.js +10 -3
  120. package/dist/policy/org-force-on-migration.js +9 -5
  121. package/dist/policy/require-human-merge.d.ts +75 -0
  122. package/dist/policy/require-human-merge.js +324 -0
  123. package/dist/policy/resolve.js +17 -6
  124. package/dist/policy/runtime-authority.d.ts +15 -2
  125. package/dist/policy/runtime-authority.js +23 -3
  126. package/dist/policy/write-fence.d.ts +78 -0
  127. package/dist/policy/write-fence.js +164 -0
  128. package/dist/pr-wait-mergeable/cascade.d.ts +10 -0
  129. package/dist/pr-wait-mergeable/cascade.js +28 -0
  130. package/dist/preflight/evaluate.js +10 -0
  131. package/dist/product-signal/consent.js +21 -5
  132. package/dist/product-signal/submit.js +22 -12
  133. package/dist/release/build-dist-runner.js +5 -2
  134. package/dist/release/build-dist.d.ts +19 -1
  135. package/dist/release/build-dist.js +50 -2
  136. package/dist/release/native-steps.js +7 -2
  137. package/dist/release-publish/pipeline.d.ts +13 -0
  138. package/dist/release-publish/pipeline.js +46 -1
  139. package/dist/scope/audit-log.js +19 -24
  140. package/dist/scope/decompose.js +10 -5
  141. package/dist/scope/decomposed-refs.js +18 -3
  142. package/dist/scope/demote.js +9 -2
  143. package/dist/scope/undo.js +9 -2
  144. package/dist/session/release-availability.js +26 -6
  145. package/dist/session/ritual-sentinel.js +19 -12
  146. package/dist/session/session-start.js +7 -0
  147. package/dist/staleness-tickler/state.js +26 -6
  148. package/dist/swarm/launch.js +13 -3
  149. package/dist/swarm/routing.js +9 -3
  150. package/dist/task-surface/index.js +24 -9
  151. package/dist/triage/bootstrap/gitignore.js +53 -10
  152. package/dist/triage/cache-path.js +10 -3
  153. package/dist/triage/welcome/summary.js +11 -4
  154. package/dist/triage/welcome/writers.js +45 -16
  155. package/dist/validate-content/validate-links.js +5 -0
  156. package/dist/value/readback.js +11 -6
  157. package/dist/vbrief-activate/activate.js +12 -4
  158. package/dist/vbrief-build/project-definition-io.js +14 -6
  159. package/dist/verify-source/contained-writes.d.ts +59 -0
  160. package/dist/verify-source/contained-writes.js +272 -0
  161. package/dist/verify-source/index.d.ts +1 -0
  162. package/dist/verify-source/index.js +1 -0
  163. package/dist/verify-source/openclaw-tier1.js +14 -1
  164. package/dist/verify-source/verify-stubs.js +3 -0
  165. package/dist/xbrief-migrate/migrate-project.js +26 -12
  166. package/dist/xbrief-migrate/transforms.d.ts +4 -2
  167. package/dist/xbrief-migrate/transforms.js +37 -14
  168. package/package.json +15 -3
package/dist/eval/run.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import { createHash } from "node:crypto";
2
- import { appendFileSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
2
+ import { mkdirSync, mkdtempSync, rmSync } from "node:fs";
3
3
  import { tmpdir } from "node:os";
4
- import { dirname, join } from "node:path";
4
+ import { join, resolve } from "node:path";
5
5
  import { readCorePackageVersion } from "../engine-version.js";
6
- import { assertWriteTargetSafe } from "../fs/projection-containment.js";
6
+ import { containedWrite } from "../fs/contained-write.js";
7
7
  import { resolveEvalPath } from "../layout/resolve.js";
8
8
  import { BYTE_DIFF_WHOLE_FILE_THRESHOLD, InstrumentedVbriefCrud } from "./crud-telemetry.js";
9
9
  import { evaluateHealth } from "./health.js";
@@ -94,16 +94,28 @@ function gradeCrudSurgicalUpdate(context) {
94
94
  };
95
95
  }
96
96
  function seedHealthFixture(tempDir) {
97
- writeFileSync(join(tempDir, "xbrief", "PROJECT-DEFINITION.xbrief.json"), JSON.stringify({
98
- xBRIEFInfo: { version: "0.6" },
99
- plan: {
100
- title: "Golden health fixture",
101
- status: "running",
102
- items: [],
103
- "x-directive/policy": { triageScope: [{ rule: "all-open" }] },
104
- },
105
- }), "utf8");
106
- writeFileSync(join(tempDir, "AGENTS.md"), "<!-- deft:managed-section v3 -->\n<!-- /deft:managed-section -->\n", "utf8");
97
+ const root = resolve(tempDir);
98
+ // #2980 wave C: product fixture seed routes through containedWrite.
99
+ containedWrite({
100
+ root,
101
+ target: join("xbrief", "PROJECT-DEFINITION.xbrief.json"),
102
+ data: JSON.stringify({
103
+ xBRIEFInfo: { version: "0.6" },
104
+ plan: {
105
+ title: "Golden health fixture",
106
+ status: "running",
107
+ items: [],
108
+ "x-directive/policy": { triageScope: [{ rule: "all-open" }] },
109
+ },
110
+ }),
111
+ mode: "replace",
112
+ });
113
+ containedWrite({
114
+ root,
115
+ target: "AGENTS.md",
116
+ data: "<!-- deft:managed-section v3 -->\n<!-- /deft:managed-section -->\n",
117
+ mode: "replace",
118
+ });
107
119
  }
108
120
  function gradeHealthFixture(context) {
109
121
  seedHealthFixture(context.tempDir);
@@ -195,9 +207,13 @@ export function goldenRunsHistoryPath(projectRoot) {
195
207
  /** Append one golden run to the versioned ledger (#1703 Tier 2). */
196
208
  export function persistGoldenRun(projectRoot, record) {
197
209
  const path = goldenRunsHistoryPath(projectRoot);
198
- assertWriteTargetSafe(projectRoot, path);
199
- mkdirSync(dirname(path), { recursive: true });
200
- appendFileSync(path, `${JSON.stringify(record)}\n`, "utf8");
210
+ // #2980 wave C: product write sink routes through containedWrite.
211
+ containedWrite({
212
+ root: resolve(projectRoot),
213
+ target: path,
214
+ data: `${JSON.stringify(record)}\n`,
215
+ mode: "append",
216
+ });
201
217
  }
202
218
  /** Stable hash for rotating holdout selection (#1703 Goodhart mitigation). */
203
219
  export function holdoutRotationIndex(directiveVersion, model, holdoutCount) {
@@ -1,7 +1,8 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
- import { dirname, join, resolve } from "node:path";
2
+ import { existsSync, readFileSync } from "node:fs";
3
+ import { join, resolve } from "node:path";
4
4
  import { readCorePackageVersion } from "../engine-version.js";
5
+ import { containedWrite } from "../fs/contained-write.js";
5
6
  import { inferRepoFromGit } from "../triage/queue/repo.js";
6
7
  /** Record-shape version for attribution ledger entries (#2376). Bump on shape changes. */
7
8
  export const ATTRIBUTION_SCHEMA_VERSION = 1;
@@ -26,8 +27,13 @@ export function resolveInstallId(projectRoot) {
26
27
  }
27
28
  try {
28
29
  const id = randomUUID();
29
- mkdirSync(dirname(path), { recursive: true });
30
- writeFileSync(path, `${id}\n`, "utf8");
30
+ // #2951 Phase 2: product write sink routes through containedWrite.
31
+ containedWrite({
32
+ root: resolve(projectRoot),
33
+ target: path,
34
+ data: `${id}\n`,
35
+ mode: "replace",
36
+ });
31
37
  return id;
32
38
  }
33
39
  catch {
@@ -0,0 +1,37 @@
1
+ /**
2
+ * directive:finish-loop — outer walk-away cascade (#871 Wave 5).
3
+ *
4
+ * 1. Gate on finish-loop human-origin grant (or DEFT_ALLOW_FINISH_LOOP)
5
+ * 2. Scan xbrief queue (active/pending)
6
+ * 3. Append progress JSONL each iteration
7
+ * 4. When --pr supplied, run pr:finish-loop
8
+ * 5. Implementation steps are agent-owned: CLI emits AGENT_STEP and exits
9
+ * ACTION_REQUIRED so a live agent continues; full autonomous coding is
10
+ * not inlined in the Taskfile.
11
+ *
12
+ * Halt: empty queue | grant expiry/deny | max iterations | gate deny
13
+ */
14
+ import { type PrFinishLoopOptions, runPrFinishLoop } from "./pr-finish-loop.js";
15
+ import { scanFinishLoopQueue } from "./queue.js";
16
+ import { type DirectiveFinishLoopResult } from "./types.js";
17
+ export declare const DEFAULT_MAX_ITERATIONS = 20;
18
+ export interface DirectiveFinishLoopOptions {
19
+ readonly projectRoot: string;
20
+ /** Optional PR number to shepherd via pr:finish-loop this iteration. */
21
+ readonly prNumber?: number | null;
22
+ readonly repo?: string | null;
23
+ readonly maxIterations?: number;
24
+ readonly maxWaitMinutes?: number;
25
+ readonly pollSeconds?: number;
26
+ readonly oneShot?: boolean;
27
+ readonly merge?: boolean;
28
+ readonly env?: Readonly<Record<string, string | undefined>>;
29
+ readonly now?: Date;
30
+ readonly skipGrantGate?: boolean;
31
+ /** Inject for tests. */
32
+ readonly scanQueueFn?: typeof scanFinishLoopQueue;
33
+ readonly prFinishLoopFn?: (opts: PrFinishLoopOptions) => ReturnType<typeof runPrFinishLoop>;
34
+ readonly writeProgress?: boolean;
35
+ }
36
+ export declare function runDirectiveFinishLoop(options: DirectiveFinishLoopOptions): DirectiveFinishLoopResult;
37
+ //# sourceMappingURL=directive-finish-loop.d.ts.map
@@ -0,0 +1,239 @@
1
+ /**
2
+ * directive:finish-loop — outer walk-away cascade (#871 Wave 5).
3
+ *
4
+ * 1. Gate on finish-loop human-origin grant (or DEFT_ALLOW_FINISH_LOOP)
5
+ * 2. Scan xbrief queue (active/pending)
6
+ * 3. Append progress JSONL each iteration
7
+ * 4. When --pr supplied, run pr:finish-loop
8
+ * 5. Implementation steps are agent-owned: CLI emits AGENT_STEP and exits
9
+ * ACTION_REQUIRED so a live agent continues; full autonomous coding is
10
+ * not inlined in the Taskfile.
11
+ *
12
+ * Halt: empty queue | grant expiry/deny | max iterations | gate deny
13
+ */
14
+ import { evaluateFinishLoopGrant } from "./grant-gate.js";
15
+ import { runPrFinishLoop } from "./pr-finish-loop.js";
16
+ import { appendFinishLoopProgress, finishLoopProgressPath, makeProgressLine } from "./progress.js";
17
+ import { scanFinishLoopQueue } from "./queue.js";
18
+ import { EXIT_ACTION_REQUIRED, EXIT_BLOCKED, EXIT_OK, } from "./types.js";
19
+ export const DEFAULT_MAX_ITERATIONS = 20;
20
+ export function runDirectiveFinishLoop(options) {
21
+ const projectRoot = options.projectRoot;
22
+ const progressPath = finishLoopProgressPath(projectRoot);
23
+ const maxIterations = options.maxIterations ?? DEFAULT_MAX_ITERATIONS;
24
+ const writeProgress = options.writeProgress !== false;
25
+ const scan = options.scanQueueFn ?? scanFinishLoopQueue;
26
+ const prLoop = options.prFinishLoopFn ?? runPrFinishLoop;
27
+ const progress = (phase, iteration, haltReason, message, fields = {}) => {
28
+ if (!writeProgress)
29
+ return;
30
+ appendFinishLoopProgress(projectRoot, makeProgressLine({
31
+ phase,
32
+ iteration,
33
+ haltReason,
34
+ message,
35
+ prNumber: fields.prNumber ?? options.prNumber ?? null,
36
+ grantId: fields.grantId ?? null,
37
+ queueCount: fields.queueCount ?? null,
38
+ exitCode: fields.exitCode ?? null,
39
+ extra: fields.extra,
40
+ }));
41
+ };
42
+ // --- Gate ---
43
+ let grantId = null;
44
+ if (options.skipGrantGate !== true) {
45
+ const gate = evaluateFinishLoopGrant({
46
+ projectRoot,
47
+ env: options.env,
48
+ now: options.now,
49
+ });
50
+ if (!gate.allowed) {
51
+ const haltReason = gate.code === "authz-grant-expired" ? "grant-expired" : "grant-missing";
52
+ const message = `BLOCKED directive:finish-loop: ${gate.reason}`;
53
+ progress("gate", 0, haltReason, message, {
54
+ grantId: gate.grantId,
55
+ exitCode: EXIT_BLOCKED,
56
+ });
57
+ return {
58
+ exitCode: EXIT_BLOCKED,
59
+ haltReason,
60
+ message,
61
+ iterations: 0,
62
+ queueCount: 0,
63
+ grantId: gate.grantId,
64
+ progressPath,
65
+ };
66
+ }
67
+ grantId = gate.grantId;
68
+ progress("gate", 0, null, `grant ok: ${gate.reason}`, { grantId });
69
+ }
70
+ let lastQueueCount = 0;
71
+ for (let iteration = 1; iteration <= maxIterations; iteration += 1) {
72
+ // Re-check grant each iteration (expiry mid-loop)
73
+ if (options.skipGrantGate !== true) {
74
+ const gate = evaluateFinishLoopGrant({
75
+ projectRoot,
76
+ env: options.env,
77
+ now: options.now,
78
+ });
79
+ if (!gate.allowed) {
80
+ const haltReason = gate.code === "authz-grant-expired" ? "grant-expired" : "grant-deny";
81
+ const message = `BLOCKED directive:finish-loop mid-loop: ${gate.reason}`;
82
+ progress("halt", iteration, haltReason, message, {
83
+ grantId: gate.grantId,
84
+ exitCode: EXIT_BLOCKED,
85
+ queueCount: lastQueueCount,
86
+ });
87
+ return {
88
+ exitCode: EXIT_BLOCKED,
89
+ haltReason,
90
+ message,
91
+ iterations: iteration,
92
+ queueCount: lastQueueCount,
93
+ grantId: gate.grantId,
94
+ progressPath,
95
+ };
96
+ }
97
+ grantId = gate.grantId;
98
+ }
99
+ const queue = scan(projectRoot);
100
+ lastQueueCount = queue.length;
101
+ progress("queue-scan", iteration, null, `queue count=${queue.length}`, {
102
+ grantId,
103
+ queueCount: queue.length,
104
+ });
105
+ if (queue.length === 0 && (options.prNumber === null || options.prNumber === undefined)) {
106
+ const message = "directive:finish-loop complete: empty scope queue (no active/pending xBRIEF work).";
107
+ progress("halt", iteration, "empty-queue", message, {
108
+ grantId,
109
+ queueCount: 0,
110
+ exitCode: EXIT_OK,
111
+ });
112
+ return {
113
+ exitCode: EXIT_OK,
114
+ haltReason: "empty-queue",
115
+ message,
116
+ iterations: iteration,
117
+ queueCount: 0,
118
+ grantId,
119
+ progressPath,
120
+ };
121
+ }
122
+ // Optional PR shepherd
123
+ if (options.prNumber !== null && options.prNumber !== undefined) {
124
+ const prResult = prLoop({
125
+ projectRoot,
126
+ prNumber: options.prNumber,
127
+ repo: options.repo,
128
+ maxWaitMinutes: options.maxWaitMinutes,
129
+ pollSeconds: options.pollSeconds,
130
+ oneShot: options.oneShot,
131
+ merge: options.merge,
132
+ skipGrantGate: true, // already gated
133
+ env: options.env,
134
+ now: options.now,
135
+ writeProgress,
136
+ iteration,
137
+ });
138
+ progress("pr-watch", iteration, prResult.haltReason, prResult.message, {
139
+ grantId,
140
+ queueCount: queue.length,
141
+ exitCode: prResult.exitCode,
142
+ prNumber: options.prNumber,
143
+ });
144
+ if (prResult.haltReason === "address-findings") {
145
+ return {
146
+ exitCode: EXIT_ACTION_REQUIRED,
147
+ haltReason: "address-findings",
148
+ message: prResult.message,
149
+ iterations: iteration,
150
+ queueCount: queue.length,
151
+ grantId,
152
+ progressPath,
153
+ };
154
+ }
155
+ if (prResult.haltReason === "require-human-merge") {
156
+ return {
157
+ exitCode: EXIT_ACTION_REQUIRED,
158
+ haltReason: "require-human-merge",
159
+ message: prResult.message,
160
+ iterations: iteration,
161
+ queueCount: queue.length,
162
+ grantId,
163
+ progressPath,
164
+ };
165
+ }
166
+ if (prResult.exitCode === EXIT_BLOCKED) {
167
+ return {
168
+ exitCode: EXIT_BLOCKED,
169
+ haltReason: prResult.haltReason,
170
+ message: prResult.message,
171
+ iterations: iteration,
172
+ queueCount: queue.length,
173
+ grantId,
174
+ progressPath,
175
+ };
176
+ }
177
+ // CLEAN / MERGED with empty queue → done
178
+ if (queue.length === 0) {
179
+ const message = `directive:finish-loop done after PR #${options.prNumber}: ${prResult.haltReason}`;
180
+ progress("halt", iteration, prResult.haltReason, message, {
181
+ grantId,
182
+ queueCount: 0,
183
+ exitCode: EXIT_OK,
184
+ prNumber: options.prNumber,
185
+ });
186
+ return {
187
+ exitCode: EXIT_OK,
188
+ haltReason: prResult.haltReason,
189
+ message,
190
+ iterations: iteration,
191
+ queueCount: 0,
192
+ grantId,
193
+ progressPath,
194
+ };
195
+ }
196
+ }
197
+ // Agent-owned implement step when queue non-empty
198
+ if (queue.length > 0) {
199
+ const top = queue[0];
200
+ const message = `AGENT_STEP directive:finish-loop iteration=${iteration}: ` +
201
+ `${queue.length} scope item(s) remain (next: ${top?.name ?? "?"}). ` +
202
+ "Implementation is agent-orchestrated: promote/activate story, implement, " +
203
+ "open PR, then re-run `task directive:finish-loop` / `task pr:finish-loop -- <N>`. " +
204
+ "Gates already verified finish-loop grant for edit/push/pr/merge.";
205
+ progress("implement", iteration, "agent-implement", message, {
206
+ grantId,
207
+ queueCount: queue.length,
208
+ exitCode: EXIT_ACTION_REQUIRED,
209
+ extra: { nextPath: top?.path },
210
+ });
211
+ return {
212
+ exitCode: EXIT_ACTION_REQUIRED,
213
+ haltReason: "agent-implement",
214
+ message,
215
+ iterations: iteration,
216
+ queueCount: queue.length,
217
+ grantId,
218
+ progressPath,
219
+ };
220
+ }
221
+ }
222
+ const message = `directive:finish-loop halted: max iterations (${maxIterations}) reached ` +
223
+ `with queueCount=${lastQueueCount}.`;
224
+ progress("halt", maxIterations, "max-iterations", message, {
225
+ grantId,
226
+ queueCount: lastQueueCount,
227
+ exitCode: EXIT_BLOCKED,
228
+ });
229
+ return {
230
+ exitCode: EXIT_BLOCKED,
231
+ haltReason: "max-iterations",
232
+ message,
233
+ iterations: maxIterations,
234
+ queueCount: lastQueueCount,
235
+ grantId,
236
+ progressPath,
237
+ };
238
+ }
239
+ //# sourceMappingURL=directive-finish-loop.js.map
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Finish-loop grant gate (#871): require a live human-origin grant covering
3
+ * edit/push/pr/merge (finish-loop template ops), or DEFT_ALLOW_FINISH_LOOP=1.
4
+ *
5
+ * Agent-authored grants never satisfy. No second mint path.
6
+ */
7
+ import type { AuthzOperation, HumanOriginGrant } from "../authz/types.js";
8
+ import type { FinishLoopGrantGateResult } from "./types.js";
9
+ export type EnvMap = Readonly<Record<string, string | undefined>>;
10
+ /**
11
+ * True when grant.operations covers every finish-loop op (edit, push, pr, merge).
12
+ * A single grant may cover the full set; partial coverage reports missing ops.
13
+ */
14
+ export declare function grantCoversFinishLoopOps(grant: HumanOriginGrant): {
15
+ covered: boolean;
16
+ missing: AuthzOperation[];
17
+ };
18
+ export interface EvaluateFinishLoopGrantInput {
19
+ readonly projectRoot: string;
20
+ readonly grants?: readonly HumanOriginGrant[];
21
+ readonly env?: EnvMap;
22
+ readonly now?: Date;
23
+ /** When set, require grant to cover at least this op (default: full finish-loop set). */
24
+ readonly requireFullSet?: boolean;
25
+ readonly op?: AuthzOperation;
26
+ }
27
+ /**
28
+ * Fail-closed gate for finish-loop / pr:finish-loop entry.
29
+ */
30
+ export declare function evaluateFinishLoopGrant(input: EvaluateFinishLoopGrantInput): FinishLoopGrantGateResult;
31
+ //# sourceMappingURL=grant-gate.d.ts.map
@@ -0,0 +1,169 @@
1
+ /**
2
+ * Finish-loop grant gate (#871): require a live human-origin grant covering
3
+ * edit/push/pr/merge (finish-loop template ops), or DEFT_ALLOW_FINISH_LOOP=1.
4
+ *
5
+ * Agent-authored grants never satisfy. No second mint path.
6
+ */
7
+ import { isHumanOriginGrant, isRejectedOriginKind } from "../authz/origin.js";
8
+ import { listActiveHumanGrants, listGrants, loadAuthzState } from "../authz/store.js";
9
+ import { FINISH_LOOP_OPERATIONS, FINISH_LOOP_TEMPLATE_NAME } from "../authz/templates.js";
10
+ const ENV_BYPASS = "DEFT_ALLOW_FINISH_LOOP";
11
+ function envTruthy(value) {
12
+ if (value === undefined)
13
+ return false;
14
+ const v = value.trim().toLowerCase();
15
+ return v === "1" || v === "true" || v === "yes";
16
+ }
17
+ function grantLive(grant, now) {
18
+ if (!isHumanOriginGrant(grant)) {
19
+ const kind = grant.origin.kind;
20
+ if (isRejectedOriginKind(kind)) {
21
+ return {
22
+ ok: false,
23
+ code: "authz-grant-origin-reject",
24
+ reason: `Directive denied finish-loop: grant ${grant.id} origin.kind=${kind} is ` +
25
+ "agent/self-authored and cannot authorize walk-away ops. " +
26
+ `Human action required: \`deft authz:grant -- --template ${FINISH_LOOP_TEMPLATE_NAME}\`.`,
27
+ };
28
+ }
29
+ return {
30
+ ok: false,
31
+ code: "authz-grant-origin-reject",
32
+ reason: `Directive denied finish-loop: grant ${grant.id} lacks human-origin provenance. ` +
33
+ `Human action required: \`deft authz:grant -- --template ${FINISH_LOOP_TEMPLATE_NAME}\`.`,
34
+ };
35
+ }
36
+ if (grant.semantics.revokedAt !== null) {
37
+ return {
38
+ ok: false,
39
+ code: "authz-grant-revoked",
40
+ reason: `Directive denied finish-loop: grant ${grant.id} was revoked at ${grant.semantics.revokedAt}.`,
41
+ };
42
+ }
43
+ if (grant.semantics.singleUse && grant.semantics.usedAt !== null) {
44
+ return {
45
+ ok: false,
46
+ code: "authz-grant-single-use-spent",
47
+ reason: `Directive denied finish-loop: single-use grant ${grant.id} already spent.`,
48
+ };
49
+ }
50
+ if (grant.semantics.expiresAt !== null) {
51
+ const exp = Date.parse(grant.semantics.expiresAt);
52
+ if (Number.isNaN(exp) || exp <= now.getTime()) {
53
+ return {
54
+ ok: false,
55
+ code: "authz-grant-expired",
56
+ reason: `Directive denied finish-loop: grant ${grant.id} expired at ${grant.semantics.expiresAt}. ` +
57
+ `Human action required: mint a fresh grant via \`deft authz:grant -- --template ${FINISH_LOOP_TEMPLATE_NAME}\`.`,
58
+ };
59
+ }
60
+ }
61
+ return { ok: true, code: "finish-loop-allow", reason: "ok" };
62
+ }
63
+ /**
64
+ * True when grant.operations covers every finish-loop op (edit, push, pr, merge).
65
+ * A single grant may cover the full set; partial coverage reports missing ops.
66
+ */
67
+ export function grantCoversFinishLoopOps(grant) {
68
+ const have = new Set(grant.scope.operations.map((o) => o.toLowerCase()));
69
+ const missing = FINISH_LOOP_OPERATIONS.filter((op) => !have.has(op.toLowerCase()));
70
+ return { covered: missing.length === 0, missing: [...missing] };
71
+ }
72
+ /**
73
+ * Fail-closed gate for finish-loop / pr:finish-loop entry.
74
+ */
75
+ export function evaluateFinishLoopGrant(input) {
76
+ const env = input.env ?? process.env;
77
+ if (envTruthy(env[ENV_BYPASS])) {
78
+ return {
79
+ allowed: true,
80
+ code: "finish-loop-env-bypass",
81
+ reason: `Directive allowed finish-loop via ephemeral env bypass ${ENV_BYPASS}=1.`,
82
+ grantId: null,
83
+ missingOps: [],
84
+ };
85
+ }
86
+ const now = input.now ?? new Date();
87
+ const state = loadAuthzState(input.projectRoot);
88
+ // Prefer active grants; if none, still inspect disk grants so expiry/origin
89
+ // denials surface as grant-expired / origin-reject rather than silent missing.
90
+ let grants;
91
+ if (input.grants !== undefined) {
92
+ grants = input.grants;
93
+ }
94
+ else {
95
+ const active = listActiveHumanGrants(input.projectRoot, state, now);
96
+ grants = active.length > 0 ? active : listGrants(input.projectRoot);
97
+ }
98
+ let lastDeny = null;
99
+ for (const grant of grants) {
100
+ const live = grantLive(grant, now);
101
+ if (!live.ok) {
102
+ lastDeny = {
103
+ allowed: false,
104
+ code: live.code,
105
+ reason: live.reason,
106
+ grantId: grant.id,
107
+ missingOps: [],
108
+ };
109
+ continue;
110
+ }
111
+ if (input.op !== undefined) {
112
+ const have = grant.scope.operations.map((o) => o.toLowerCase());
113
+ if (!have.includes(input.op.toLowerCase())) {
114
+ lastDeny = {
115
+ allowed: false,
116
+ code: "authz-grant-scope-deny",
117
+ reason: `Directive denied finish-loop: grant ${grant.id} ops=[${grant.scope.operations.join(",")}] ` +
118
+ `does not cover '${input.op}'. Human action required: ` +
119
+ `\`deft authz:grant -- --template ${FINISH_LOOP_TEMPLATE_NAME}\`.`,
120
+ grantId: grant.id,
121
+ missingOps: [input.op],
122
+ };
123
+ continue;
124
+ }
125
+ return {
126
+ allowed: true,
127
+ code: "finish-loop-allow",
128
+ reason: `Directive allowed finish-loop op '${input.op}' via human-origin grant ${grant.id}.`,
129
+ grantId: grant.id,
130
+ missingOps: [],
131
+ };
132
+ }
133
+ const cover = grantCoversFinishLoopOps(grant);
134
+ if (!cover.covered && input.requireFullSet !== false) {
135
+ lastDeny = {
136
+ allowed: false,
137
+ code: "authz-grant-scope-deny",
138
+ reason: `Directive denied finish-loop: grant ${grant.id} missing ops ` +
139
+ `[${cover.missing.join(",")}]. Finish-loop requires ` +
140
+ `[${FINISH_LOOP_OPERATIONS.join(",")}]. Human action required: ` +
141
+ `\`deft authz:grant -- --template ${FINISH_LOOP_TEMPLATE_NAME}\`.`,
142
+ grantId: grant.id,
143
+ missingOps: cover.missing,
144
+ };
145
+ continue;
146
+ }
147
+ return {
148
+ allowed: true,
149
+ code: "finish-loop-allow",
150
+ reason: `Directive allowed finish-loop via human-origin grant ${grant.id}.`,
151
+ grantId: grant.id,
152
+ missingOps: [],
153
+ };
154
+ }
155
+ if (lastDeny !== null)
156
+ return lastDeny;
157
+ return {
158
+ allowed: false,
159
+ code: "authz-grant-missing",
160
+ reason: "Directive denied finish-loop: no human-origin grant covers edit/push/pr/merge " +
161
+ `and ${ENV_BYPASS} is not set. Human action required: ` +
162
+ `\`deft authz:grant -- --template ${FINISH_LOOP_TEMPLATE_NAME}\` ` +
163
+ `(or set ${ENV_BYPASS}=1 for a single-shell ephemeral bypass). ` +
164
+ "Release-class verbs are NOT authorized by this template.",
165
+ grantId: null,
166
+ missingOps: [...FINISH_LOOP_OPERATIONS],
167
+ };
168
+ }
169
+ //# sourceMappingURL=grant-gate.js.map
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Walk-away finish-loop product (#871 Wave 5 / #2948).
3
+ */
4
+ export * from "./directive-finish-loop.js";
5
+ export * from "./grant-gate.js";
6
+ export * from "./main.js";
7
+ export * from "./pr-finish-loop.js";
8
+ export * from "./progress.js";
9
+ export * from "./queue.js";
10
+ export * from "./types.js";
11
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Walk-away finish-loop product (#871 Wave 5 / #2948).
3
+ */
4
+ export * from "./directive-finish-loop.js";
5
+ export * from "./grant-gate.js";
6
+ export * from "./main.js";
7
+ export * from "./pr-finish-loop.js";
8
+ export * from "./progress.js";
9
+ export * from "./queue.js";
10
+ export * from "./types.js";
11
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,35 @@
1
+ /**
2
+ * CLI entry for pr:finish-loop and directive:finish-loop (#871).
3
+ */
4
+ export interface ParsedPrFinishArgs {
5
+ readonly prNumber: number | null;
6
+ readonly repo: string | null;
7
+ readonly projectRoot: string;
8
+ readonly maxWaitMinutes: number | null;
9
+ readonly pollSeconds: number | null;
10
+ readonly oneShot: boolean;
11
+ readonly merge: boolean;
12
+ readonly emitJson: boolean;
13
+ readonly help: boolean;
14
+ readonly error?: string;
15
+ }
16
+ export interface ParsedDirectiveFinishArgs {
17
+ readonly prNumber: number | null;
18
+ readonly repo: string | null;
19
+ readonly projectRoot: string;
20
+ readonly maxIterations: number | null;
21
+ readonly maxWaitMinutes: number | null;
22
+ readonly pollSeconds: number | null;
23
+ readonly oneShot: boolean;
24
+ readonly merge: boolean;
25
+ readonly emitJson: boolean;
26
+ readonly help: boolean;
27
+ readonly error?: string;
28
+ }
29
+ export declare function parsePrFinishArgs(argv: readonly string[]): ParsedPrFinishArgs;
30
+ export declare function parseDirectiveFinishArgs(argv: readonly string[]): ParsedDirectiveFinishArgs;
31
+ export declare function formatPrFinishHelp(): string;
32
+ export declare function formatDirectiveFinishHelp(): string;
33
+ export declare function cmdPrFinishLoop(argv?: readonly string[]): number;
34
+ export declare function cmdDirectiveFinishLoop(argv?: readonly string[]): number;
35
+ //# sourceMappingURL=main.d.ts.map