@deftai/directive 0.99.0 → 0.101.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.
@@ -1,10 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
3
  * agents:refresh — rewrite AGENTS.md managed section from the canonical template (#768 / #1996).
4
+ * #3286: when the managed section is already current, print one-line
5
+ * "unchanged - sha match" (content-based; always plans against the live AGENTS.md).
4
6
  */
5
7
  import { resolve } from "node:path";
6
8
  import { fileURLToPath } from "node:url";
7
9
  import { applyAgentsRefresh } from "@deftai/directive-core/platform";
10
+ import { formatDepositShaMatchLine } from "@deftai/directive-core/session";
8
11
  export function parseAgentsRefreshArgs(argv) {
9
12
  let projectRoot = process.cwd();
10
13
  let check = false;
@@ -40,6 +43,9 @@ export function runAgentsRefresh(argv) {
40
43
  process.stderr.write(`agents:refresh: ${args.error}\n`);
41
44
  return 2;
42
45
  }
46
+ // Always plan against live AGENTS.md (#3286 Greptile P1: deposit fingerprint
47
+ // alone cannot detect local managed-section edits). Content-current → one-line
48
+ // sha-match phrasing for harness parsers.
43
49
  // The read->compute->write is serialized behind an advisory lock and written
44
50
  // atomically inside applyAgentsRefresh, so concurrent refreshers cannot clobber
45
51
  // one another's session= write or observe a partial write (#1329).
@@ -54,7 +60,8 @@ export function runAgentsRefresh(argv) {
54
60
  return 1;
55
61
  }
56
62
  if (state === "current") {
57
- process.stdout.write("AGENTS.md managed section is current — no changes.\n");
63
+ // #3286: one-line no-op phrasing when managed section already matches template.
64
+ process.stdout.write(`${formatDepositShaMatchLine("agents:refresh")}\n`);
58
65
  return 0;
59
66
  }
60
67
  if (state === "template-missing" || state === "template-malformed" || state === "unreadable") {
package/dist/authz.js CHANGED
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * Authz CLI (#2944 Wave 1 + #1095 Wave 4 + #3110): human-origin grants + UAT lease +
4
- * AFK closed-verb templates (mint via mintHumanOriginGrant only).
3
+ * Authz CLI (#2944 Wave 1 + #1095 Wave 4 + #3110 + #3291): human-origin grants + UAT lease +
4
+ * AFK closed-verb templates + structural scope:decompose apply grants
5
+ * (mint via mintHumanOriginGrant / mintDecomposeStructuralApplyGrant only).
5
6
  *
6
7
  * deft authz:show
7
8
  * deft authz:uat-start -- --campaign <id> [--actor <name>] [--note <text>] [--confirm]
@@ -9,6 +10,7 @@
9
10
  * deft authz:grant -- --operations edit,push --surfaces 'src/**' --cohort <id> ... [--confirm]
10
11
  * deft authz:grant -- --template release-publish --target 0.30.0 [--confirm]
11
12
  * deft authz:grant -- --template finish-loop [--confirm]
13
+ * deft authz:grant -- --parent <parent.xbrief.json> --draft <draft.json> [--repo owner/name] [--confirm]
12
14
  * deft authz:revoke -- <grant-id> [--confirm]
13
15
  *
14
16
  * **UAT-active hard refuse (#3110):** while any UAT lease is active, ALL mutating
@@ -20,8 +22,9 @@
20
22
  * TTY + controlling terminal + `--confirm` + typed phrase `mint`; agent/CI env
21
23
  * markers refuse fail-closed. Argv `--confirm` alone is never enough.
22
24
  */
23
- import { closeSync, openSync, readSync } from "node:fs";
24
- import { AFK_TEMPLATE_NAMES, AUTHZ_OPERATIONS, CLOSED_VERB_TEMPLATE_NAMES, FINISH_LOOP_TEMPLATE_NAME, isAfkTemplateName, isClosedVerbTemplateName, isFinishLoopTemplateName, loadAuthzState, mintAfkTemplateGrant, mintHumanOriginGrant, revokeGrant, showAuthzSnapshot, startUatLease, suspendUatLease, } from "@deftai/directive-core/authz";
25
+ import { closeSync, existsSync, openSync, readSync } from "node:fs";
26
+ import { isAbsolute, resolve } from "node:path";
27
+ import { AFK_TEMPLATE_NAMES, AUTHZ_OPERATIONS, CLOSED_VERB_TEMPLATE_NAMES, FINISH_LOOP_TEMPLATE_NAME, formatDecomposeStructuralMintCommand, isAfkTemplateName, isClosedVerbTemplateName, isFinishLoopTemplateName, loadAuthzState, mintAfkTemplateGrant, mintDecomposeStructuralApplyGrant, mintHumanOriginGrant, revokeGrant, showAuthzSnapshot, startUatLease, suspendUatLease, toProjectRelativePosix, } from "@deftai/directive-core/authz";
25
28
  /**
26
29
  * Env markers that indicate an agent/host/CI shell even when stdin reports a TTY
27
30
  * (pseudo-terminal residual; #3110 Greptile). Presence refuses mutating authz.
@@ -104,6 +107,8 @@ function parseArgv(argv) {
104
107
  grantId: null,
105
108
  template: null,
106
109
  target: null,
110
+ parent: null,
111
+ draft: null,
107
112
  format: "text",
108
113
  confirm: false,
109
114
  };
@@ -224,6 +229,14 @@ function parseArgv(argv) {
224
229
  base.target = args[++i] ?? null;
225
230
  continue;
226
231
  }
232
+ if (a === "--parent") {
233
+ base.parent = args[++i] ?? null;
234
+ continue;
235
+ }
236
+ if (a === "--draft") {
237
+ base.draft = args[++i] ?? null;
238
+ continue;
239
+ }
227
240
  if (a === "--confirm") {
228
241
  base.confirm = true;
229
242
  continue;
@@ -248,6 +261,8 @@ function helpText() {
248
261
  " [--stories 2944] [--plan-ref <id>] [--repo owner/name] [--branch <b>] [--expires ISO] [--confirm]",
249
262
  " deft authz:grant -- --template release-publish --target 0.30.0 [--actor <name>] [--expires ISO] [--confirm]",
250
263
  " deft authz:grant -- --template finish-loop [--actor <name>] [--expires ISO] [--confirm]",
264
+ " deft authz:grant -- --parent <parent.xbrief.json> --draft <draft.json> \\",
265
+ " [--repo owner/name] [--expires ISO] [--single-use] [--actor <name>] [--confirm]",
251
266
  " deft authz:revoke -- <grant-id> [--confirm]",
252
267
  "",
253
268
  "Human-origin grants are minted only via this CLI (origin.kind=operator-cli).",
@@ -263,6 +278,16 @@ function helpText() {
263
278
  " End UAT only after the lease is cleared out-of-band (state edit / suspend path",
264
279
  " that does not run while the lease is active) — or suspend before re-minting.",
265
280
  "",
281
+ "Structural scope:decompose apply (#3239 / #3291):",
282
+ " Pass --parent + --draft (paths under project root). Digest is SHA-256 of exact",
283
+ " draft bytes; binds parent, target, worktree, and optional --repo. Mints only",
284
+ " scope.decompose.apply.structural via mintDecomposeStructuralApplyGrant.",
285
+ " Example (after --check validates the draft):",
286
+ " deft authz:grant -- --parent xbrief/pending/epic.xbrief.json \\",
287
+ " --draft xbrief/.triage-cache/decompositions/draft.json --confirm",
288
+ " Then: deft scope:decompose -- <parent> --draft <draft>",
289
+ " scope:decompose --check remains ungated (no structural grant required).",
290
+ "",
266
291
  `AFK templates (#1095 / #871): ${AFK_TEMPLATE_NAMES.join(", ")}`,
267
292
  ` Closed-verb (#1095): ${CLOSED_VERB_TEMPLATE_NAMES.join(", ")} — require --target`,
268
293
  ` Finish-loop (#871): ${FINISH_LOOP_TEMPLATE_NAME} — edit/push/pr/merge (no release ops)`,
@@ -487,6 +512,75 @@ export function main(argv = process.argv.slice(2), seams = {}) {
487
512
  return 0;
488
513
  }
489
514
  case "grant": {
515
+ // Structural decompose apply path (#3291): --parent + --draft → mintDecomposeStructuralApplyGrant.
516
+ const parentRaw = args.parent?.trim() ?? "";
517
+ const draftRaw = args.draft?.trim() ?? "";
518
+ const hasParent = parentRaw.length > 0;
519
+ const hasDraft = draftRaw.length > 0;
520
+ if (hasParent !== hasDraft) {
521
+ process.stderr.write("authz:grant structural mint requires both --parent <path> and --draft <path>\n" +
522
+ ` Example: ${formatDecomposeStructuralMintCommand(hasParent ? parentRaw : "xbrief/pending/parent.xbrief.json", hasDraft ? draftRaw : "xbrief/.triage-cache/decompositions/draft.json", { repo: args.repo })}\n`);
523
+ return 2;
524
+ }
525
+ if (hasParent && hasDraft) {
526
+ if (args.template !== null && args.template.trim().length > 0) {
527
+ process.stderr.write("authz:grant: --parent/--draft structural mint cannot be combined with --template\n");
528
+ return 2;
529
+ }
530
+ if (args.operations.length > 0) {
531
+ process.stderr.write("authz:grant: --parent/--draft structural mint cannot be combined with --operations " +
532
+ "(operation is fixed to scope.decompose.apply.structural)\n");
533
+ return 2;
534
+ }
535
+ const root = resolve(args.projectRoot);
536
+ const parentAbs = isAbsolute(parentRaw) ? resolve(parentRaw) : resolve(root, parentRaw);
537
+ const draftAbs = isAbsolute(draftRaw) ? resolve(draftRaw) : resolve(root, draftRaw);
538
+ const parentRel = toProjectRelativePosix(root, parentAbs);
539
+ const draftRel = toProjectRelativePosix(root, draftAbs);
540
+ if (parentRel === null) {
541
+ process.stderr.write(`authz:grant: --parent path is outside project root: ${parentRaw}\n`);
542
+ return 2;
543
+ }
544
+ if (draftRel === null) {
545
+ process.stderr.write(`authz:grant: --draft path is outside project root: ${draftRaw}\n`);
546
+ return 2;
547
+ }
548
+ if (!existsSync(parentAbs)) {
549
+ process.stderr.write(`authz:grant: --parent path does not exist: ${parentRaw}\n`);
550
+ return 2;
551
+ }
552
+ if (!existsSync(draftAbs)) {
553
+ process.stderr.write(`authz:grant: --draft path does not exist: ${draftRaw}\n`);
554
+ return 2;
555
+ }
556
+ const blocked = gateConfirm();
557
+ if (blocked !== null)
558
+ return blocked;
559
+ const grant = mintDecomposeStructuralApplyGrant({
560
+ projectRoot: root,
561
+ parentPath: parentAbs,
562
+ draftPath: draftAbs,
563
+ actor: args.actor,
564
+ repo: args.repo,
565
+ expiresAt: args.expiresAt,
566
+ singleUse: args.singleUse,
567
+ });
568
+ // mintDecomposeStructuralApplyGrant always sets contentDigest + worktree.
569
+ const digest = grant.scope.contentDigest || "";
570
+ process.stdout.write(`✓ human-origin structural grant minted id=${grant.id} origin=${grant.origin.kind}\n`);
571
+ process.stdout.write(` ops=[${grant.scope.operations.join(",")}] digest=${digest.slice(0, 12)}…\n`);
572
+ process.stdout.write(` parent=${parentRel} draft=${draftRel}\n`);
573
+ if (grant.scope.repo) {
574
+ process.stdout.write(` repo=${grant.scope.repo}\n`);
575
+ }
576
+ process.stdout.write(` worktree=${grant.scope.worktree || root}\n`);
577
+ if (grant.semantics.singleUse) {
578
+ process.stdout.write(" single-use=true (spent on first successful apply)\n");
579
+ }
580
+ process.stdout.write(" Apply: deft scope:decompose -- <parent> --draft <draft>\n" +
581
+ " Authorization SoT: Wave 1 grant store (.deft/authz/grants) — not session-auth.\n");
582
+ return 0;
583
+ }
490
584
  // AFK template path (#1095 / #871): presets only — still mintHumanOriginGrant.
491
585
  if (args.template !== null && args.template.trim().length > 0) {
492
586
  if (!isAfkTemplateName(args.template)) {
@@ -529,7 +623,8 @@ export function main(argv = process.argv.slice(2), seams = {}) {
529
623
  return 0;
530
624
  }
531
625
  if (args.operations.length === 0) {
532
- process.stderr.write("authz:grant requires --operations <edit,push,...> or --template <finish-loop|release-*> \n");
626
+ process.stderr.write("authz:grant requires --operations <edit,push,...>, --template <finish-loop|release-*>, " +
627
+ "or --parent + --draft (structural scope:decompose apply)\n");
533
628
  return 2;
534
629
  }
535
630
  {
@@ -10,7 +10,7 @@ export interface DispatchIo {
10
10
  writeErr: (text: string) => void;
11
11
  }
12
12
  /** CLI modules in packages/cli/src (excluding parity harnesses and bin/index). */
13
- export declare const CLI_MODULE_VERBS: readonly ["agents-refresh", "cache", "check", "capacity-backfill", "capacity-show", "codebase-default-extractor", "codebase-map", "codebase-map-fresh", "codebase-projection-registry", "codebase-provider", "doctor", "install-upgrade", "install-uninstall", "migrate-preflight", "migrate-xbrief", "migrate-category-b", "framework-check-updates", "hook-dispatch", "umbrella-current-shape", "changelog-check", "change-init", "commit-lint", "coverage-hotspots", "policy", "authz", "escalation-cli", "pr-closing-keywords", "pr-merge-readiness", "pr-monitor", "pr-protected-issues", "pr-wait-mergeable", "pr-watch", "pr-finish-loop", "directive-finish-loop", "preflight-cache", "preflight-gh", "probe-session", "release", "release-e2e", "release-publish", "release-rollback", "scope-lifecycle", "scope-record-approved-scope", "lifecycle-event", "lifecycle-stats", "session-start", "session-ready", "plan-sequence", "slice", "subagent-monitor", "toolchain-check", "triage-actions", "triage-bootstrap", "triage-bulk", "triage-classify", "triage-help", "triage-queue", "triage-reconcile", "triage-refresh", "triage-scope", "triage-scope-drift", "triage-smoketest", "triage-subscribe", "triage-summary", "triage-welcome", "ts-check-lane", "vbrief-activate", "vbrief-build", "vbrief-preflight", "vbrief-reconcile", "vbrief-validate", "vbrief-validation", "xbrief-create", "xbrief-verify", "verify-branch", "verify-encoding", "verify-forward-coverage", "verify-test-boundary", "verify-scope-provenance", "verify-consumer-check-contract", "verify-hooks-installed", "verify-investigation", "verify-judgment-gates", "verify-no-task-runtime", "validate-links", "validate-strategy-output", "verify-biome-config", "verify-bridge-drift", "verify-capacity", "verify-contained-writes", "verify-content-manifest", "verify-skill-external-fetch-gate", "verify-contract-drift", "verify-cursor-tier1", "verify-openclaw-tier1", "verify-go-freeze", "verify-scm-boundary", "verify-session-ritual", "verify-plan-sequence", "verify-stubs", "verify-xbrief-drift", "rule-ownership-lint", "verify-story-ready", "verify-review-monitor", "verify-l4-owner", "verify-subagent-alive", "review-monitor-register", "review-monitor-release", "verify-tools", "verify-wip-cap", "verify-orphan-active", "verify-agents-md-budget", "verify-agents-md-advisory", "verify-eval-health-relocation", "verify-eval-triggers-relocation", "eval-health", "eval-run", "eval-report", "eval-triggers"];
13
+ export declare const CLI_MODULE_VERBS: readonly ["agents-refresh", "cache", "check", "capacity-backfill", "capacity-show", "codebase-default-extractor", "codebase-map", "codebase-map-fresh", "codebase-projection-registry", "codebase-provider", "doctor", "install-upgrade", "install-uninstall", "migrate-preflight", "migrate-xbrief", "migrate-category-b", "framework-check-updates", "hook-dispatch", "umbrella-current-shape", "changelog-check", "change-init", "commit-lint", "coverage-hotspots", "policy", "authz", "escalation-cli", "pr-closing-keywords", "pr-merge-readiness", "pr-monitor", "pr-protected-issues", "pr-wait-mergeable", "pr-watch", "pr-finish-loop", "directive-finish-loop", "preflight-cache", "preflight-gh", "probe-session", "release", "release-e2e", "release-publish", "release-rollback", "scope-lifecycle", "scope-record-approved-scope", "lifecycle-event", "lifecycle-stats", "session-start", "session-ready", "plan-sequence", "slice", "subagent-monitor", "toolchain-check", "triage-actions", "triage-bootstrap", "triage-bulk", "triage-classify", "triage-help", "triage-queue", "triage-reconcile", "triage-refresh", "triage-scope", "triage-scope-drift", "triage-smoketest", "triage-subscribe", "triage-summary", "triage-welcome", "ts-check-lane", "vbrief-activate", "vbrief-build", "vbrief-preflight", "vbrief-reconcile", "vbrief-validate", "vbrief-validation", "xbrief-create", "xbrief-verify", "verify-branch", "verify-encoding", "verify-forward-coverage", "verify-test-boundary", "verify-scope-provenance", "verify-consumer-check-contract", "verify-hooks-installed", "verify-investigation", "verify-judgment-gates", "verify-no-task-runtime", "validate-links", "validate-strategy-output", "verify-biome-config", "verify-bridge-drift", "verify-capacity", "verify-contained-writes", "verify-content-manifest", "verify-skill-external-fetch-gate", "verify-contract-drift", "verify-cursor-tier1", "verify-openclaw-tier1", "verify-go-freeze", "verify-scm-boundary", "verify-session-ritual", "verify-plan-sequence", "verify-stubs", "verify-xbrief-drift", "rule-ownership-lint", "verify-story-ready", "verify-review-monitor", "verify-l4-owner", "verify-subagent-alive", "review-monitor-register", "review-monitor-release", "verify-tools", "verify-wip-cap", "verify-orphan-active", "verify-completed-tracked", "verify-literal-ac", "verify-ac", "verify-agents-md-budget", "verify-agents-md-advisory", "verify-eval-health-relocation", "verify-eval-triggers-relocation", "eval-health", "eval-run", "eval-report", "eval-triggers"];
14
14
  /** Core-only CLI entrypoints without a packages/cli wrapper. */
15
15
  export declare const CORE_MODULE_VERBS: readonly ["scm", "scm-readiness", "github-auth-modes", "github-body", "issue-emit", "issue-ingest", "issue-sync-from-xbrief", "reconcile-issues", "swarm-launch", "swarm-complete-cohort", "swarm-finalize-cohort", "swarm-pre-dispatch", "swarm-readiness", "swarm-routing-verify", "swarm-routing-set", "swarm-verify-review-clean", "swarm-worktrees", "framework-commands", "pack-render", "packs-slice", "prd-render", "export-spec", "project-render", "roadmap-render", "rule-map", "spec-render", "spec-validate", "code-structure-validate", "pack-migrate-skills", "pack-migrate-rules", "pack-migrate-strategies", "pack-migrate-patterns", "pack-migrate-swarm-spec", "policy-set", "setup-ghx", "scope-undo", "scope-demote", "scope-decompose", "changelog-resolve-unreleased", "architecture-preflight-sor", "feedback-file", "value-readback", "product-signal", "freshness-report", "decision-write", "decision-list"];
16
16
  /** Colon aliases for triage-actions (mirrors cli-router SUBCOMMAND_ROUTES). */
package/dist/dispatch.js CHANGED
@@ -137,6 +137,9 @@ export const CLI_MODULE_VERBS = [
137
137
  "verify-tools",
138
138
  "verify-wip-cap",
139
139
  "verify-orphan-active",
140
+ "verify-completed-tracked",
141
+ "verify-literal-ac",
142
+ "verify-ac",
140
143
  "verify-agents-md-budget",
141
144
  "verify-agents-md-advisory",
142
145
  "verify-eval-health-relocation",
@@ -279,6 +282,11 @@ export const VERB_ALIASES = {
279
282
  "verify:vbrief-conformance": "vbrief-validate",
280
283
  "verify:wip-cap": "verify-wip-cap",
281
284
  "verify:orphan-active": "verify-orphan-active",
285
+ "verify:completed-tracked": "verify-completed-tracked",
286
+ "verify:literal-ac": "verify-literal-ac",
287
+ "verify:literal-acceptance": "verify-literal-ac",
288
+ "verify:ac": "verify-ac",
289
+ "verify:acceptance": "verify-ac",
282
290
  "verify:agents-md-budget": "verify-agents-md-budget",
283
291
  "verify:agents-md-advisory": "verify-agents-md-advisory",
284
292
  "verify:eval-health-relocation": "verify-eval-health-relocation",
@@ -18,6 +18,20 @@ export interface ParsedSessionStartArgs {
18
18
  * Applied as resolve inputs via a one-shot config override in run().
19
19
  */
20
20
  ceremonyDepthOverride: CeremonyDepth | null;
21
+ /**
22
+ * #3266: host/CLI effort-budget cap fields for bank-the-pass guidance.
23
+ * Merged with DEFT_HOST_EFFORT_BUDGET JSON when present.
24
+ */
25
+ effortBudgetHost: {
26
+ maxTurns?: number;
27
+ maxBudget?: number;
28
+ hardBudget?: boolean;
29
+ };
30
+ /**
31
+ * #3286: compact orientation output (terse machine format). Verbose remains
32
+ * the default; also settable via DEFT_SESSION_COMPACT=1.
33
+ */
34
+ compact: boolean;
21
35
  error?: string;
22
36
  }
23
37
  /** Parse session:start CLI args, mirroring scripts/session_start.py. */
@@ -2,7 +2,7 @@
2
2
  import { resolve } from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
4
  import { CEREMONY_DEPTHS, normalizeCeremonyModelTier, normalizeCeremonyProjectShape, normalizeCeremonyTaskSize, selectCeremonyDepth, } from "@deftai/directive-core/policy";
5
- import { COLD_CEREMONY_TIER, parseDeferrals, READ_ONLY_POSTURE, REARM_CEREMONY_TIER, ritualStatePath, runSessionStart, SESSION_CEREMONY_TIERS, } from "@deftai/directive-core/session";
5
+ import { COLD_CEREMONY_TIER, parseDeferrals, READ_ONLY_POSTURE, REARM_CEREMONY_TIER, resolveProductionHostEffortDescriptor, ritualStatePath, runSessionStart, SESSION_CEREMONY_TIERS, } from "@deftai/directive-core/session";
6
6
  function parseCeremonyDepth(raw) {
7
7
  const value = raw.trim().toLowerCase();
8
8
  if (CEREMONY_DEPTHS.includes(value)) {
@@ -29,6 +29,8 @@ export function parseArgs(argv) {
29
29
  ceremonyTier: COLD_CEREMONY_TIER,
30
30
  ceremonyDialInputs: {},
31
31
  ceremonyDepthOverride: null,
32
+ effortBudgetHost: {},
33
+ compact: false,
32
34
  };
33
35
  const dialInputs = {};
34
36
  for (let i = 0; i < argv.length; i += 1) {
@@ -211,6 +213,66 @@ export function parseArgs(argv) {
211
213
  }
212
214
  parsed.ceremonyDepthOverride = depth;
213
215
  }
216
+ else if (arg === "--max-turns") {
217
+ // #3266: harness hard turn budget for bank-the-pass guidance
218
+ const value = argv[i + 1];
219
+ if (value === undefined) {
220
+ return { ...parsed, error: "argument --max-turns: expected one argument" };
221
+ }
222
+ const n = Number(value);
223
+ if (!Number.isFinite(n) || n < 0) {
224
+ return {
225
+ ...parsed,
226
+ error: `argument --max-turns: expected non-negative number, got ${JSON.stringify(value)}`,
227
+ };
228
+ }
229
+ parsed.effortBudgetHost.maxTurns = n;
230
+ i += 1;
231
+ }
232
+ else if (arg?.startsWith("--max-turns=")) {
233
+ const value = arg.slice("--max-turns=".length);
234
+ const n = Number(value);
235
+ if (!Number.isFinite(n) || n < 0) {
236
+ return {
237
+ ...parsed,
238
+ error: `argument --max-turns: expected non-negative number, got ${JSON.stringify(value)}`,
239
+ };
240
+ }
241
+ parsed.effortBudgetHost.maxTurns = n;
242
+ }
243
+ else if (arg === "--max-budget") {
244
+ const value = argv[i + 1];
245
+ if (value === undefined) {
246
+ return { ...parsed, error: "argument --max-budget: expected one argument" };
247
+ }
248
+ const n = Number(value);
249
+ if (!Number.isFinite(n) || n < 0) {
250
+ return {
251
+ ...parsed,
252
+ error: `argument --max-budget: expected non-negative number, got ${JSON.stringify(value)}`,
253
+ };
254
+ }
255
+ parsed.effortBudgetHost.maxBudget = n;
256
+ i += 1;
257
+ }
258
+ else if (arg?.startsWith("--max-budget=")) {
259
+ const value = arg.slice("--max-budget=".length);
260
+ const n = Number(value);
261
+ if (!Number.isFinite(n) || n < 0) {
262
+ return {
263
+ ...parsed,
264
+ error: `argument --max-budget: expected non-negative number, got ${JSON.stringify(value)}`,
265
+ };
266
+ }
267
+ parsed.effortBudgetHost.maxBudget = n;
268
+ }
269
+ else if (arg === "--hard-budget") {
270
+ parsed.effortBudgetHost.hardBudget = true;
271
+ }
272
+ else if (arg === "--compact") {
273
+ // #3286: terse machine orientation output (opt-in; verbose remains default)
274
+ parsed.compact = true;
275
+ }
214
276
  else {
215
277
  return { ...parsed, error: `unrecognized argument: ${arg}` };
216
278
  }
@@ -247,6 +309,19 @@ export function run(argv) {
247
309
  }
248
310
  return true;
249
311
  });
312
+ // #3266: production host adapter — native harness env + DEFT_HOST_EFFORT_BUDGET + CLI flags.
313
+ const hostDescriptor = {
314
+ ...resolveProductionHostEffortDescriptor(process.env),
315
+ };
316
+ if (args.effortBudgetHost.maxTurns !== undefined) {
317
+ hostDescriptor.maxTurns = args.effortBudgetHost.maxTurns;
318
+ }
319
+ if (args.effortBudgetHost.maxBudget !== undefined) {
320
+ hostDescriptor.maxBudget = args.effortBudgetHost.maxBudget;
321
+ }
322
+ if (args.effortBudgetHost.hardBudget === true) {
323
+ hostDescriptor.hardBudget = true;
324
+ }
250
325
  let result;
251
326
  try {
252
327
  // #3214: --ceremony-depth forces this session only (not persisted to policy).
@@ -257,6 +332,9 @@ export function run(argv) {
257
332
  allowOptionalNetwork: args.withNetwork ? true : undefined,
258
333
  ceremonyTier: args.ceremonyTier,
259
334
  ceremonyDialInputs: args.ceremonyDialInputs,
335
+ effortBudgetSeams: { hostDescriptor, environ: process.env },
336
+ // #3286: --compact flag; DEFT_SESSION_COMPACT still resolved inside core.
337
+ compact: args.compact ? true : undefined,
260
338
  ...(args.ceremonyDepthOverride !== null
261
339
  ? {
262
340
  ceremonyDial: selectCeremonyDepth({
@@ -17,6 +17,11 @@ export interface ParsedArgs {
17
17
  * (#3197). Requires --mirror; dry-run by default until --apply.
18
18
  */
19
19
  reEnrich: boolean;
20
+ /**
21
+ * Dismiss SCM label-mirror discovery tip without running a dry-run (#3124).
22
+ * Standalone production entry for recordMirrorDiscoveryAcked.
23
+ */
24
+ ackDiscovery: boolean;
20
25
  /** Raw --author value (LOGIN, @me, comma allow-list); null = no filter (#3129). */
21
26
  author: string | null;
22
27
  /** Apply batch size (rate-limit awareness). */
@@ -3,7 +3,7 @@ import { statSync } from "node:fs";
3
3
  import { resolve } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { resolveAuthorFilter, } from "@deftai/directive-core/dist/triage/author-filter.js";
6
- import { labelMirrorOutcomeToJson, listProject, mirrorLabels, renderLabelMirrorReport, validateProject, } from "@deftai/directive-core/dist/triage/classify/index.js";
6
+ import { labelMirrorOutcomeToJson, listProject, mirrorDiscoveryStateExists, mirrorLabels, recordMirrorDiscoveryAcked, recordMirrorDiscoverySuccessfulDryRun, renderLabelMirrorReport, validateProject, } from "@deftai/directive-core/dist/triage/classify/index.js";
7
7
  function parseNonNegInt(raw, flag) {
8
8
  const n = Number.parseInt(raw, 10);
9
9
  if (!Number.isFinite(n) || String(n) !== raw.trim() || n < 0) {
@@ -37,6 +37,7 @@ export function parseArgs(argv) {
37
37
  allowCrossRepo: false,
38
38
  includeClosed: false,
39
39
  reEnrich: false,
40
+ ackDiscovery: false,
40
41
  author: null,
41
42
  batchSize: null,
42
43
  delayMs: null,
@@ -68,6 +69,9 @@ export function parseArgs(argv) {
68
69
  else if (arg === "--re-enrich") {
69
70
  parsed.reEnrich = true;
70
71
  }
72
+ else if (arg === "--ack-discovery") {
73
+ parsed.ackDiscovery = true;
74
+ }
71
75
  else if (arg === "--author-mine") {
72
76
  parsed.author = "@me";
73
77
  }
@@ -179,6 +183,8 @@ export function parseArgs(argv) {
179
183
  else if (arg === "--help" || arg === "-h") {
180
184
  return parsed;
181
185
  }
186
+ else if (arg === "--") {
187
+ }
182
188
  else if (arg?.startsWith("-")) {
183
189
  return { ...parsed, error: `unrecognized arguments: ${arg}` };
184
190
  }
@@ -206,6 +212,14 @@ export function parseArgs(argv) {
206
212
  error: "--include-closed / --author / --batch-size / --delay-ms / --sample-limit require --mirror (#3125 / #3129)",
207
213
  };
208
214
  }
215
+ // --ack-discovery is standalone (production entry for tip dismissal without dry-run).
216
+ // Combining with --mirror is allowed: ack runs first, then mirror proceeds.
217
+ if (parsed.ackDiscovery && (parsed.doList || parsed.doValidate) && !parsed.doMirror) {
218
+ return {
219
+ ...parsed,
220
+ error: "--ack-discovery cannot combine with --list/--validate without --mirror",
221
+ };
222
+ }
209
223
  return parsed;
210
224
  }
211
225
  /** Run the CLI and return the process exit code. */
@@ -237,6 +251,29 @@ export function run(argv, options = {}) {
237
251
  }
238
252
  return result.code;
239
253
  }
254
+ // #3124: production entry for operator ack (dismiss tip without dry-run).
255
+ if (args.ackDiscovery) {
256
+ try {
257
+ recordMirrorDiscoveryAcked(projectRoot);
258
+ const statePresent = mirrorDiscoveryStateExists(projectRoot);
259
+ if (args.json) {
260
+ process.stdout.write(`${JSON.stringify({ ok: true, ack_discovery: true, projectRoot, statePresent }, null, 2)}\n`);
261
+ }
262
+ else {
263
+ process.stdout.write("[deft triage] SCM label-mirror discovery tip acknowledged " +
264
+ `(state ${statePresent ? "persisted" : "missing"}) — ` +
265
+ "will not re-surface until state is cleared.\n");
266
+ }
267
+ }
268
+ catch (err) {
269
+ const msg = err instanceof Error ? err.message : String(err);
270
+ process.stderr.write(`ERR: failed to record discovery ack: ${msg}\n`);
271
+ return 1;
272
+ }
273
+ if (!args.doMirror) {
274
+ return 0;
275
+ }
276
+ }
240
277
  if (args.doMirror) {
241
278
  let authorFilter = null;
242
279
  // Flag present (including empty `--author=`) must resolve or fail closed —
@@ -268,6 +305,15 @@ export function run(argv, options = {}) {
268
305
  else {
269
306
  process.stdout.write(renderLabelMirrorReport(outcome));
270
307
  }
308
+ // #3124: first successful --mirror dry-run hides the operator discovery tip.
309
+ if (code === 0 && outcome.dry_run) {
310
+ try {
311
+ recordMirrorDiscoverySuccessfulDryRun(projectRoot);
312
+ }
313
+ catch {
314
+ // Advisory tip state — never change the classify exit code.
315
+ }
316
+ }
271
317
  return code;
272
318
  }
273
319
  // Default / --list: print effective rules
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env node
2
+ interface ParsedArgs {
3
+ projectRoot: string;
4
+ xbriefPath: string | null;
5
+ quiet: boolean;
6
+ captureOnly: boolean;
7
+ softMissingXbrief: boolean;
8
+ error?: string;
9
+ }
10
+ /** Parse verify:ac CLI args. */
11
+ export declare function parseArgs(argv: string[]): ParsedArgs;
12
+ /** Result of scanning lifecycle active/ for scope artifacts. */
13
+ export type FindActiveXbriefResult = {
14
+ readonly kind: "one";
15
+ readonly path: string;
16
+ } | {
17
+ readonly kind: "none";
18
+ } | {
19
+ readonly kind: "many";
20
+ readonly paths: readonly string[];
21
+ readonly dir: string;
22
+ };
23
+ /** Run the gate and return the process exit code. */
24
+ export declare function run(argv: string[]): number;
25
+ export {};
26
+ //# sourceMappingURL=verify-ac.d.ts.map
@@ -0,0 +1,244 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * CLI: task verify:ac — product-first acceptance criteria gate (#3284).
4
+ *
5
+ * Runs plan.acceptance.commands (or #3267 literal ledger) verbatim.
6
+ * Primary name for the product-first done-gate; verify:literal-ac remains
7
+ * the #3267 mechanism alias.
8
+ */
9
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
10
+ import { join, resolve } from "node:path";
11
+ import { fileURLToPath } from "node:url";
12
+ import { formatRejectedLedger, resolveLiteralAcceptanceDetailed, } from "@deftai/directive-core/literal-acceptance";
13
+ import { evaluateVerifyAcFromPath, readPlanAcceptance, } from "@deftai/directive-core/product-first-done-gate";
14
+ /** Parse verify:ac CLI args. */
15
+ export function parseArgs(argv) {
16
+ const parsed = {
17
+ projectRoot: ".",
18
+ xbriefPath: null,
19
+ quiet: false,
20
+ captureOnly: false,
21
+ softMissingXbrief: false,
22
+ };
23
+ const positionals = [];
24
+ for (let i = 0; i < argv.length; i += 1) {
25
+ const arg = argv[i];
26
+ if (arg === "--quiet") {
27
+ parsed.quiet = true;
28
+ }
29
+ else if (arg === "--capture-only") {
30
+ parsed.captureOnly = true;
31
+ }
32
+ else if (arg === "--soft-missing-xbrief") {
33
+ parsed.softMissingXbrief = true;
34
+ }
35
+ else if (arg === "--project-root") {
36
+ const value = argv[i + 1];
37
+ if (value === undefined) {
38
+ return { ...parsed, error: "argument --project-root: expected one argument" };
39
+ }
40
+ parsed.projectRoot = value;
41
+ i += 1;
42
+ }
43
+ else if (arg?.startsWith("--project-root=")) {
44
+ parsed.projectRoot = arg.slice("--project-root=".length);
45
+ }
46
+ else if (arg === "--xbrief" || arg === "--vbrief") {
47
+ const value = argv[i + 1];
48
+ if (value === undefined) {
49
+ return { ...parsed, error: `argument ${arg}: expected one argument` };
50
+ }
51
+ parsed.xbriefPath = value;
52
+ i += 1;
53
+ }
54
+ else if (arg?.startsWith("--xbrief=") || arg?.startsWith("--vbrief=")) {
55
+ parsed.xbriefPath = arg.slice(arg.indexOf("=") + 1);
56
+ }
57
+ else if (arg?.startsWith("-")) {
58
+ return { ...parsed, error: `unrecognized argument: ${arg}` };
59
+ }
60
+ else if (arg !== undefined) {
61
+ positionals.push(arg);
62
+ }
63
+ }
64
+ if (parsed.xbriefPath === null && positionals.length > 0) {
65
+ parsed.xbriefPath = positionals[0] ?? null;
66
+ }
67
+ return parsed;
68
+ }
69
+ function listActiveXbriefs(projectRoot) {
70
+ // Greptile conf residual #3284: scan BOTH lifecycle roots (xbrief + read-accepted
71
+ // vbrief). Returning after the first non-empty root left the other unchecked.
72
+ const paths = [];
73
+ const dirs = [];
74
+ for (const dirName of ["xbrief", "vbrief"]) {
75
+ const active = join(projectRoot, dirName, "active");
76
+ if (!existsSync(active))
77
+ continue;
78
+ let names = [];
79
+ try {
80
+ names = readdirSync(active)
81
+ .filter((n) => n.endsWith(".xbrief.json") || n.endsWith(".vbrief.json"))
82
+ .sort();
83
+ }
84
+ catch {
85
+ continue;
86
+ }
87
+ if (names.length === 0)
88
+ continue;
89
+ dirs.push(active);
90
+ for (const n of names) {
91
+ paths.push(join(active, n));
92
+ }
93
+ }
94
+ if (paths.length === 0)
95
+ return { kind: "none" };
96
+ if (paths.length === 1) {
97
+ return { kind: "one", path: paths[0] };
98
+ }
99
+ return {
100
+ kind: "many",
101
+ paths,
102
+ dir: dirs.join(" + "),
103
+ };
104
+ }
105
+ /** Evaluate one or many xBRIEF paths; return worst non-zero code (fail closed). */
106
+ function evaluatePaths(paths, options) {
107
+ let worst = 0;
108
+ for (const path of paths) {
109
+ if (!options.quiet && paths.length > 1) {
110
+ process.stdout.write(`verify:ac — evaluating ${path}\n`);
111
+ }
112
+ const result = evaluateVerifyAcFromPath(path, {
113
+ projectRoot: options.projectRoot,
114
+ quiet: options.quiet,
115
+ softMissingXbrief: options.softMissingXbrief,
116
+ checkIntegrated: options.softMissingXbrief,
117
+ });
118
+ if (result.message.length > 0) {
119
+ if (result.ok) {
120
+ process.stdout.write(`${result.message}\n`);
121
+ }
122
+ else {
123
+ process.stderr.write(`${result.message}\n`);
124
+ }
125
+ }
126
+ if (result.code !== 0 && (worst === 0 || result.code > worst)) {
127
+ // Prefer code 1 (fail) over 2 when both present — still non-zero.
128
+ worst = result.code;
129
+ }
130
+ }
131
+ return worst;
132
+ }
133
+ function asRecord(value) {
134
+ if (typeof value === "object" && value !== null && !Array.isArray(value)) {
135
+ return value;
136
+ }
137
+ return null;
138
+ }
139
+ /** Run the gate and return the process exit code. */
140
+ export function run(argv) {
141
+ const args = parseArgs(argv);
142
+ if (args.error !== undefined) {
143
+ process.stderr.write(`verify_ac: ${args.error}\n`);
144
+ return 2;
145
+ }
146
+ const projectRoot = resolve(args.projectRoot);
147
+ let paths = [];
148
+ if (args.xbriefPath !== null) {
149
+ paths = [resolve(projectRoot, args.xbriefPath)];
150
+ }
151
+ else {
152
+ const found = listActiveXbriefs(projectRoot);
153
+ if (found.kind === "one") {
154
+ paths = [found.path];
155
+ }
156
+ else if (found.kind === "many") {
157
+ // Greptile #3284: multi-active must verify EVERY active scope, not skip.
158
+ if (args.softMissingXbrief || args.captureOnly) {
159
+ paths = [...found.paths];
160
+ if (!args.quiet) {
161
+ process.stdout.write(`verify:ac multi-active (#3284): evaluating ${paths.length} scopes in ${found.dir}\n`);
162
+ }
163
+ }
164
+ else {
165
+ process.stderr.write(`verify_ac: ${found.paths.length} active scopes in ${found.dir}.\n` +
166
+ " Pass an explicit path, or use check composition (--soft-missing-xbrief) to evaluate all.\n" +
167
+ " Usage: task verify:ac -- <path-to-active.xbrief.json>\n" +
168
+ " Refs #3284 product-first done-gate\n");
169
+ return 1;
170
+ }
171
+ }
172
+ else if (args.softMissingXbrief) {
173
+ if (!args.quiet) {
174
+ process.stdout.write("verify:ac skipped (#3284 soft-missing): no active xBRIEF in xbrief/active/\n");
175
+ }
176
+ return 0;
177
+ }
178
+ else {
179
+ process.stderr.write("verify_ac: pass an xBRIEF path or ensure exactly one artifact in xbrief/active/\n" +
180
+ " Usage: task verify:ac -- <path-to-active.xbrief.json>\n" +
181
+ " Refs #3284 product-first done-gate (mechanism #3267)\n");
182
+ return 2;
183
+ }
184
+ }
185
+ if (args.captureOnly) {
186
+ // Capture-only: report each path (multi-active) as a list.
187
+ const reports = [];
188
+ for (const xbriefPath of paths) {
189
+ try {
190
+ const data = asRecord(JSON.parse(readFileSync(xbriefPath, "utf8")));
191
+ if (data === null) {
192
+ process.stderr.write(`verify_ac: xBRIEF top-level is not an object: ${xbriefPath}\n`);
193
+ return 2;
194
+ }
195
+ const plan = asRecord(data.plan);
196
+ if (plan === null) {
197
+ process.stderr.write(`verify_ac: xBRIEF missing plan object: ${xbriefPath}\n`);
198
+ return 2;
199
+ }
200
+ const acceptance = readPlanAcceptance(plan);
201
+ const resolved = resolveLiteralAcceptanceDetailed(plan, { captureFromNarratives: true });
202
+ reports.push({
203
+ xbrief: xbriefPath,
204
+ source_rung: acceptance.source_rung,
205
+ none_stated: acceptance.none_stated,
206
+ acceptance_commands: acceptance.commands,
207
+ count: resolved.commands.length,
208
+ rejected_count: resolved.rejected.length,
209
+ commands: resolved.commands.map((c) => ({
210
+ command: c.command,
211
+ source: c.source,
212
+ sourceSpan: c.sourceSpan ?? null,
213
+ cwd: c.cwd ?? null,
214
+ expectedExitCode: c.expectedExitCode ?? 0,
215
+ })),
216
+ rejected: resolved.rejected.map((r) => ({
217
+ command: r.command,
218
+ reason: r.reason,
219
+ sourceSpan: r.sourceSpan ?? null,
220
+ })),
221
+ });
222
+ if (resolved.rejected.length > 0) {
223
+ process.stderr.write(`${formatRejectedLedger(resolved.rejected)}\n`);
224
+ }
225
+ }
226
+ catch (err) {
227
+ const msg = err instanceof Error ? err.message : String(err);
228
+ process.stderr.write(`verify_ac: ${msg}\n`);
229
+ return 2;
230
+ }
231
+ }
232
+ process.stdout.write(`${JSON.stringify(reports.length === 1 ? reports[0] : { scopes: reports }, null, 2)}\n`);
233
+ return 0;
234
+ }
235
+ return evaluatePaths(paths, {
236
+ projectRoot,
237
+ quiet: args.quiet,
238
+ softMissingXbrief: args.softMissingXbrief,
239
+ });
240
+ }
241
+ if (process.argv[1] !== undefined && fileURLToPath(import.meta.url) === process.argv[1]) {
242
+ process.exit(run(process.argv.slice(2)));
243
+ }
244
+ //# sourceMappingURL=verify-ac.js.map
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env node
2
+ interface ParsedArgs {
3
+ projectRoot: string;
4
+ repo: string | null;
5
+ tip: string | null;
6
+ quiet: boolean;
7
+ skipGh: boolean;
8
+ error?: string;
9
+ }
10
+ /** Parse verify-completed-tracked CLI args (#3264). */
11
+ export declare function parseArgs(argv: string[]): ParsedArgs;
12
+ /** Run the gate and return the process exit code. */
13
+ export declare function run(argv: string[]): number;
14
+ export {};
15
+ //# sourceMappingURL=verify-completed-tracked.d.ts.map
@@ -0,0 +1,88 @@
1
+ #!/usr/bin/env node
2
+ import { resolve } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { evaluateCompletedTracked } from "@deftai/directive-core/lifecycle";
5
+ /** Parse verify-completed-tracked CLI args (#3264). */
6
+ export function parseArgs(argv) {
7
+ const parsed = {
8
+ projectRoot: ".",
9
+ repo: null,
10
+ tip: null,
11
+ quiet: false,
12
+ skipGh: false,
13
+ };
14
+ for (let i = 0; i < argv.length; i += 1) {
15
+ const arg = argv[i];
16
+ if (arg === "--quiet") {
17
+ parsed.quiet = true;
18
+ }
19
+ else if (arg === "--skip-gh") {
20
+ parsed.skipGh = true;
21
+ }
22
+ else if (arg === "--project-root") {
23
+ const value = argv[i + 1];
24
+ if (value === undefined) {
25
+ return { ...parsed, error: "argument --project-root: expected one argument" };
26
+ }
27
+ parsed.projectRoot = value;
28
+ i += 1;
29
+ }
30
+ else if (arg?.startsWith("--project-root=")) {
31
+ parsed.projectRoot = arg.slice("--project-root=".length);
32
+ }
33
+ else if (arg === "--repo") {
34
+ const value = argv[i + 1];
35
+ if (value === undefined) {
36
+ return { ...parsed, error: "argument --repo: expected one argument" };
37
+ }
38
+ parsed.repo = value;
39
+ i += 1;
40
+ }
41
+ else if (arg?.startsWith("--repo=")) {
42
+ parsed.repo = arg.slice("--repo=".length);
43
+ }
44
+ else if (arg === "--tip") {
45
+ const value = argv[i + 1];
46
+ if (value === undefined) {
47
+ return { ...parsed, error: "argument --tip: expected one argument" };
48
+ }
49
+ parsed.tip = value;
50
+ i += 1;
51
+ }
52
+ else if (arg?.startsWith("--tip=")) {
53
+ parsed.tip = arg.slice("--tip=".length);
54
+ }
55
+ else {
56
+ return { ...parsed, error: `unrecognized argument: ${arg}` };
57
+ }
58
+ }
59
+ return parsed;
60
+ }
61
+ /** Run the gate and return the process exit code. */
62
+ export function run(argv) {
63
+ const args = parseArgs(argv);
64
+ if (args.error !== undefined) {
65
+ process.stderr.write(`verify_completed_tracked: ${args.error}\n`);
66
+ return 2;
67
+ }
68
+ const projectRoot = resolve(args.projectRoot);
69
+ const result = evaluateCompletedTracked(projectRoot, {
70
+ quiet: args.quiet,
71
+ repo: args.repo,
72
+ tip: args.tip,
73
+ skipGh: args.skipGh,
74
+ });
75
+ if (result.message.length > 0) {
76
+ if (result.stream === "stdout") {
77
+ process.stdout.write(`${result.message}\n`);
78
+ }
79
+ else if (result.stream === "stderr") {
80
+ process.stderr.write(`${result.message}\n`);
81
+ }
82
+ }
83
+ return result.code;
84
+ }
85
+ if (process.argv[1] !== undefined && fileURLToPath(import.meta.url) === process.argv[1]) {
86
+ process.exit(run(process.argv.slice(2)));
87
+ }
88
+ //# sourceMappingURL=verify-completed-tracked.js.map
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env node
2
+ interface ParsedArgs {
3
+ projectRoot: string;
4
+ xbriefPath: string | null;
5
+ quiet: boolean;
6
+ captureOnly: boolean;
7
+ error?: string;
8
+ }
9
+ /** Parse verify-literal-ac CLI args. */
10
+ export declare function parseArgs(argv: string[]): ParsedArgs;
11
+ /** Run the gate and return the process exit code. */
12
+ export declare function run(argv: string[]): number;
13
+ export {};
14
+ //# sourceMappingURL=verify-literal-ac.d.ts.map
@@ -0,0 +1,161 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * CLI: task verify:literal-ac — run stored literal acceptance commands verbatim (#3267).
4
+ */
5
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
6
+ import { join, resolve } from "node:path";
7
+ import { fileURLToPath } from "node:url";
8
+ import { evaluateLiteralAcceptanceFromPath, formatRejectedLedger, resolveLiteralAcceptanceDetailed, } from "@deftai/directive-core/literal-acceptance";
9
+ /** Parse verify-literal-ac CLI args. */
10
+ export function parseArgs(argv) {
11
+ const parsed = {
12
+ projectRoot: ".",
13
+ xbriefPath: null,
14
+ quiet: false,
15
+ captureOnly: false,
16
+ };
17
+ const positionals = [];
18
+ for (let i = 0; i < argv.length; i += 1) {
19
+ const arg = argv[i];
20
+ if (arg === "--quiet") {
21
+ parsed.quiet = true;
22
+ }
23
+ else if (arg === "--capture-only") {
24
+ parsed.captureOnly = true;
25
+ }
26
+ else if (arg === "--project-root") {
27
+ const value = argv[i + 1];
28
+ if (value === undefined) {
29
+ return { ...parsed, error: "argument --project-root: expected one argument" };
30
+ }
31
+ parsed.projectRoot = value;
32
+ i += 1;
33
+ }
34
+ else if (arg?.startsWith("--project-root=")) {
35
+ parsed.projectRoot = arg.slice("--project-root=".length);
36
+ }
37
+ else if (arg === "--xbrief" || arg === "--vbrief") {
38
+ const value = argv[i + 1];
39
+ if (value === undefined) {
40
+ return { ...parsed, error: `argument ${arg}: expected one argument` };
41
+ }
42
+ parsed.xbriefPath = value;
43
+ i += 1;
44
+ }
45
+ else if (arg?.startsWith("--xbrief=") || arg?.startsWith("--vbrief=")) {
46
+ parsed.xbriefPath = arg.slice(arg.indexOf("=") + 1);
47
+ }
48
+ else if (arg?.startsWith("-")) {
49
+ return { ...parsed, error: `unrecognized argument: ${arg}` };
50
+ }
51
+ else if (arg !== undefined) {
52
+ positionals.push(arg);
53
+ }
54
+ }
55
+ if (parsed.xbriefPath === null && positionals.length > 0) {
56
+ parsed.xbriefPath = positionals[0] ?? null;
57
+ }
58
+ return parsed;
59
+ }
60
+ function findActiveXbrief(projectRoot) {
61
+ for (const dirName of ["xbrief", "vbrief"]) {
62
+ const active = join(projectRoot, dirName, "active");
63
+ if (!existsSync(active))
64
+ continue;
65
+ let names = [];
66
+ try {
67
+ names = readdirSync(active).filter((n) => n.endsWith(".xbrief.json") || n.endsWith(".vbrief.json"));
68
+ }
69
+ catch {
70
+ continue;
71
+ }
72
+ if (names.length === 1) {
73
+ return join(active, names[0]);
74
+ }
75
+ }
76
+ return null;
77
+ }
78
+ function asRecord(value) {
79
+ if (typeof value === "object" && value !== null && !Array.isArray(value)) {
80
+ return value;
81
+ }
82
+ return null;
83
+ }
84
+ /** Run the gate and return the process exit code. */
85
+ export function run(argv) {
86
+ const args = parseArgs(argv);
87
+ if (args.error !== undefined) {
88
+ process.stderr.write(`verify_literal_ac: ${args.error}\n`);
89
+ return 2;
90
+ }
91
+ const projectRoot = resolve(args.projectRoot);
92
+ let xbriefPath = args.xbriefPath !== null ? resolve(projectRoot, args.xbriefPath) : null;
93
+ if (xbriefPath === null) {
94
+ xbriefPath = findActiveXbrief(projectRoot);
95
+ }
96
+ if (xbriefPath === null) {
97
+ process.stderr.write("verify_literal_ac: pass an xBRIEF path or ensure exactly one artifact in xbrief/active/\n" +
98
+ " Usage: task verify:literal-ac -- <path-to-active.xbrief.json>\n" +
99
+ " Refs #3267 literal acceptance-command verification\n");
100
+ return 2;
101
+ }
102
+ if (args.captureOnly) {
103
+ try {
104
+ const data = asRecord(JSON.parse(readFileSync(xbriefPath, "utf8")));
105
+ if (data === null) {
106
+ process.stderr.write("verify_literal_ac: xBRIEF top-level is not an object\n");
107
+ return 2;
108
+ }
109
+ const plan = asRecord(data.plan);
110
+ if (plan === null) {
111
+ process.stderr.write("verify_literal_ac: xBRIEF missing plan object\n");
112
+ return 2;
113
+ }
114
+ const resolved = resolveLiteralAcceptanceDetailed(plan, { captureFromNarratives: true });
115
+ const payload = {
116
+ xbrief: xbriefPath,
117
+ count: resolved.commands.length,
118
+ rejected_count: resolved.rejected.length,
119
+ commands: resolved.commands.map((c) => ({
120
+ command: c.command,
121
+ source: c.source,
122
+ sourceSpan: c.sourceSpan ?? null,
123
+ cwd: c.cwd ?? null,
124
+ expectedExitCode: c.expectedExitCode ?? 0,
125
+ })),
126
+ rejected: resolved.rejected.map((r) => ({
127
+ command: r.command,
128
+ reason: r.reason,
129
+ sourceSpan: r.sourceSpan ?? null,
130
+ })),
131
+ };
132
+ process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
133
+ if (resolved.rejected.length > 0) {
134
+ process.stderr.write(`${formatRejectedLedger(resolved.rejected)}\n`);
135
+ }
136
+ return 0;
137
+ }
138
+ catch (err) {
139
+ const msg = err instanceof Error ? err.message : String(err);
140
+ process.stderr.write(`verify_literal_ac: ${msg}\n`);
141
+ return 2;
142
+ }
143
+ }
144
+ const result = evaluateLiteralAcceptanceFromPath(xbriefPath, {
145
+ projectRoot,
146
+ quiet: args.quiet,
147
+ });
148
+ if (result.message.length > 0) {
149
+ if (result.ok) {
150
+ process.stdout.write(`${result.message}\n`);
151
+ }
152
+ else {
153
+ process.stderr.write(`${result.message}\n`);
154
+ }
155
+ }
156
+ return result.code;
157
+ }
158
+ if (process.argv[1] !== undefined && fileURLToPath(import.meta.url) === process.argv[1]) {
159
+ process.exit(run(process.argv.slice(2)));
160
+ }
161
+ //# sourceMappingURL=verify-literal-ac.js.map
@@ -148,6 +148,7 @@ export function run(argv) {
148
148
  allocationContext: allocationContextText,
149
149
  allowDirty: args.allowDirty,
150
150
  parsed,
151
+ projectRoot,
151
152
  });
152
153
  if (result.exitCode === 0 && !args.skipRouting) {
153
154
  const provider = resolveDispatchProvider(process.env);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deftai/directive",
3
- "version": "0.99.0",
3
+ "version": "0.101.0",
4
4
  "description": "Directive CLI — npm install path for the Deft Directive framework.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://deftai.github.io/directive/",
@@ -34,8 +34,8 @@
34
34
  "provenance": true
35
35
  },
36
36
  "dependencies": {
37
- "@deftai/directive-core": "^0.99.0",
38
- "@deftai/directive-content": "^0.99.0"
37
+ "@deftai/directive-core": "^0.101.0",
38
+ "@deftai/directive-content": "^0.101.0"
39
39
  },
40
40
  "scripts": {
41
41
  "build": "tsc -b"