@deftai/directive 0.94.0 → 0.95.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.
package/dist/authz.d.ts CHANGED
@@ -1,4 +1,32 @@
1
1
  #!/usr/bin/env node
2
- export declare function main(argv?: string[]): number;
2
+ /**
3
+ * Env markers that indicate an agent/host/CI shell even when stdin reports a TTY
4
+ * (pseudo-terminal residual; #3110 Greptile). Presence refuses mutating authz.
5
+ * Expanded for dogfood conf 5/5 — markers are fail-closed (any non-empty value).
6
+ */
7
+ export declare const AUTHZ_AGENT_SHELL_ENV_MARKERS: readonly ["CLAUDECODE", "CLAUDE_CODE", "CLAUDE_CODE_ENTRYPOINT", "CURSOR_AGENT", "CURSOR_TRACE_ID", "CURSOR_SESSION_ID", "AIDER", "CONTINUE_CLI", "CODEX_SANDBOX", "CODEX_CI", "OPENAI_CODEX", "OPENCLAW", "OPENCLAW_STATE_DIR", "DEFT_PROBE_OPENCLAW", "DEFT_HOOK_HOST", "DEFT_AGENT_SHELL", "DEFT_AGENT_RUNTIME", "WARP_SESSION_ID", "WARP_HARNESS", "WARP_RUN_ID", "GEMINI_CLI", "AMP_CLI", "SWARM_AGENT", "AI_AGENT", "CI", "CONTINUOUS_INTEGRATION", "GITHUB_ACTIONS", "GITLAB_CI", "CIRCLECI", "BUILDKITE", "TRAVIS", "JENKINS_URL", "TEAMCITY_VERSION", "TF_BUILD", "APPVEYOR", "BITBUCKET_BUILD_NUMBER", "CODEBUILD_BUILD_ID"];
8
+ /** Phrase an operator must type on the controlling TTY after --confirm (#3110). */
9
+ export declare const AUTHZ_INTERACTIVE_CONFIRM_PHRASE = "mint";
10
+ /** Testable seams for TTY / agent-shell detection (#3110). */
11
+ export interface AuthzMainSeams {
12
+ /**
13
+ * When true, interactive human TTY is present.
14
+ * Default: both stdin and stdout report isTTY (pseudo-TTY residual; #3110).
15
+ */
16
+ readonly isTty?: () => boolean;
17
+ /** Environ for agent-shell marker detection (default: process.env). */
18
+ readonly environ?: NodeJS.ProcessEnv;
19
+ /**
20
+ * True when a controlling terminal device is available (`/dev/tty` or `CONIN$`).
21
+ * Default: open/close the platform controlling terminal (fail-closed on error).
22
+ */
23
+ readonly hasControllingTerminal?: () => boolean;
24
+ /**
25
+ * Read one interactive confirmation line from the operator (trimmed).
26
+ * Default: one line from stdin. Tests inject a fixed phrase.
27
+ */
28
+ readonly readInteractiveConfirm?: () => string | null;
29
+ }
30
+ export declare function main(argv?: string[], seams?: AuthzMainSeams): number;
3
31
  export default main;
4
32
  //# sourceMappingURL=authz.d.ts.map
package/dist/authz.js CHANGED
@@ -1,17 +1,75 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * Authz CLI (#2944 Wave 1 + #1095 Wave 4): human-origin grants + UAT lease +
3
+ * Authz CLI (#2944 Wave 1 + #1095 Wave 4 + #3110): human-origin grants + UAT lease +
4
4
  * AFK closed-verb templates (mint via mintHumanOriginGrant only).
5
5
  *
6
6
  * deft authz:show
7
- * deft authz:uat-start -- --campaign <id> [--actor <name>] [--note <text>]
8
- * deft authz:uat-suspend
9
- * deft authz:grant -- --operations edit,push --surfaces 'src/**' --cohort <id> ...
10
- * deft authz:grant -- --template release-publish --target 0.30.0
11
- * deft authz:grant -- --template finish-loop
12
- * deft authz:revoke -- <grant-id>
7
+ * deft authz:uat-start -- --campaign <id> [--actor <name>] [--note <text>] [--confirm]
8
+ * deft authz:uat-suspend [--confirm]
9
+ * deft authz:grant -- --operations edit,push --surfaces 'src/**' --cohort <id> ... [--confirm]
10
+ * deft authz:grant -- --template release-publish --target 0.30.0 [--confirm]
11
+ * deft authz:grant -- --template finish-loop [--confirm]
12
+ * deft authz:revoke -- <grant-id> [--confirm]
13
+ *
14
+ * **UAT-active hard refuse (#3110):** while any UAT lease is active, ALL mutating
15
+ * verbs (`uat-start`, `uat-suspend`, `grant`, `revoke`) refuse unconditionally —
16
+ * no TTY, no `--confirm`, no typed phrase path. Self-approval under UAT is
17
+ * impossible by construction (agent PTY cannot mint operator-cli authority).
18
+ *
19
+ * **Outside UAT:** mutating verbs require multi-factor human presence: interactive
20
+ * TTY + controlling terminal + `--confirm` + typed phrase `mint`; agent/CI env
21
+ * markers refuse fail-closed. Argv `--confirm` alone is never enough.
22
+ */
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
+ /**
26
+ * Env markers that indicate an agent/host/CI shell even when stdin reports a TTY
27
+ * (pseudo-terminal residual; #3110 Greptile). Presence refuses mutating authz.
28
+ * Expanded for dogfood conf 5/5 — markers are fail-closed (any non-empty value).
13
29
  */
14
- import { AFK_TEMPLATE_NAMES, AUTHZ_OPERATIONS, CLOSED_VERB_TEMPLATE_NAMES, FINISH_LOOP_TEMPLATE_NAME, isAfkTemplateName, isClosedVerbTemplateName, isFinishLoopTemplateName, mintAfkTemplateGrant, mintHumanOriginGrant, revokeGrant, showAuthzSnapshot, startUatLease, suspendUatLease, } from "@deftai/directive-core/authz";
30
+ export const AUTHZ_AGENT_SHELL_ENV_MARKERS = [
31
+ // Coding agents / IDEs
32
+ "CLAUDECODE",
33
+ "CLAUDE_CODE",
34
+ "CLAUDE_CODE_ENTRYPOINT",
35
+ "CURSOR_AGENT",
36
+ "CURSOR_TRACE_ID",
37
+ "CURSOR_SESSION_ID",
38
+ "AIDER",
39
+ "CONTINUE_CLI",
40
+ "CODEX_SANDBOX",
41
+ "CODEX_CI",
42
+ "OPENAI_CODEX",
43
+ "OPENCLAW",
44
+ "OPENCLAW_STATE_DIR",
45
+ "DEFT_PROBE_OPENCLAW",
46
+ "DEFT_HOOK_HOST",
47
+ "DEFT_AGENT_SHELL",
48
+ "DEFT_AGENT_RUNTIME",
49
+ "WARP_SESSION_ID",
50
+ "WARP_HARNESS",
51
+ "WARP_RUN_ID",
52
+ "GEMINI_CLI",
53
+ "AMP_CLI",
54
+ "SWARM_AGENT",
55
+ "AI_AGENT",
56
+ // CI / automation (never a human interactive operator mint)
57
+ "CI",
58
+ "CONTINUOUS_INTEGRATION",
59
+ "GITHUB_ACTIONS",
60
+ "GITLAB_CI",
61
+ "CIRCLECI",
62
+ "BUILDKITE",
63
+ "TRAVIS",
64
+ "JENKINS_URL",
65
+ "TEAMCITY_VERSION",
66
+ "TF_BUILD",
67
+ "APPVEYOR",
68
+ "BITBUCKET_BUILD_NUMBER",
69
+ "CODEBUILD_BUILD_ID",
70
+ ];
71
+ /** Phrase an operator must type on the controlling TTY after --confirm (#3110). */
72
+ export const AUTHZ_INTERACTIVE_CONFIRM_PHRASE = "mint";
15
73
  function parseOps(raw) {
16
74
  const allowed = new Set(AUTHZ_OPERATIONS);
17
75
  const out = [];
@@ -47,6 +105,7 @@ function parseArgv(argv) {
47
105
  template: null,
48
106
  target: null,
49
107
  format: "text",
108
+ confirm: false,
50
109
  };
51
110
  const args = [...argv];
52
111
  // Drop leading `--` separators from task-style invocation.
@@ -165,6 +224,10 @@ function parseArgv(argv) {
165
224
  base.target = args[++i] ?? null;
166
225
  continue;
167
226
  }
227
+ if (a === "--confirm") {
228
+ base.confirm = true;
229
+ continue;
230
+ }
168
231
  if (!a.startsWith("-") && base.cmd === "revoke" && base.grantId === null) {
169
232
  base.grantId = a;
170
233
  continue;
@@ -179,16 +242,26 @@ function helpText() {
179
242
  return [
180
243
  "Usage:",
181
244
  " deft authz:show [--format json]",
182
- " deft authz:uat-start -- --campaign <id> [--actor <name>] [--note <text>]",
183
- " deft authz:uat-suspend",
245
+ " deft authz:uat-start -- --campaign <id> [--actor <name>] [--note <text>] [--confirm]",
246
+ " deft authz:uat-suspend [--confirm]",
184
247
  " deft authz:grant -- --operations edit,push --surfaces 'src/**' --cohort <id> \\",
185
- " [--stories 2944] [--plan-ref <id>] [--repo owner/name] [--branch <b>] [--expires ISO]",
186
- " deft authz:grant -- --template release-publish --target 0.30.0 [--actor <name>] [--expires ISO]",
187
- " deft authz:grant -- --template finish-loop [--actor <name>] [--expires ISO]",
188
- " deft authz:revoke -- <grant-id>",
248
+ " [--stories 2944] [--plan-ref <id>] [--repo owner/name] [--branch <b>] [--expires ISO] [--confirm]",
249
+ " deft authz:grant -- --template release-publish --target 0.30.0 [--actor <name>] [--expires ISO] [--confirm]",
250
+ " deft authz:grant -- --template finish-loop [--actor <name>] [--expires ISO] [--confirm]",
251
+ " deft authz:revoke -- <grant-id> [--confirm]",
189
252
  "",
190
253
  "Human-origin grants are minted only via this CLI (origin.kind=operator-cli).",
191
254
  "Self-authored xBRIEF/lifecycle/dispatch tokens never satisfy implement gates (#2944).",
255
+ "While any UAT lease is ACTIVE (#3110): ALL mutating verbs refuse unconditionally",
256
+ " (grant / uat-start / uat-suspend / revoke) — no TTY, --confirm, or phrase path.",
257
+ " Self-approval under UAT is impossible by construction. Mint grants BEFORE uat-start.",
258
+ "Outside UAT, mutating verbs require multi-factor human presence (#3110):",
259
+ " - Interactive TTY (stdin+stdout) + controlling terminal (/dev/tty|CONIN$)",
260
+ " - Explicit --confirm (argv flag alone never enough)",
261
+ " - Typed phrase 'mint' on the controlling TTY (PTY+--confirm alone never enough)",
262
+ " - Known agent/CI env markers always refuse (fail-closed).",
263
+ " End UAT only after the lease is cleared out-of-band (state edit / suspend path",
264
+ " that does not run while the lease is active) — or suspend before re-minting.",
192
265
  "",
193
266
  `AFK templates (#1095 / #871): ${AFK_TEMPLATE_NAMES.join(", ")}`,
194
267
  ` Closed-verb (#1095): ${CLOSED_VERB_TEMPLATE_NAMES.join(", ")} — require --target`,
@@ -197,7 +270,131 @@ function helpText() {
197
270
  " Env bypass for a single shell: DEFT_ALLOW_RELEASE_PUBLISH=1 / DEFT_ALLOW_FINISH_LOOP=1.",
198
271
  ].join("\n");
199
272
  }
200
- export function main(argv = process.argv.slice(2)) {
273
+ function looksLikeAgentShell(environ) {
274
+ for (const key of AUTHZ_AGENT_SHELL_ENV_MARKERS) {
275
+ const v = environ[key];
276
+ if (v !== undefined && String(v).trim().length > 0)
277
+ return true;
278
+ }
279
+ return false;
280
+ }
281
+ function defaultHasControllingTerminal() {
282
+ try {
283
+ const path = process.platform === "win32" ? "CONIN$" : "/dev/tty";
284
+ const fd = openSync(path, "r");
285
+ closeSync(fd);
286
+ return true;
287
+ }
288
+ catch {
289
+ return false;
290
+ }
291
+ }
292
+ function defaultReadInteractiveConfirm() {
293
+ // Read from the controlling terminal device — not redirected/piped stdin —
294
+ // so agent-controlled stdin alone cannot supply the confirm phrase (#3110).
295
+ let fd = null;
296
+ try {
297
+ const path = process.platform === "win32" ? "CONIN$" : "/dev/tty";
298
+ fd = openSync(path, "r");
299
+ const buf = Buffer.alloc(256);
300
+ const n = readSync(fd, buf, 0, buf.length, null);
301
+ if (n <= 0)
302
+ return null;
303
+ return buf.subarray(0, n).toString("utf8").trim();
304
+ }
305
+ catch {
306
+ return null;
307
+ }
308
+ finally {
309
+ if (fd !== null) {
310
+ try {
311
+ closeSync(fd);
312
+ }
313
+ catch {
314
+ /* ignore */
315
+ }
316
+ }
317
+ }
318
+ }
319
+ /**
320
+ * While any UAT lease is active, refuse ALL mutating authz CLI verbs (#3110).
321
+ *
322
+ * No multi-factor escape: TTY, `--confirm`, and typed phrase never authorize.
323
+ * Self-approval under UAT is impossible by construction — agents with a PTY
324
+ * cannot mint/suspend/revoke operator-cli authority during an active lease.
325
+ * Operators mint fix-cohort grants *before* `uat-start`.
326
+ *
327
+ * Returns exit code when blocked, or null when the command may continue.
328
+ */
329
+ function refuseMutatingAuthzWhileUatActive(projectRoot, cmd) {
330
+ if (cmd === "show")
331
+ return null;
332
+ const state = loadAuthzState(projectRoot);
333
+ if (state.uat === null || !state.uat.active)
334
+ return null;
335
+ process.stderr.write(`authz:${cmd}: refusing mutating authz while UAT lease is ACTIVE ` +
336
+ `(campaign=${state.uat.campaignId}). Under active UAT, grant / uat-start / ` +
337
+ "uat-suspend / revoke are hard-refused — no TTY, --confirm, or phrase path " +
338
+ "authorizes self-approval (#3110). Mint grants before uat-start; clear the " +
339
+ "lease out-of-band (edit .deft/authz/state.json as a human outside agent hooks) " +
340
+ "to end UAT.\n");
341
+ return 2;
342
+ }
343
+ /**
344
+ * Refuse non-interactive / agent-shell operator-cli stamps outside UAT (#3110).
345
+ *
346
+ * Multi-factor human-presence gate (applies only when UAT lease is inactive):
347
+ * 1. No known agent/CI env markers
348
+ * 2. Interactive TTY (stdin + stdout isTTY)
349
+ * 3. Controlling terminal device present (`/dev/tty` / `CONIN$`)
350
+ * 4. Explicit argv `--confirm` (flag alone never enough)
351
+ * 5. Interactive typed phrase `mint` (argv --confirm alone never enough even on PTY)
352
+ *
353
+ * Fail-closed: if a real human interactive path cannot be proven, refuse mint.
354
+ * Returns an exit code when blocked, or null when the mutation may proceed.
355
+ */
356
+ function refuseNonInteractiveMint(cmd, isTty, environ, confirm, hasControllingTerminal, readInteractiveConfirm) {
357
+ if (cmd === "show")
358
+ return null;
359
+ if (looksLikeAgentShell(environ)) {
360
+ process.stderr.write(`authz:${cmd}: refusing operator-cli stamp from an agent/host/CI shell ` +
361
+ `(detected agent or CI env marker). Mutating authz requires a human interactive ` +
362
+ "TTY without agent-shell markers, plus --confirm and typed phrase (#3110).\n");
363
+ return 2;
364
+ }
365
+ const tty = isTty();
366
+ if (!tty && !confirm) {
367
+ process.stderr.write(`authz:${cmd}: refusing non-interactive operator-cli stamp. ` +
368
+ "Mutating authz verbs require interactive TTY, --confirm, and typed phrase " +
369
+ `'${AUTHZ_INTERACTIVE_CONFIRM_PHRASE}' (#3110).\n`);
370
+ return 2;
371
+ }
372
+ if (!tty) {
373
+ process.stderr.write(`authz:${cmd}: refusing non-TTY operator-cli stamp. ` +
374
+ "--confirm alone never authorizes mint — interactive TTY is required (#3110).\n");
375
+ return 2;
376
+ }
377
+ if (!confirm) {
378
+ process.stderr.write(`authz:${cmd}: refusing operator-cli stamp without --confirm. ` +
379
+ "Interactive TTY alone never authorizes mint — pass --confirm explicitly (#3110).\n");
380
+ return 2;
381
+ }
382
+ if (!hasControllingTerminal()) {
383
+ process.stderr.write(`authz:${cmd}: refusing operator-cli stamp without a controlling terminal. ` +
384
+ "Open a real interactive console (not a headless/agent pipe) to mint (#3110).\n");
385
+ return 2;
386
+ }
387
+ process.stderr.write(`authz:${cmd}: type '${AUTHZ_INTERACTIVE_CONFIRM_PHRASE}' and press Enter to confirm operator mint: `);
388
+ const line = readInteractiveConfirm();
389
+ const phrase = (line ?? "").trim().toLowerCase();
390
+ if (phrase !== AUTHZ_INTERACTIVE_CONFIRM_PHRASE) {
391
+ process.stderr.write(`\nauthz:${cmd}: interactive confirm phrase mismatch (got ${JSON.stringify(line ?? "")}). ` +
392
+ `Type exactly '${AUTHZ_INTERACTIVE_CONFIRM_PHRASE}' on the controlling TTY (#3110).\n`);
393
+ return 2;
394
+ }
395
+ return null;
396
+ }
397
+ export function main(argv = process.argv.slice(2), seams = {}) {
201
398
  const args = parseArgv(argv);
202
399
  if (args.error === "help") {
203
400
  process.stdout.write(`${helpText()}\n`);
@@ -208,6 +405,23 @@ export function main(argv = process.argv.slice(2)) {
208
405
  process.stderr.write(`${helpText()}\n`);
209
406
  return 2;
210
407
  }
408
+ // Both stdin and stdout TTY — agent-allocated single-side PTY is not enough.
409
+ const isTty = seams.isTty ?? (() => process.stdin.isTTY === true && process.stdout.isTTY === true);
410
+ const environ = seams.environ ?? process.env;
411
+ const hasControllingTerminal = seams.hasControllingTerminal ?? defaultHasControllingTerminal;
412
+ const readInteractiveConfirm = seams.readInteractiveConfirm ?? defaultReadInteractiveConfirm;
413
+ /**
414
+ * Mutating-verb gate stack (#3110):
415
+ * 1. Active UAT → hard refuse (no multi-factor escape; self-approval impossible)
416
+ * 2. Else multi-factor: TTY + controlling tty + --confirm + typed phrase; agent/CI markers refuse
417
+ * Required-arg validation runs before this so missing --campaign / --ops still report clearly.
418
+ */
419
+ const gateConfirm = () => {
420
+ const uatBlocked = refuseMutatingAuthzWhileUatActive(args.projectRoot, args.cmd);
421
+ if (uatBlocked !== null)
422
+ return uatBlocked;
423
+ return refuseNonInteractiveMint(args.cmd, isTty, environ, args.confirm, hasControllingTerminal, readInteractiveConfirm);
424
+ };
211
425
  try {
212
426
  switch (args.cmd) {
213
427
  case "show": {
@@ -242,6 +456,9 @@ export function main(argv = process.argv.slice(2)) {
242
456
  process.stderr.write("authz:uat-start requires --campaign <id>\n");
243
457
  return 2;
244
458
  }
459
+ const blocked = gateConfirm();
460
+ if (blocked !== null)
461
+ return blocked;
245
462
  const { lease } = startUatLease({
246
463
  projectRoot: args.projectRoot,
247
464
  campaignId: args.campaign,
@@ -254,6 +471,9 @@ export function main(argv = process.argv.slice(2)) {
254
471
  return 0;
255
472
  }
256
473
  case "uat-suspend": {
474
+ const blocked = gateConfirm();
475
+ if (blocked !== null)
476
+ return blocked;
257
477
  const state = suspendUatLease({
258
478
  projectRoot: args.projectRoot,
259
479
  actor: args.actor,
@@ -278,6 +498,9 @@ export function main(argv = process.argv.slice(2)) {
278
498
  process.stderr.write(`authz:grant --template ${args.template} requires --target <version>\n`);
279
499
  return 2;
280
500
  }
501
+ const blocked = gateConfirm();
502
+ if (blocked !== null)
503
+ return blocked;
281
504
  const grant = mintAfkTemplateGrant({
282
505
  projectRoot: args.projectRoot,
283
506
  template: args.template,
@@ -309,6 +532,11 @@ export function main(argv = process.argv.slice(2)) {
309
532
  process.stderr.write("authz:grant requires --operations <edit,push,...> or --template <finish-loop|release-*> \n");
310
533
  return 2;
311
534
  }
535
+ {
536
+ const blocked = gateConfirm();
537
+ if (blocked !== null)
538
+ return blocked;
539
+ }
312
540
  const grant = mintHumanOriginGrant({
313
541
  projectRoot: args.projectRoot,
314
542
  actor: args.actor,
@@ -335,6 +563,11 @@ export function main(argv = process.argv.slice(2)) {
335
563
  process.stderr.write("authz:revoke requires <grant-id>\n");
336
564
  return 2;
337
565
  }
566
+ {
567
+ const blocked = gateConfirm();
568
+ if (blocked !== null)
569
+ return blocked;
570
+ }
338
571
  const revoked = revokeGrant({
339
572
  projectRoot: args.projectRoot,
340
573
  grantId: args.grantId,
@@ -12,7 +12,7 @@ export interface DispatchIo {
12
12
  /** CLI modules in packages/cli/src (excluding parity harnesses and bin/index). */
13
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", "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-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"];
14
14
  /** Core-only CLI entrypoints without a packages/cli wrapper. */
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-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"];
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-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"];
16
16
  /** Colon aliases for triage-actions (mirrors cli-router SUBCOMMAND_ROUTES). */
17
17
  export declare const TRIAGE_ACTION_ALIAS_SUBCOMMANDS: Readonly<Record<string, string>>;
18
18
  /** Colon aliases for policy subcommands (mirrors cli-router SUBCOMMAND_ROUTES). */
@@ -25,6 +25,8 @@ export declare const ESCALATION_ACTION_ALIAS_SUBCOMMANDS: Readonly<Record<string
25
25
  export declare const PLAN_SEQUENCE_ALIAS_SUBCOMMANDS: Readonly<Record<string, string>>;
26
26
  /** Colon aliases for product-signal subcommands (#2693). */
27
27
  export declare const PRODUCT_SIGNAL_ALIAS_SUBCOMMANDS: Readonly<Record<string, string>>;
28
+ /** Colon aliases for freshness subcommands (#3117). */
29
+ export declare const FRESHNESS_ALIAS_SUBCOMMANDS: Readonly<Record<string, string>>;
28
30
  /** Task-style aliases (framework_commands / Taskfile names). */
29
31
  export declare const VERB_ALIASES: Readonly<Record<string, string>>;
30
32
  /** Pinned ghx version (display only) — keep in lockstep with .github/workflows/ci.yml env.GHX_VERSION. */
package/dist/dispatch.js CHANGED
@@ -186,6 +186,7 @@ export const CORE_MODULE_VERBS = [
186
186
  "feedback-file",
187
187
  "value-readback",
188
188
  "product-signal",
189
+ "freshness-report",
189
190
  ];
190
191
  /** Colon aliases for triage-actions (mirrors cli-router SUBCOMMAND_ROUTES). */
191
192
  export const TRIAGE_ACTION_ALIAS_SUBCOMMANDS = {
@@ -245,6 +246,14 @@ export const PRODUCT_SIGNAL_ALIAS_SUBCOMMANDS = {
245
246
  "product-signal:bootstrap-sink": "bootstrap-sink",
246
247
  };
247
248
  const PRODUCT_SIGNAL_COLON_ALIASES = Object.fromEntries(Object.keys(PRODUCT_SIGNAL_ALIAS_SUBCOMMANDS).map((alias) => [alias, "product-signal"]));
249
+ /** Colon aliases for freshness subcommands (#3117). */
250
+ export const FRESHNESS_ALIAS_SUBCOMMANDS = {
251
+ "freshness:report": "report",
252
+ "freshness:bind": "bind",
253
+ "session:freshness": "report",
254
+ freshness: "report",
255
+ };
256
+ const FRESHNESS_COLON_ALIASES = Object.fromEntries(Object.keys(FRESHNESS_ALIAS_SUBCOMMANDS).map((alias) => [alias, "freshness-report"]));
248
257
  /** Task-style aliases (framework_commands / Taskfile names). */
249
258
  export const VERB_ALIASES = {
250
259
  "hook:dispatch": "hook-dispatch",
@@ -321,6 +330,7 @@ export const VERB_ALIASES = {
321
330
  upgrade: "install-upgrade",
322
331
  "session:start": "session-start",
323
332
  "session:ready": "session-ready",
333
+ ...FRESHNESS_COLON_ALIASES,
324
334
  "lifecycle:event": "lifecycle-event",
325
335
  "lifecycle:stats": "lifecycle-stats",
326
336
  "toolchain:check": "toolchain-check",
@@ -2445,6 +2455,10 @@ async function loadCoreModuleHandler(verb, io) {
2445
2455
  const { mainEntry } = await import("@deftai/directive-core/dist/product-signal/submit.js");
2446
2456
  return mainEntry;
2447
2457
  }
2458
+ case "freshness-report": {
2459
+ const { mainEntry } = await import("@deftai/directive-core/dist/freshness/cli.js");
2460
+ return mainEntry;
2461
+ }
2448
2462
  default:
2449
2463
  throw new Error(`unknown core verb: ${verb}`);
2450
2464
  }
@@ -2547,6 +2561,14 @@ const CURATED_HELP_GROUPS = [
2547
2561
  name: "session:ready",
2548
2562
  summary: "One-shot recovery to gated write-ready (session + ritual + cache)",
2549
2563
  },
2564
+ {
2565
+ name: "freshness:report",
2566
+ summary: "Bound vs live deposit generation (current|stale_soft|stale_hard)",
2567
+ },
2568
+ {
2569
+ name: "freshness:bind",
2570
+ summary: "Bind live deposit generation into this session (no host restart)",
2571
+ },
2550
2572
  {
2551
2573
  name: "scm:status",
2552
2574
  summary: "Probe gh/ghx + auth readiness in this execution env (#2275)",
@@ -2683,6 +2705,7 @@ export async function dispatch(argv, io = defaultIo()) {
2683
2705
  const escalationSubcommand = verb !== undefined ? ESCALATION_ACTION_ALIAS_SUBCOMMANDS[verb] : undefined;
2684
2706
  const planSequenceSubcommand = verb !== undefined ? PLAN_SEQUENCE_ALIAS_SUBCOMMANDS[verb] : undefined;
2685
2707
  const productSignalSubcommand = verb !== undefined ? PRODUCT_SIGNAL_ALIAS_SUBCOMMANDS[verb] : undefined;
2708
+ const freshnessSubcommand = verb !== undefined ? FRESHNESS_ALIAS_SUBCOMMANDS[verb] : undefined;
2686
2709
  const handlerArgv = canonical === "framework-commands" && verb !== undefined && verb !== canonical
2687
2710
  ? [verb, ...rest]
2688
2711
  : triageSubcommand !== undefined && canonical === "triage-actions"
@@ -2697,7 +2720,9 @@ export async function dispatch(argv, io = defaultIo()) {
2697
2720
  ? [planSequenceSubcommand, ...rest]
2698
2721
  : productSignalSubcommand !== undefined && canonical === "product-signal"
2699
2722
  ? [productSignalSubcommand, ...rest]
2700
- : rest;
2723
+ : freshnessSubcommand !== undefined && canonical === "freshness-report"
2724
+ ? [freshnessSubcommand, ...rest]
2725
+ : rest;
2701
2726
  return await invokeHandler(handler, handlerArgv);
2702
2727
  }
2703
2728
  catch (err) {
@@ -1,13 +1,22 @@
1
1
  #!/usr/bin/env node
2
- interface ParsedArgs {
2
+ import type { LabelClient } from "@deftai/directive-core/dist/vbrief-reconcile/types.js";
3
+ export interface ParsedArgs {
3
4
  projectRoot: string;
4
5
  doList: boolean;
5
6
  doValidate: boolean;
7
+ doMirror: boolean;
8
+ apply: boolean;
9
+ json: boolean;
10
+ repo: string | null;
11
+ allowCrossRepo: boolean;
6
12
  error?: string;
7
13
  }
8
- /** Parse triage-classify CLI args, mirroring the Python argparse surface. */
14
+ /** Parse triage-classify CLI args (#1129 + #1423 Wave 1 mirror flags). */
9
15
  export declare function parseArgs(argv: string[]): ParsedArgs;
16
+ export interface RunOptions {
17
+ /** Injected LabelClient for tests (apply path). */
18
+ readonly labelClient?: LabelClient;
19
+ }
10
20
  /** Run the CLI and return the process exit code. */
11
- export declare function run(argv: string[]): number;
12
- export {};
21
+ export declare function run(argv: string[], options?: RunOptions): number;
13
22
  //# sourceMappingURL=triage-classify.d.ts.map
@@ -2,13 +2,18 @@
2
2
  import { statSync } from "node:fs";
3
3
  import { resolve } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
- import { listProject, validateProject } from "@deftai/directive-core/dist/triage/classify/index.js";
6
- /** Parse triage-classify CLI args, mirroring the Python argparse surface. */
5
+ import { labelMirrorOutcomeToJson, listProject, mirrorLabels, renderLabelMirrorReport, validateProject, } from "@deftai/directive-core/dist/triage/classify/index.js";
6
+ /** Parse triage-classify CLI args (#1129 + #1423 Wave 1 mirror flags). */
7
7
  export function parseArgs(argv) {
8
8
  const parsed = {
9
9
  projectRoot: ".",
10
10
  doList: false,
11
11
  doValidate: false,
12
+ doMirror: false,
13
+ apply: false,
14
+ json: false,
15
+ repo: null,
16
+ allowCrossRepo: false,
12
17
  };
13
18
  for (let i = 0; i < argv.length; i += 1) {
14
19
  const arg = argv[i];
@@ -18,6 +23,29 @@ export function parseArgs(argv) {
18
23
  else if (arg === "--validate") {
19
24
  parsed.doValidate = true;
20
25
  }
26
+ else if (arg === "--mirror") {
27
+ parsed.doMirror = true;
28
+ }
29
+ else if (arg === "--apply") {
30
+ parsed.apply = true;
31
+ }
32
+ else if (arg === "--json") {
33
+ parsed.json = true;
34
+ }
35
+ else if (arg === "--allow-cross-repo") {
36
+ parsed.allowCrossRepo = true;
37
+ }
38
+ else if (arg === "--repo") {
39
+ const value = argv[i + 1];
40
+ if (value === undefined) {
41
+ return { ...parsed, error: "argument --repo: expected one argument" };
42
+ }
43
+ parsed.repo = value;
44
+ i += 1;
45
+ }
46
+ else if (arg?.startsWith("--repo=")) {
47
+ parsed.repo = arg.slice("--repo=".length);
48
+ }
21
49
  else if (arg === "--project-root") {
22
50
  const value = argv[i + 1];
23
51
  if (value === undefined) {
@@ -36,10 +64,16 @@ export function parseArgs(argv) {
36
64
  return { ...parsed, error: `unrecognized arguments: ${arg}` };
37
65
  }
38
66
  }
67
+ if (parsed.apply && !parsed.doMirror) {
68
+ return {
69
+ ...parsed,
70
+ error: "--apply requires --mirror (Tier-1 label mirror, #1423)",
71
+ };
72
+ }
39
73
  return parsed;
40
74
  }
41
75
  /** Run the CLI and return the process exit code. */
42
- export function run(argv) {
76
+ export function run(argv, options = {}) {
43
77
  const args = parseArgs(argv);
44
78
  if (args.error !== undefined) {
45
79
  process.stderr.write(`ERR: ${args.error}\n`);
@@ -67,6 +101,23 @@ export function run(argv) {
67
101
  }
68
102
  return result.code;
69
103
  }
104
+ if (args.doMirror) {
105
+ const mirrorOpts = {
106
+ dryRun: !args.apply,
107
+ repo: args.repo,
108
+ allowCrossRepo: args.allowCrossRepo,
109
+ ...(options.labelClient !== undefined ? { client: options.labelClient } : {}),
110
+ };
111
+ const [code, outcome] = mirrorLabels(projectRoot, mirrorOpts);
112
+ if (args.json) {
113
+ process.stdout.write(`${JSON.stringify(labelMirrorOutcomeToJson(outcome), null, 2)}\n`);
114
+ }
115
+ else {
116
+ process.stdout.write(renderLabelMirrorReport(outcome));
117
+ }
118
+ return code;
119
+ }
120
+ // Default / --list: print effective rules
70
121
  process.stdout.write(listProject(projectRoot));
71
122
  return 0;
72
123
  }
@@ -1,12 +1,22 @@
1
1
  #!/usr/bin/env node
2
+ import { type AgentHookHealthResult, type AgentHookReadinessResult, type EvaluateResult } from "@deftai/directive-core/verify-env";
2
3
  type HookScope = "git" | "agent" | "all";
3
4
  interface ParsedArgs {
4
5
  projectRoot: string;
5
6
  quiet: boolean;
6
7
  scope: HookScope;
8
+ live: boolean;
7
9
  error?: string;
8
10
  }
11
+ export interface VerifyHooksInstalledCliSeams {
12
+ readonly evaluateGit?: (projectRoot: string) => EvaluateResult;
13
+ readonly evaluateAgent?: (projectRoot: string) => AgentHookHealthResult;
14
+ readonly evaluateReadiness?: (projectRoot: string) => AgentHookReadinessResult;
15
+ readonly writeOut?: (text: string) => void;
16
+ readonly writeErr?: (text: string) => void;
17
+ }
9
18
  export declare function parseArgs(argv: string[]): ParsedArgs;
10
- export declare function run(argv: string[]): number;
19
+ /** Verify git hooks, structural agent hooks, or functional agent-hook readiness. */
20
+ export declare function run(argv: string[], seams?: VerifyHooksInstalledCliSeams): number;
11
21
  export {};
12
22
  //# sourceMappingURL=verify-hooks-installed.d.ts.map
@@ -1,13 +1,15 @@
1
1
  #!/usr/bin/env node
2
2
  import { resolve } from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
- import { evaluate, evaluateAgentHooks } from "@deftai/directive-core/verify-env";
4
+ import { evaluate, evaluateAgentHookReadinessSafely, evaluateAgentHooks, } from "@deftai/directive-core/verify-env";
5
5
  export function parseArgs(argv) {
6
- const parsed = { projectRoot: ".", quiet: false, scope: "git" };
6
+ const parsed = { projectRoot: ".", quiet: false, scope: "git", live: false };
7
7
  for (let i = 0; i < argv.length; i += 1) {
8
8
  const arg = argv[i];
9
9
  if (arg === "--quiet")
10
10
  parsed.quiet = true;
11
+ else if (arg === "--live")
12
+ parsed.live = true;
11
13
  else if (arg === "--project-root") {
12
14
  const value = argv[i + 1];
13
15
  if (value === undefined) {
@@ -41,26 +43,39 @@ export function parseArgs(argv) {
41
43
  return { ...parsed, error: `unrecognized arguments: ${arg}` };
42
44
  }
43
45
  }
46
+ if (parsed.live && parsed.scope === "git") {
47
+ return { ...parsed, error: "argument --live requires --scope=agent or --scope=all" };
48
+ }
44
49
  return parsed;
45
50
  }
46
- export function run(argv) {
51
+ /** Verify git hooks, structural agent hooks, or functional agent-hook readiness. */
52
+ export function run(argv, seams = {}) {
47
53
  const args = parseArgs(argv);
54
+ const writeOut = seams.writeOut ?? ((text) => process.stdout.write(text));
55
+ const writeErr = seams.writeErr ?? ((text) => process.stderr.write(text));
48
56
  if (args.error !== undefined) {
49
- process.stderr.write(`${args.error}\n`);
57
+ writeErr(`${args.error}\n`);
50
58
  return 2;
51
59
  }
52
60
  const projectRoot = resolve(args.projectRoot);
61
+ const evaluateGit = seams.evaluateGit ?? evaluate;
62
+ const evaluateAgent = seams.evaluateAgent ?? evaluateAgentHooks;
63
+ const evaluateReadiness = seams.evaluateReadiness
64
+ ? (root) => evaluateAgentHookReadinessSafely(root, seams.evaluateReadiness)
65
+ : evaluateAgentHookReadinessSafely;
53
66
  const results = [
54
- ...(args.scope === "git" || args.scope === "all" ? [evaluate(projectRoot)] : []),
55
- ...(args.scope === "agent" || args.scope === "all" ? [evaluateAgentHooks(projectRoot)] : []),
67
+ ...(args.scope === "git" || args.scope === "all" ? [evaluateGit(projectRoot)] : []),
68
+ ...(args.scope === "agent" || args.scope === "all"
69
+ ? [args.live ? evaluateReadiness(projectRoot) : evaluateAgent(projectRoot)]
70
+ : []),
56
71
  ];
57
72
  if (!args.quiet) {
58
73
  for (const result of results) {
59
74
  if (result.stream === "stdout") {
60
- process.stdout.write(`${result.message}\n`);
75
+ writeOut(`${result.message}\n`);
61
76
  }
62
77
  else if (result.stream === "stderr") {
63
- process.stderr.write(`${result.message}\n`);
78
+ writeErr(`${result.message}\n`);
64
79
  }
65
80
  }
66
81
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deftai/directive",
3
- "version": "0.94.0",
3
+ "version": "0.95.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.94.0",
38
- "@deftai/directive-content": "^0.94.0"
37
+ "@deftai/directive-core": "^0.95.0",
38
+ "@deftai/directive-content": "^0.95.0"
39
39
  },
40
40
  "scripts": {
41
41
  "build": "tsc -b"