@deftai/directive 0.100.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-completed-tracked", "verify-literal-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"];
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
@@ -139,6 +139,7 @@ export const CLI_MODULE_VERBS = [
139
139
  "verify-orphan-active",
140
140
  "verify-completed-tracked",
141
141
  "verify-literal-ac",
142
+ "verify-ac",
142
143
  "verify-agents-md-budget",
143
144
  "verify-agents-md-advisory",
144
145
  "verify-eval-health-relocation",
@@ -284,6 +285,8 @@ export const VERB_ALIASES = {
284
285
  "verify:completed-tracked": "verify-completed-tracked",
285
286
  "verify:literal-ac": "verify-literal-ac",
286
287
  "verify:literal-acceptance": "verify-literal-ac",
288
+ "verify:ac": "verify-ac",
289
+ "verify:acceptance": "verify-ac",
287
290
  "verify:agents-md-budget": "verify-agents-md-budget",
288
291
  "verify:agents-md-advisory": "verify-agents-md-advisory",
289
292
  "verify:eval-health-relocation": "verify-eval-health-relocation",
@@ -27,6 +27,11 @@ export interface ParsedSessionStartArgs {
27
27
  maxBudget?: number;
28
28
  hardBudget?: boolean;
29
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;
30
35
  error?: string;
31
36
  }
32
37
  /** Parse session:start CLI args, mirroring scripts/session_start.py. */
@@ -30,6 +30,7 @@ export function parseArgs(argv) {
30
30
  ceremonyDialInputs: {},
31
31
  ceremonyDepthOverride: null,
32
32
  effortBudgetHost: {},
33
+ compact: false,
33
34
  };
34
35
  const dialInputs = {};
35
36
  for (let i = 0; i < argv.length; i += 1) {
@@ -268,6 +269,10 @@ export function parseArgs(argv) {
268
269
  else if (arg === "--hard-budget") {
269
270
  parsed.effortBudgetHost.hardBudget = true;
270
271
  }
272
+ else if (arg === "--compact") {
273
+ // #3286: terse machine orientation output (opt-in; verbose remains default)
274
+ parsed.compact = true;
275
+ }
271
276
  else {
272
277
  return { ...parsed, error: `unrecognized argument: ${arg}` };
273
278
  }
@@ -328,6 +333,8 @@ export function run(argv) {
328
333
  ceremonyTier: args.ceremonyTier,
329
334
  ceremonyDialInputs: args.ceremonyDialInputs,
330
335
  effortBudgetSeams: { hostDescriptor, environ: process.env },
336
+ // #3286: --compact flag; DEFT_SESSION_COMPACT still resolved inside core.
337
+ compact: args.compact ? true : undefined,
331
338
  ...(args.ceremonyDepthOverride !== null
332
339
  ? {
333
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deftai/directive",
3
- "version": "0.100.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.100.0",
38
- "@deftai/directive-content": "^0.100.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"