@deftai/directive 0.104.0 → 0.105.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.
@@ -0,0 +1,34 @@
1
+ /**
2
+ * CLI for `scm:sync-default` (#3391).
3
+ *
4
+ * Opens dest-targeted default-branch sync PRs using the shared detector and
5
+ * syncMaxFiles. Over-limit legs are new branches and new PRs.
6
+ */
7
+ import { planSyncDefault, type SyncDefaultForge, type SyncDefaultOpenPull } from "@deftai/directive-core/policy";
8
+ export declare const USAGE: string;
9
+ export interface SyncDefaultCliArgs {
10
+ readonly dryRun?: boolean;
11
+ readonly json?: boolean;
12
+ readonly help?: boolean;
13
+ readonly maxFiles?: number;
14
+ readonly projectRoot?: string;
15
+ readonly repo?: string;
16
+ }
17
+ export declare function parseSyncDefaultArgs(argv: readonly string[]): {
18
+ args: SyncDefaultCliArgs;
19
+ error: string | null;
20
+ };
21
+ export declare function pullsFromRestJson(payload: unknown): readonly SyncDefaultOpenPull[];
22
+ export declare function createGhSyncDefaultForge(): SyncDefaultForge;
23
+ export declare function resolveRepoFromGit(projectRoot: string): string | null;
24
+ export declare function runSyncDefaultCli(args: SyncDefaultCliArgs, options?: {
25
+ writeOut?: (s: string) => void;
26
+ writeErr?: (s: string) => void;
27
+ forge?: SyncDefaultForge;
28
+ cwd?: string;
29
+ runGit?: Parameters<typeof planSyncDefault>[0]["runGit"];
30
+ resolveRepo?: (projectRoot: string) => string | null;
31
+ }): number;
32
+ export declare function mainEntry(argv?: string[]): number;
33
+ export declare function main(argv?: string[]): number;
34
+ //# sourceMappingURL=scm-sync-default.d.ts.map
@@ -0,0 +1,210 @@
1
+ /**
2
+ * CLI for `scm:sync-default` (#3391).
3
+ *
4
+ * Opens dest-targeted default-branch sync PRs using the shared detector and
5
+ * syncMaxFiles. Over-limit legs are new branches and new PRs.
6
+ */
7
+ import { writeFileSync } from "node:fs";
8
+ import { tmpdir } from "node:os";
9
+ import { join } from "node:path";
10
+ import { fileURLToPath } from "node:url";
11
+ import { applySyncDefault, formatSyncDefaultHuman, parseGithubOwnerRepo, planSyncDefault, SYNC_DEFAULT_VERB, } from "@deftai/directive-core/policy";
12
+ import { runGhApi, splitRepo } from "@deftai/directive-core/scm";
13
+ import { defaultGitRunner } from "@deftai/directive-core/session";
14
+ export const USAGE = `usage: deft ${SYNC_DEFAULT_VERB} [--dry-run] [--json] [--max-files N] [--repo OWNER/REPO]\n` +
15
+ " Open dest-targeted sync PRs under syncMaxFiles. Under the limit: one new PR\n" +
16
+ " from source tip to dest. Over: merge-commit cuts; each dest-based leg is a\n" +
17
+ " new branch and a new PR. After a leg merges, run again. Never retarget or\n" +
18
+ " reuse an oversized PR. Each leg must be new when the reviewer first sees it.\n" +
19
+ " Required checks stay on except the Wave 1 core-guard sync exemption.\n";
20
+ export function parseSyncDefaultArgs(argv) {
21
+ const out = {};
22
+ for (let i = 0; i < argv.length; i += 1) {
23
+ const arg = argv[i];
24
+ if (arg === "--dry-run") {
25
+ out.dryRun = true;
26
+ }
27
+ else if (arg === "--json") {
28
+ out.json = true;
29
+ }
30
+ else if (arg === "--help" || arg === "-h") {
31
+ out.help = true;
32
+ }
33
+ else if (arg === "--max-files") {
34
+ const raw = argv[++i];
35
+ const parsed = raw === undefined ? Number.NaN : Number(raw);
36
+ if (!Number.isInteger(parsed) || parsed < 0) {
37
+ return { args: out, error: `--max-files expects a non-negative integer, got ${raw ?? ""}` };
38
+ }
39
+ out.maxFiles = parsed;
40
+ }
41
+ else if (arg === "--project-root") {
42
+ const value = argv[++i];
43
+ if (value === undefined) {
44
+ return { args: out, error: "--project-root expects a path" };
45
+ }
46
+ out.projectRoot = value;
47
+ }
48
+ else if (arg === "--repo") {
49
+ const value = argv[++i];
50
+ if (value === undefined) {
51
+ return { args: out, error: "--repo expects OWNER/REPO" };
52
+ }
53
+ out.repo = value;
54
+ }
55
+ else if (arg.startsWith("-")) {
56
+ return { args: out, error: `unknown flag ${JSON.stringify(arg)}` };
57
+ }
58
+ else {
59
+ return { args: out, error: `unexpected argument ${JSON.stringify(arg)}` };
60
+ }
61
+ }
62
+ return { args: out, error: null };
63
+ }
64
+ export function pullsFromRestJson(payload) {
65
+ if (!Array.isArray(payload))
66
+ return [];
67
+ const pulls = [];
68
+ for (const row of payload) {
69
+ if (typeof row !== "object" || row === null)
70
+ continue;
71
+ const rec = row;
72
+ const head = rec.head;
73
+ const baseRec = rec.base;
74
+ if (typeof head !== "object" || head === null)
75
+ continue;
76
+ if (typeof baseRec !== "object" || baseRec === null)
77
+ continue;
78
+ const headRec = head;
79
+ const baseObj = baseRec;
80
+ const number = rec.number;
81
+ const htmlUrl = rec.html_url;
82
+ const headRef = headRec.ref;
83
+ const headSha = headRec.sha;
84
+ const baseRef = baseObj.ref;
85
+ if (typeof number !== "number" ||
86
+ typeof htmlUrl !== "string" ||
87
+ typeof headRef !== "string" ||
88
+ typeof headSha !== "string" ||
89
+ typeof baseRef !== "string") {
90
+ continue;
91
+ }
92
+ pulls.push({ number, htmlUrl, headRef, headSha, baseRef });
93
+ }
94
+ return pulls;
95
+ }
96
+ export function createGhSyncDefaultForge() {
97
+ return {
98
+ listOpenPulls(repo, base) {
99
+ const [owner, name] = splitRepo(repo);
100
+ const endpoint = `repos/${owner}/${name}/pulls`;
101
+ const result = runGhApi([
102
+ endpoint,
103
+ "--method",
104
+ "GET",
105
+ "--raw-field",
106
+ `base=${base}`,
107
+ "--raw-field",
108
+ "state=open",
109
+ "--raw-field",
110
+ "per_page=100",
111
+ ]);
112
+ if (result.returncode !== 0) {
113
+ throw new Error(result.stderr || `failed to list open pulls for ${repo}`);
114
+ }
115
+ return pullsFromRestJson(JSON.parse(result.stdout || "[]"));
116
+ },
117
+ createPull(repo, input) {
118
+ const [owner, name] = splitRepo(repo);
119
+ const endpoint = `repos/${owner}/${name}/pulls`;
120
+ const bodyPath = join(tmpdir(), `deft-sync-default-${Date.now()}.json`);
121
+ writeFileSync(bodyPath, JSON.stringify({
122
+ title: input.title,
123
+ head: input.head,
124
+ base: input.base,
125
+ body: input.body,
126
+ }), { encoding: "utf8" });
127
+ const result = runGhApi(["-X", "POST", endpoint, "--input", bodyPath]);
128
+ if (result.returncode !== 0) {
129
+ throw new Error(result.stderr || `failed to create pull for ${repo}`);
130
+ }
131
+ const created = JSON.parse(result.stdout);
132
+ if (typeof created.number !== "number" || typeof created.html_url !== "string") {
133
+ throw new Error("create pull returned no number/html_url");
134
+ }
135
+ return { number: created.number, htmlUrl: created.html_url };
136
+ },
137
+ };
138
+ }
139
+ export function resolveRepoFromGit(projectRoot) {
140
+ const remote = defaultGitRunner(projectRoot, ["remote", "get-url", "origin"]);
141
+ if (remote.code !== 0)
142
+ return null;
143
+ return parseGithubOwnerRepo(remote.stdout);
144
+ }
145
+ export function runSyncDefaultCli(args, options = {}) {
146
+ const writeOut = options.writeOut ?? ((s) => process.stdout.write(s));
147
+ const writeErr = options.writeErr ?? ((s) => process.stderr.write(s));
148
+ if (args.help === true) {
149
+ writeOut(USAGE);
150
+ return 0;
151
+ }
152
+ const projectRoot = args.projectRoot ?? options.cwd ?? process.cwd();
153
+ const plan = planSyncDefault({
154
+ projectRoot,
155
+ maxFiles: args.maxFiles,
156
+ runGit: options.runGit,
157
+ });
158
+ if (plan.action === "noop") {
159
+ if (args.json === true) {
160
+ writeOut(`${JSON.stringify({ ...plan, opened: [], retargeted: false }, null, 2)}\n`);
161
+ }
162
+ else {
163
+ writeOut(`${plan.message}\n`);
164
+ }
165
+ return plan.noopReason === "fetch-failed" || plan.noopReason === "diff-failed" ? 2 : 0;
166
+ }
167
+ const resolveRepo = options.resolveRepo ?? resolveRepoFromGit;
168
+ const repo = args.repo ?? resolveRepo(projectRoot);
169
+ if (repo === null && args.dryRun !== true) {
170
+ writeErr("scm:sync-default: could not resolve OWNER/REPO from origin; pass --repo\n");
171
+ return 2;
172
+ }
173
+ try {
174
+ const result = applySyncDefault({
175
+ projectRoot,
176
+ repo: repo ?? "owner/repo",
177
+ plan,
178
+ dryRun: args.dryRun === true,
179
+ runGit: options.runGit,
180
+ forge: args.dryRun === true ? undefined : (options.forge ?? createGhSyncDefaultForge()),
181
+ });
182
+ if (args.json === true) {
183
+ writeOut(`${JSON.stringify(result, null, 2)}\n`);
184
+ }
185
+ else {
186
+ writeOut(formatSyncDefaultHuman(result));
187
+ }
188
+ return 0;
189
+ }
190
+ catch (err) {
191
+ const message = err instanceof Error ? err.message : String(err);
192
+ writeErr(`scm:sync-default: ${message}\n`);
193
+ return 1;
194
+ }
195
+ }
196
+ export function mainEntry(argv = process.argv.slice(2)) {
197
+ const { args, error } = parseSyncDefaultArgs(argv);
198
+ if (error !== null) {
199
+ process.stderr.write(`error: ${error}\n${USAGE}`);
200
+ return 2;
201
+ }
202
+ return runSyncDefaultCli(args);
203
+ }
204
+ export function main(argv = process.argv.slice(2)) {
205
+ return mainEntry(argv);
206
+ }
207
+ if (process.argv[1] !== undefined && fileURLToPath(import.meta.url) === process.argv[1]) {
208
+ process.exit(mainEntry());
209
+ }
210
+ //# sourceMappingURL=scm-sync-default.js.map
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import { type HumanPresenceMintSeams } from "./human-presence-mint.js";
2
3
  /** True when `candidate` is the same as or a descendant of `root` after realpath. */
3
4
  export declare function isPathInsideRoot(root: string, candidate: string): boolean;
4
5
  export interface ParsedArgs {
@@ -9,6 +10,10 @@ export interface ParsedArgs {
9
10
  /** Optional override for recorded xbriefRelPath (defaults: pending→active map). */
10
11
  xbriefRelPath: string;
11
12
  quiet: boolean;
13
+ /** Explicit operator confirm for the #3110 human-presence mint (#3384). */
14
+ confirm: boolean;
15
+ /** Optional owner/name seed for preimage approvedRepos (#3385 R5). */
16
+ repo: string;
12
17
  error?: string;
13
18
  }
14
19
  /**
@@ -18,5 +23,5 @@ export interface ParsedArgs {
18
23
  */
19
24
  export declare function resolveApprovalXbriefRelPath(sourceRelOrAbs: string, projectRoot: string, override?: string): string | null;
20
25
  export declare function parseArgs(argv: string[]): ParsedArgs;
21
- export declare function run(argv: string[]): number;
26
+ export declare function run(argv: string[], seams?: HumanPresenceMintSeams): number;
22
27
  //# sourceMappingURL=scope-record-approved-scope.d.ts.map
@@ -9,8 +9,9 @@
9
9
  */
10
10
  import { existsSync, lstatSync, readFileSync, realpathSync } from "node:fs";
11
11
  import { basename, join, relative, resolve, sep } from "node:path";
12
- import { scopeProvenance } from "@deftai/directive-core";
12
+ import { scopeProvenance, slice } from "@deftai/directive-core";
13
13
  import { isDirectEntrypoint } from "./entrypoint.js";
14
+ import { refuseMintWhileUatActive, refuseNonInteractiveMint, resolveHumanPresenceMintSeams, } from "./human-presence-mint.js";
14
15
  /** True when `candidate` is the same as or a descendant of `root` after realpath. */
15
16
  export function isPathInsideRoot(root, candidate) {
16
17
  let rootReal;
@@ -37,13 +38,13 @@ export function isPathInsideRoot(root, candidate) {
37
38
  return false;
38
39
  return !rel.startsWith("..");
39
40
  }
40
- const { buildApprovedScopeRecord, isHumanApprovalStamp, writeApprovedScopeRecord } = scopeProvenance;
41
+ const { isHumanApprovalStamp, mintApprovedScopeArtifacts } = scopeProvenance;
41
42
  function usage() {
42
- return ("usage: scope:record-approved-scope -- <xbrief-path> --actor <name> " +
43
- "[--kind operator] [--project-root <dir>] [--xbrief-rel-path <posix>] [--quiet]\n" +
44
- " Writes .deft/approved-scope/<plan-id>.json with a humanApproval stamp (#3205).\n" +
45
- " Agent-shaped stamps are refused. Path-binds to xbrief/active/ by default when " +
46
- "the source is under pending/ or active/.");
43
+ return ("usage: scope:record-approved-scope -- <xbrief-path> --actor <name> --confirm " +
44
+ "[--kind operator] [--project-root <dir>] [--xbrief-rel-path <posix>] [--repo owner/name] [--quiet]\n" +
45
+ " Writes .deft/approved-scope/<plan-id>.json and <plan-id>.intent.json (#3205 / #3384 / #3385).\n" +
46
+ " --actor is display only and never authorizes mint. Mint requires a real TTY, " +
47
+ "controlling terminal, --confirm, and typed phrase mint (#3110). Agent/CI shells refuse.");
47
48
  }
48
49
  /**
49
50
  * Map pending/ → active/ for path-bound approval records.
@@ -96,6 +97,8 @@ export function parseArgs(argv) {
96
97
  kind: "operator",
97
98
  xbriefRelPath: "",
98
99
  quiet: false,
100
+ confirm: false,
101
+ repo: "",
99
102
  };
100
103
  const positionals = [];
101
104
  for (let i = 0; i < argv.length; i += 1) {
@@ -106,6 +109,9 @@ export function parseArgs(argv) {
106
109
  if (arg === "--quiet") {
107
110
  parsed.quiet = true;
108
111
  }
112
+ else if (arg === "--confirm") {
113
+ parsed.confirm = true;
114
+ }
109
115
  else if (arg === "--project-root") {
110
116
  const value = argv[i + 1];
111
117
  if (value === undefined) {
@@ -150,6 +156,17 @@ export function parseArgs(argv) {
150
156
  else if (arg?.startsWith("--xbrief-rel-path=")) {
151
157
  parsed.xbriefRelPath = arg.slice("--xbrief-rel-path=".length);
152
158
  }
159
+ else if (arg === "--repo") {
160
+ const value = argv[i + 1];
161
+ if (value === undefined) {
162
+ return { ...parsed, error: "argument --repo: expected one argument" };
163
+ }
164
+ parsed.repo = value;
165
+ i += 1;
166
+ }
167
+ else if (arg?.startsWith("--repo=")) {
168
+ parsed.repo = arg.slice("--repo=".length);
169
+ }
153
170
  else if (arg?.startsWith("-")) {
154
171
  return { ...parsed, error: `unrecognized argument: ${arg}` };
155
172
  }
@@ -172,7 +189,7 @@ export function parseArgs(argv) {
172
189
  }
173
190
  return parsed;
174
191
  }
175
- export function run(argv) {
192
+ export function run(argv, seams = {}) {
176
193
  const args = parseArgs(argv);
177
194
  if (args.error !== undefined) {
178
195
  process.stderr.write(`scope_record_approved_scope: ${args.error}\n`);
@@ -202,14 +219,27 @@ export function run(argv) {
202
219
  process.stderr.write(`scope_record_approved_scope: failed to read xBRIEF: ${String(err)}\n`);
203
220
  return 2;
204
221
  }
205
- let payload;
206
- try {
207
- payload = JSON.parse(raw);
208
- }
209
- catch {
210
- process.stderr.write("scope_record_approved_scope: xBRIEF is not valid JSON\n");
222
+ const parsed = scopeProvenance.parseJsonRejectingDuplicateKeys(raw);
223
+ if (!parsed.ok) {
224
+ process.stderr.write(`scope_record_approved_scope: ${parsed.error}\n`);
211
225
  return 2;
212
226
  }
227
+ const payload = parsed.value;
228
+ const verb = "scope:record-approved-scope";
229
+ const uatBlocked = refuseMintWhileUatActive(verb, projectRoot);
230
+ if (uatBlocked !== null)
231
+ return uatBlocked;
232
+ const resolved = resolveHumanPresenceMintSeams(seams);
233
+ const mintBlocked = refuseNonInteractiveMint({
234
+ verb,
235
+ confirm: args.confirm,
236
+ isTty: resolved.isTty,
237
+ environ: resolved.environ,
238
+ hasControllingTerminal: resolved.hasControllingTerminal,
239
+ readInteractiveConfirm: resolved.readInteractiveConfirm,
240
+ });
241
+ if (mintBlocked !== null)
242
+ return mintBlocked;
213
243
  const stamp = {
214
244
  kind: args.kind.trim(),
215
245
  actor: args.actor.trim(),
@@ -228,22 +258,40 @@ export function run(argv) {
228
258
  "(repo-relative path required for approval binding) (#3205).\n");
229
259
  return 2;
230
260
  }
231
- const record = buildApprovedScopeRecord({
232
- xbriefRelPath,
233
- payload,
234
- humanApproval: stamp,
235
- xbriefRawText: raw,
236
- });
237
- const outPath = writeApprovedScopeRecord(projectRoot, record);
261
+ const repoSeed = slice.resolveProjectRepo(args.repo.trim().length > 0 ? args.repo.trim() : undefined, projectRoot);
262
+ // Mint writes record + preimage as one fail-closed pair. A dest-write
263
+ // failure restores the prior pair or leaves neither dest (#3385 residual).
264
+ let minted;
265
+ try {
266
+ minted = mintApprovedScopeArtifacts({
267
+ xbriefRelPath,
268
+ payload,
269
+ rawText: raw,
270
+ projectRoot,
271
+ humanApproval: stamp,
272
+ extract: {
273
+ projectRoot,
274
+ approvedReposSeed: repoSeed !== null ? [repoSeed] : [],
275
+ },
276
+ });
277
+ }
278
+ catch (err) {
279
+ process.stderr.write(`scope_record_approved_scope: ${String(err)}\n`);
280
+ return 1;
281
+ }
282
+ const { record, recordPath, intentPath } = minted;
238
283
  if (!args.quiet) {
239
- process.stdout.write(`scope_record_approved_scope: wrote ${outPath}\n` +
284
+ process.stdout.write(`scope_record_approved_scope: wrote ${recordPath}\n` +
285
+ ` preimage: ${intentPath}\n` +
240
286
  ` planId: ${record.planId}\n` +
241
287
  ` xbriefRelPath: ${record.xbriefRelPath}\n` +
242
288
  ` fileScopeDigest: ${record.fileScopeDigest}\n` +
289
+ ` intentDigest: ${record.intentDigest ?? ""}\n` +
243
290
  ` paths: ${record.fileScope.length}\n` +
244
291
  ` humanApproval: ${stamp.kind}/${stamp.actor}\n` +
245
- " Next: commit this file on the merge base (or a prior PR) before " +
246
- "activation/expansion in the implementation change set (#3205).\n");
292
+ " Read the preimage before you commit. That file is the approved intent (#3385).\n" +
293
+ " Next: commit record + preimage on the merge base (or a prior PR) before " +
294
+ "activation/expansion in the implementation change set (#3205 / #3385).\n");
247
295
  }
248
296
  return 0;
249
297
  }
@@ -32,6 +32,10 @@ export interface ParsedSessionStartArgs {
32
32
  * the default; also settable via DEFT_SESSION_COMPACT=1.
33
33
  */
34
34
  compact: boolean;
35
+ /** #3433: steal the worktree occupancy lease. */
36
+ steal: boolean;
37
+ confirm: boolean;
38
+ occupant: string | null;
35
39
  error?: string;
36
40
  }
37
41
  /** Parse session:start CLI args, mirroring scripts/session_start.py. */
@@ -31,6 +31,9 @@ export function parseArgs(argv) {
31
31
  ceremonyDepthOverride: null,
32
32
  effortBudgetHost: {},
33
33
  compact: false,
34
+ steal: false,
35
+ confirm: false,
36
+ occupant: null,
34
37
  };
35
38
  const dialInputs = {};
36
39
  for (let i = 0; i < argv.length; i += 1) {
@@ -273,6 +276,23 @@ export function parseArgs(argv) {
273
276
  // #3286: terse machine orientation output (opt-in; verbose remains default)
274
277
  parsed.compact = true;
275
278
  }
279
+ else if (arg === "--steal") {
280
+ parsed.steal = true;
281
+ }
282
+ else if (arg === "--confirm") {
283
+ parsed.confirm = true;
284
+ }
285
+ else if (arg === "--occupant") {
286
+ const value = argv[i + 1];
287
+ if (value === undefined) {
288
+ return { ...parsed, error: "argument --occupant: expected one argument" };
289
+ }
290
+ parsed.occupant = value;
291
+ i += 1;
292
+ }
293
+ else if (arg?.startsWith("--occupant=")) {
294
+ parsed.occupant = arg.slice("--occupant=".length);
295
+ }
276
296
  else {
277
297
  return { ...parsed, error: `unrecognized argument: ${arg}` };
278
298
  }
@@ -335,6 +355,9 @@ export function run(argv) {
335
355
  effortBudgetSeams: { hostDescriptor, environ: process.env },
336
356
  // #3286: --compact flag; DEFT_SESSION_COMPACT still resolved inside core.
337
357
  compact: args.compact ? true : undefined,
358
+ steal: args.steal ? true : undefined,
359
+ confirm: args.confirm ? true : undefined,
360
+ occupant: args.occupant ?? undefined,
338
361
  ...(args.ceremonyDepthOverride !== null
339
362
  ? {
340
363
  ceremonyDial: selectCeremonyDepth({
@@ -20,6 +20,8 @@ export type FindActiveXbriefResult = {
20
20
  readonly paths: readonly string[];
21
21
  readonly dir: string;
22
22
  };
23
+ /** Three-state exit: pass=0, fail=1, config=2. A printed pass must not leak 201. */
24
+ export declare function clampVerifyAcExit(ok: boolean, code: number): number;
23
25
  /** Run the gate and return the process exit code. */
24
26
  export declare function run(argv: string[]): number;
25
27
  export {};
package/dist/verify-ac.js CHANGED
@@ -11,7 +11,7 @@ import { join, resolve } from "node:path";
11
11
  import { fileURLToPath } from "node:url";
12
12
  import { resolveSessionCompletedVerifyAcTarget } from "@deftai/directive-core/check";
13
13
  import { formatRejectedLedger, resolveLiteralAcceptanceDetailed, } from "@deftai/directive-core/literal-acceptance";
14
- import { emitVerifyAcTerminalOutcome, evaluateVerifyAcFromPath, readPlanAcceptance, } from "@deftai/directive-core/product-first-done-gate";
14
+ import { emitVerifyAcTerminalOutcome, evaluateVerifyAcFromPath, readPlanAcceptance, resolveAcceptanceGateProfile, } from "@deftai/directive-core/product-first-done-gate";
15
15
  /** Parse verify:ac CLI args. */
16
16
  export function parseArgs(argv) {
17
17
  const parsed = {
@@ -103,9 +103,20 @@ function listActiveXbriefs(projectRoot) {
103
103
  dir: dirs.join(" + "),
104
104
  };
105
105
  }
106
+ /** Three-state exit: pass=0, fail=1, config=2. A printed pass must not leak 201. */
107
+ export function clampVerifyAcExit(ok, code) {
108
+ if (ok)
109
+ return 0;
110
+ if (code === 2)
111
+ return 2;
112
+ return 1;
113
+ }
106
114
  /** Evaluate one or many xBRIEF paths; return worst non-zero code (fail closed). */
107
115
  function evaluatePaths(paths, options) {
108
116
  let worst = 0;
117
+ // One shared option profile per reader (#3497) — scope:complete resolves its own
118
+ // profile from the same table, so the two readers cannot drift apart again.
119
+ const profile = resolveAcceptanceGateProfile(options.softMissingXbrief ? "check" : "standalone");
109
120
  for (const path of paths) {
110
121
  if (!options.quiet && paths.length > 1) {
111
122
  process.stdout.write(`verify:ac — evaluating ${path}\n`);
@@ -114,7 +125,9 @@ function evaluatePaths(paths, options) {
114
125
  projectRoot: options.projectRoot,
115
126
  quiet: options.quiet,
116
127
  softMissingXbrief: options.softMissingXbrief,
117
- checkIntegrated: options.softMissingXbrief,
128
+ checkIntegrated: profile.checkIntegrated,
129
+ captureFromNarratives: profile.captureFromNarratives,
130
+ reuseMode: profile.reuseMode,
118
131
  env: process.env,
119
132
  });
120
133
  if (result.message.length > 0) {
@@ -125,9 +138,10 @@ function evaluatePaths(paths, options) {
125
138
  process.stderr.write(`${result.message}\n`);
126
139
  }
127
140
  }
128
- if (result.code !== 0 && (worst === 0 || result.code > worst)) {
141
+ const code = clampVerifyAcExit(result.ok, result.code);
142
+ if (code !== 0 && (worst === 0 || code > worst)) {
129
143
  // Prefer code 1 (fail) over 2 when both present — still non-zero.
130
- worst = result.code;
144
+ worst = code;
131
145
  }
132
146
  }
133
147
  return worst;
@@ -242,14 +256,24 @@ export function run(argv) {
242
256
  }
243
257
  const acceptance = readPlanAcceptance(plan);
244
258
  const resolved = resolveLiteralAcceptanceDetailed(plan, { captureFromNarratives: true });
259
+ // Executor reads plan.acceptance.commands when the #3267 ledger is empty (#3449).
260
+ const executorCommands = resolved.commands.length > 0
261
+ ? resolved.commands
262
+ : acceptance.commands.map((c) => ({
263
+ command: c.command,
264
+ source: "explicit",
265
+ sourceSpan: "plan.acceptance.commands",
266
+ cwd: c.cwd ?? null,
267
+ expectedExitCode: c.expectedExitCode ?? 0,
268
+ }));
245
269
  reports.push({
246
270
  xbrief: xbriefPath,
247
271
  source_rung: acceptance.source_rung,
248
272
  none_stated: acceptance.none_stated,
249
273
  acceptance_commands: acceptance.commands,
250
- count: resolved.commands.length,
274
+ count: executorCommands.length,
251
275
  rejected_count: resolved.rejected.length,
252
- commands: resolved.commands.map((c) => ({
276
+ commands: executorCommands.map((c) => ({
253
277
  command: c.command,
254
278
  source: c.source,
255
279
  sourceSpan: c.sourceSpan ?? null,
@@ -3,11 +3,12 @@ interface ParsedArgs {
3
3
  projectRoot: string;
4
4
  repo: string | null;
5
5
  tip: string | null;
6
+ issue: number | null;
6
7
  quiet: boolean;
7
8
  skipGh: boolean;
8
9
  error?: string;
9
10
  }
10
- /** Parse verify-completed-tracked CLI args (#3264). */
11
+ /** Parse verify-completed-tracked CLI args (#3264 / #3476). */
11
12
  export declare function parseArgs(argv: string[]): ParsedArgs;
12
13
  /** Run the gate and return the process exit code. */
13
14
  export declare function run(argv: string[]): number;
@@ -2,12 +2,21 @@
2
2
  import { resolve } from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
4
  import { evaluateCompletedTracked } from "@deftai/directive-core/lifecycle";
5
- /** Parse verify-completed-tracked CLI args (#3264). */
5
+ function parseIssueNumber(raw) {
6
+ const trimmed = raw.startsWith("#") ? raw.slice(1) : raw;
7
+ if (!/^\d+$/.test(trimmed)) {
8
+ return null;
9
+ }
10
+ const value = Number(trimmed);
11
+ return Number.isInteger(value) && value > 0 ? value : null;
12
+ }
13
+ /** Parse verify-completed-tracked CLI args (#3264 / #3476). */
6
14
  export function parseArgs(argv) {
7
15
  const parsed = {
8
16
  projectRoot: ".",
9
17
  repo: null,
10
18
  tip: null,
19
+ issue: null,
11
20
  quiet: false,
12
21
  skipGh: false,
13
22
  };
@@ -52,6 +61,26 @@ export function parseArgs(argv) {
52
61
  else if (arg?.startsWith("--tip=")) {
53
62
  parsed.tip = arg.slice("--tip=".length);
54
63
  }
64
+ else if (arg === "--issue") {
65
+ const value = argv[i + 1];
66
+ if (value === undefined) {
67
+ return { ...parsed, error: "argument --issue: expected one argument" };
68
+ }
69
+ const issue = parseIssueNumber(value);
70
+ if (issue === null) {
71
+ return { ...parsed, error: `argument --issue: expected a positive integer, got ${value}` };
72
+ }
73
+ parsed.issue = issue;
74
+ i += 1;
75
+ }
76
+ else if (arg?.startsWith("--issue=")) {
77
+ const value = arg.slice("--issue=".length);
78
+ const issue = parseIssueNumber(value);
79
+ if (issue === null) {
80
+ return { ...parsed, error: `argument --issue: expected a positive integer, got ${value}` };
81
+ }
82
+ parsed.issue = issue;
83
+ }
55
84
  else {
56
85
  return { ...parsed, error: `unrecognized argument: ${arg}` };
57
86
  }
@@ -70,6 +99,7 @@ export function run(argv) {
70
99
  quiet: args.quiet,
71
100
  repo: args.repo,
72
101
  tip: args.tip,
102
+ issue: args.issue,
73
103
  skipGh: args.skipGh,
74
104
  });
75
105
  if (result.message.length > 0) {
@@ -128,6 +128,13 @@ export function run(argv) {
128
128
  reason: r.reason,
129
129
  sourceSpan: r.sourceSpan ?? null,
130
130
  })),
131
+ // Prose-derived captures demoted by structured acceptance commands (#3484).
132
+ advisory_rejected_count: resolved.advisoryRejected.length,
133
+ advisory_rejected: resolved.advisoryRejected.map((r) => ({
134
+ command: r.command,
135
+ reason: r.reason,
136
+ sourceSpan: r.sourceSpan ?? null,
137
+ })),
131
138
  };
132
139
  process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
133
140
  if (resolved.rejected.length > 0) {
@@ -2,6 +2,7 @@
2
2
  interface ParsedArgs {
3
3
  projectRoot: string;
4
4
  repo: string | null;
5
+ issue: number | null;
5
6
  quiet: boolean;
6
7
  skipGh: boolean;
7
8
  error?: string;