@deftai/directive 0.119.8 → 0.119.9

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.
@@ -29,6 +29,7 @@ export const SCOPE_LIFECYCLE_VERBS = new Set([
29
29
  "block",
30
30
  "unblock",
31
31
  "stamp-evidence",
32
+ "bind-clause",
32
33
  ]);
33
34
  /** pr:* task names that do not hyphenate 1:1 to handler stems. */
34
35
  export const PR_VERB_MAP = {
@@ -82,6 +83,8 @@ export const SUBCOMMAND_ROUTES = {
82
83
  "policy:enable-directive": ["policy", "enable-directive"],
83
84
  "policy:enforce-branches": ["policy", "enforce-branches"],
84
85
  "policy:allow-direct-commits": ["policy", "allow-direct-commits"],
86
+ "policy:allow-destructive-gh-verbs": ["policy", "allow-destructive-gh-verbs"],
87
+ "policy:enforce-destructive-gh-verbs": ["policy", "enforce-destructive-gh-verbs"],
85
88
  "policy:allow-bot-merge": ["policy", "allow-bot-merge"],
86
89
  "policy:enable-value-feedback": ["policy", "enable-value-feedback"],
87
90
  "policy:disable-host-hooks": ["policy", "disable-host-hooks"],
@@ -207,6 +210,9 @@ function routeNamespaceVerb(ns, verb, rest) {
207
210
  if (ns === "scm" && verb === "issue:design-critique-chip") {
208
211
  return { kind: "dispatch", argv: ["scm", "issue", "design-critique-chip", ...rest] };
209
212
  }
213
+ if (ns === "scm" && verb === "issue:design-critique-stale-ready") {
214
+ return { kind: "dispatch", argv: ["scm", "issue", "design-critique-stale-ready", ...rest] };
215
+ }
210
216
  if (ns === "scm" && verb === "issue:work-claim") {
211
217
  return { kind: "dispatch", argv: ["scm", "issue", "work-claim", ...rest] };
212
218
  }
@@ -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", "release-wait-npm", "scope-lifecycle", "scope-record-approved-scope", "scope-record-observable-scope", "scope-record-intent-constraint", "scm-sync-default", "lifecycle-event", "lifecycle-stats", "session-start", "session-ready", "occupancy-steal", "occupancy-release", "occupancy-heartbeat", "occupancy-grant", "plan-sequence", "slice", "subagent-monitor", "toolchain-check", "triage-actions", "triage-bootstrap", "triage-bulk", "triage-classify", "triage-strip-withdrawn", "triage-evaluate", "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-telemetry-coverage", "verify-content-manifest", "verify-deposit-closure", "verify-consumer-hard-stops", "verify-closing-keywords", "verify-skill-external-fetch-gate", "verify-semantic-single-source", "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", "verify-spec-prd-fresh", "rule-ownership-lint", "verify-story-ready", "verify-review-monitor", "verify-l4-owner", "verify-subagent-alive", "verify-subagent-steer", "review-monitor-register", "review-monitor-release", "verify-tools", "verify-wip-cap", "verify-orphan-active", "verify-pr-closeout-attestable", "verify-lifecycle-visible", "verify-completed-tracked", "verify-completed-write-guard", "verify-literal-ac", "verify-ac", "verify-agents-md-budget", "verify-agents-md-advisory", "verify-eval-health-relocation", "verify-eval-triggers-relocation", "verify-evaluator-surface", "verify-observable-scope", "verify-intent-constraint", "verify-consumer-test-lane", "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", "release-wait-npm", "scope-lifecycle", "scope-record-approved-scope", "scope-record-observable-scope", "scope-record-intent-constraint", "scm-sync-default", "lifecycle-event", "lifecycle-stats", "session-start", "session-ready", "occupancy-steal", "occupancy-release", "occupancy-heartbeat", "occupancy-grant", "plan-sequence", "slice", "subagent-monitor", "toolchain-check", "triage-actions", "triage-bootstrap", "triage-bulk", "triage-classify", "triage-strip-withdrawn", "triage-evaluate", "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", "xbrief-adopt-stored-plan-id", "verify-branch", "verify-encoding", "verify-forward-coverage", "verify-test-boundary", "verify-class-checks", "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-telemetry-coverage", "verify-content-manifest", "verify-deposit-closure", "verify-consumer-hard-stops", "verify-closing-keywords", "verify-skill-external-fetch-gate", "verify-semantic-single-source", "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", "verify-spec-prd-fresh", "rule-ownership-lint", "verify-story-ready", "verify-review-monitor", "verify-l4-owner", "verify-subagent-alive", "verify-subagent-steer", "review-monitor-register", "review-monitor-release", "verify-tools", "verify-wip-cap", "verify-orphan-active", "verify-pr-closeout-attestable", "verify-lifecycle-visible", "verify-completed-tracked", "verify-completed-write-guard", "verify-literal-ac", "verify-ac", "verify-agents-md-budget", "verify-agents-md-advisory", "verify-eval-health-relocation", "verify-eval-triggers-relocation", "verify-evaluator-surface", "verify-observable-scope", "verify-intent-constraint", "verify-consumer-test-lane", "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", "project-write-narratives", "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", "docs-impact"];
16
16
  /** Colon aliases for triage-actions (mirrors cli-router SUBCOMMAND_ROUTES). */
package/dist/dispatch.js CHANGED
@@ -111,10 +111,12 @@ export const CLI_MODULE_VERBS = [
111
111
  "vbrief-validation",
112
112
  "xbrief-create",
113
113
  "xbrief-verify",
114
+ "xbrief-adopt-stored-plan-id",
114
115
  "verify-branch",
115
116
  "verify-encoding",
116
117
  "verify-forward-coverage",
117
118
  "verify-test-boundary",
119
+ "verify-class-checks",
118
120
  "verify-scope-provenance",
119
121
  "verify-consumer-check-contract",
120
122
  "verify-hooks-installed",
@@ -242,6 +244,8 @@ export const POLICY_ACTION_ALIAS_SUBCOMMANDS = {
242
244
  "policy:show": "show",
243
245
  "policy:enforce-branches": "enforce-branches",
244
246
  "policy:allow-direct-commits": "allow-direct-commits",
247
+ "policy:allow-destructive-gh-verbs": "allow-destructive-gh-verbs",
248
+ "policy:enforce-destructive-gh-verbs": "enforce-destructive-gh-verbs",
245
249
  "policy:allow-bot-merge": "allow-bot-merge",
246
250
  "policy:enable-value-feedback": "enable-value-feedback",
247
251
  "policy:disable-host-hooks": "disable-host-hooks",
@@ -299,6 +303,7 @@ export const VERB_ALIASES = {
299
303
  "verify:encoding": "verify-encoding",
300
304
  "verify:forward-coverage": "verify-forward-coverage",
301
305
  "verify:test-boundary": "verify-test-boundary",
306
+ "verify:class-checks": "verify-class-checks",
302
307
  "verify:scope-provenance": "verify-scope-provenance",
303
308
  "scope:record-approved-scope": "scope-record-approved-scope",
304
309
  "scope:record-observable-scope": "scope-record-observable-scope",
@@ -337,6 +342,7 @@ export const VERB_ALIASES = {
337
342
  "xbrief:preflight": "vbrief-preflight",
338
343
  "xbrief:create": "xbrief-create",
339
344
  "xbrief:verify": "xbrief-verify",
345
+ "xbrief:adopt-stored-plan-id": "xbrief-adopt-stored-plan-id",
340
346
  "vbrief:activate": "vbrief-activate",
341
347
  "verify:story-ready": "verify-story-ready",
342
348
  "verify:review-monitor": "verify-review-monitor",
@@ -478,6 +484,7 @@ const WRAPPER_CLI_STEMS = new Set([
478
484
  "vbrief-validation",
479
485
  "xbrief-create",
480
486
  "xbrief-verify",
487
+ "xbrief-adopt-stored-plan-id",
481
488
  ]);
482
489
  function emitCliResult(result, io) {
483
490
  if (result.stdout)
@@ -553,6 +560,10 @@ async function loadWrapperCliHandler(stem, io) {
553
560
  const { runXbriefVerifyCli } = await import("@deftai/directive-core/xbrief");
554
561
  return (argv) => emitCliResult(runXbriefVerifyCli(argv), io);
555
562
  }
563
+ case "xbrief-adopt-stored-plan-id": {
564
+ const { runAdoptStoredPlanIdCli } = await import("@deftai/directive-core/xbrief");
565
+ return (argv) => emitCliResult(runAdoptStoredPlanIdCli(argv), io);
566
+ }
556
567
  default:
557
568
  throw new Error(`no wrapper handler for ${stem}`);
558
569
  }
@@ -2771,6 +2782,10 @@ const CURATED_HELP_GROUPS = [
2771
2782
  name: "xbrief:verify",
2772
2783
  summary: "Verify a dense xBRIEF artifact at --out (requires --format); not scope:*",
2773
2784
  },
2785
+ {
2786
+ name: "xbrief:adopt-stored-plan-id",
2787
+ summary: "Set plan.id from the stored plan-id binding; refuse if another artifact has it",
2788
+ },
2774
2789
  ],
2775
2790
  },
2776
2791
  {
package/dist/policy.d.ts CHANGED
@@ -8,7 +8,7 @@ interface ShowArgs {
8
8
  error?: string;
9
9
  }
10
10
  interface SetArgs {
11
- cmd: "show" | "enforce-branches" | "allow-direct-commits" | "allow-bot-merge" | "enable-value-feedback" | "clear-value-feedback" | "set-ceremony-dial" | "disable-host-hooks" | "disable-directive" | "enable-directive" | "resolve";
11
+ cmd: "show" | "enforce-branches" | "allow-direct-commits" | "allow-destructive-gh-verbs" | "enforce-destructive-gh-verbs" | "allow-bot-merge" | "enable-value-feedback" | "clear-value-feedback" | "set-ceremony-dial" | "disable-host-hooks" | "disable-directive" | "enable-directive" | "resolve";
12
12
  confirm: boolean;
13
13
  actor: string;
14
14
  note: string;
package/dist/policy.js CHANGED
@@ -6,7 +6,7 @@
6
6
  import { existsSync } from "node:fs";
7
7
  import { resolve as pathResolve, relative } from "node:path";
8
8
  import { fileURLToPath } from "node:url";
9
- import { ALLOW_BOT_MERGE_CAPABILITY_COST, CEREMONY_DEPTHS, clearValueFeedback, createNoDeftDirectiveFlag, describeShadowedPlanExtension, detectNoDeftDirective, detectShadowedPlanExtensions, disableHostHooks, disclosureLine, enableValueFeedback, FIELD_VALUE_FEEDBACK, FIELD_VALUE_FEEDBACK_CLI_ALIAS, formatValueFeedbackStatusLine, humanMergeDisclosureLine, inspectAllPolicies, inspectOnePolicy, loadProjectDefinition, NO_DEFT_DIRECTIVE_DISABLED_MESSAGE, NO_DEFT_DIRECTIVE_FLAG_NAME, NO_DEFT_DIRECTIVE_INCONSISTENT_MESSAGE, POLICY_AUDIT_NOOP_STDOUT, parseHookHost, policyColonInvocation, projectDefinitionPath, pythonListRepr, pythonStringRepr, registeredPolicyNames, removeNoDeftDirectiveFlag, renderJson, renderText, resolveHumanMergePolicy, resolvePolicy, resolveValueFeedback, setCeremonyDial, setPolicy, setRequireHumanMerge, } from "@deftai/directive-core/policy";
9
+ import { ALLOW_BOT_MERGE_CAPABILITY_COST, ALLOW_DESTRUCTIVE_GH_VERBS_CAPABILITY_COST, CEREMONY_DEPTHS, clearValueFeedback, createNoDeftDirectiveFlag, describeShadowedPlanExtension, detectNoDeftDirective, detectShadowedPlanExtensions, disableHostHooks, disclosureLine, enableValueFeedback, FIELD_VALUE_FEEDBACK, FIELD_VALUE_FEEDBACK_CLI_ALIAS, formatValueFeedbackStatusLine, humanMergeDisclosureLine, inspectAllPolicies, inspectOnePolicy, loadProjectDefinition, NO_DEFT_DIRECTIVE_DISABLED_MESSAGE, NO_DEFT_DIRECTIVE_FLAG_NAME, NO_DEFT_DIRECTIVE_INCONSISTENT_MESSAGE, POLICY_AUDIT_NOOP_STDOUT, parseHookHost, policyColonInvocation, projectDefinitionPath, pythonListRepr, pythonStringRepr, registeredPolicyNames, removeNoDeftDirectiveFlag, renderJson, renderText, resolveHumanMergePolicy, resolvePolicy, resolveValueFeedback, setAllowDestructiveGhVerbs, setCeremonyDial, setPolicy, setRequireHumanMerge, } from "@deftai/directive-core/policy";
10
10
  const CAPABILITY_COST_DISCLOSURE = "\u26a0 Capability-cost disclosure -- enabling direct commits to the default " +
11
11
  "branch turns OFF the deft branch-protection policy.\n" +
12
12
  " \u2022 Pre-commit + pre-push hooks will no longer block default-branch " +
@@ -114,7 +114,7 @@ export function parseShowArgs(argv) {
114
114
  /** Parse argv for the policy CLI (show + set subcommands). */
115
115
  export function parseArgs(argv) {
116
116
  if (argv.length === 0) {
117
- const usage = "usage: policy [show|enforce-branches|allow-direct-commits|allow-bot-merge|enable-value-feedback|clear-value-feedback|set-ceremony-dial|disable-host-hooks|disable-directive|enable-directive|resolve] ...";
117
+ const usage = "usage: policy [show|enforce-branches|allow-direct-commits|allow-destructive-gh-verbs|enforce-destructive-gh-verbs|allow-bot-merge|enable-value-feedback|clear-value-feedback|set-ceremony-dial|disable-host-hooks|disable-directive|enable-directive|resolve] ...";
118
118
  return makeSetError(usage);
119
119
  }
120
120
  const cmd = argv[0];
@@ -148,6 +148,8 @@ export function parseArgs(argv) {
148
148
  }
149
149
  if (cmd === "enforce-branches" ||
150
150
  cmd === "allow-direct-commits" ||
151
+ cmd === "allow-destructive-gh-verbs" ||
152
+ cmd === "enforce-destructive-gh-verbs" ||
151
153
  cmd === "allow-bot-merge" ||
152
154
  cmd === "enable-value-feedback" ||
153
155
  cmd === "clear-value-feedback" ||
@@ -160,19 +162,23 @@ export function parseArgs(argv) {
160
162
  ? policyColonInvocation("enforce-branches")
161
163
  : cmd === "allow-direct-commits"
162
164
  ? policyColonInvocation("allow-direct-commits")
163
- : cmd === "allow-bot-merge"
164
- ? policyColonInvocation("allow-bot-merge")
165
- : cmd === "enable-value-feedback"
166
- ? policyColonInvocation("enable-value-feedback")
167
- : cmd === "clear-value-feedback"
168
- ? policyColonInvocation("clear-value-feedback")
169
- : cmd === "set-ceremony-dial"
170
- ? policyColonInvocation("set-ceremony-dial")
171
- : cmd === "disable-host-hooks"
172
- ? policyColonInvocation("disable-host-hooks")
173
- : cmd === "disable-directive"
174
- ? policyColonInvocation("disable-directive")
175
- : policyColonInvocation("enable-directive");
165
+ : cmd === "allow-destructive-gh-verbs"
166
+ ? policyColonInvocation("allow-destructive-gh-verbs")
167
+ : cmd === "enforce-destructive-gh-verbs"
168
+ ? policyColonInvocation("enforce-destructive-gh-verbs")
169
+ : cmd === "allow-bot-merge"
170
+ ? policyColonInvocation("allow-bot-merge")
171
+ : cmd === "enable-value-feedback"
172
+ ? policyColonInvocation("enable-value-feedback")
173
+ : cmd === "clear-value-feedback"
174
+ ? policyColonInvocation("clear-value-feedback")
175
+ : cmd === "set-ceremony-dial"
176
+ ? policyColonInvocation("set-ceremony-dial")
177
+ : cmd === "disable-host-hooks"
178
+ ? policyColonInvocation("disable-host-hooks")
179
+ : cmd === "disable-directive"
180
+ ? policyColonInvocation("disable-directive")
181
+ : policyColonInvocation("enable-directive");
176
182
  let note = "";
177
183
  let projectRoot = ".";
178
184
  let host;
@@ -476,6 +482,51 @@ function runEnableDirective(args) {
476
482
  process.stdout.write("Directive opt-out cleared. Run `directive init` or `directive update` to ensure install.\n");
477
483
  return 0;
478
484
  }
485
+ /** Allow or re-enforce the #1019 destructive-gh-verb gate. */
486
+ function runDestructiveGhVerbs(args) {
487
+ const projectRoot = pathResolve(args.projectRoot);
488
+ const allow = args.cmd === "allow-destructive-gh-verbs";
489
+ if (allow && !args.confirm) {
490
+ process.stdout.write(`${ALLOW_DESTRUCTIVE_GH_VERBS_CAPABILITY_COST}\n\n`);
491
+ process.stdout.write(`Re-run with --confirm to apply: ${policyColonInvocation("allow-destructive-gh-verbs", " -- --confirm")}\n`);
492
+ return 1;
493
+ }
494
+ try {
495
+ const result = setAllowDestructiveGhVerbs(projectRoot, {
496
+ allowDestructiveGhVerbs: allow,
497
+ actor: args.actor,
498
+ note: args.note,
499
+ });
500
+ if (!result.ok) {
501
+ process.stderr.write(`\u274c ${result.message}\n`);
502
+ if (result.message.includes("PROJECT-DEFINITION not found")) {
503
+ const pdRel = relative(projectRoot, projectDefinitionPath(projectRoot));
504
+ process.stderr.write(` Recovery: run \`task setup\` to generate ${pdRel}.\n`);
505
+ }
506
+ return 2;
507
+ }
508
+ process.stdout.write(`\u2713 plan.policy.allowDestructiveGhVerbs=${allow ? "true" : "false"} ` +
509
+ `(#1019 gate ${allow ? "OFF" : "ON"}).\n`);
510
+ if (result.changed) {
511
+ process.stdout.write(` audit: meta/policy-changes.log :: ${result.auditEntry}\n`);
512
+ }
513
+ else {
514
+ process.stdout.write(`${POLICY_AUDIT_NOOP_STDOUT}\n`);
515
+ }
516
+ return 0;
517
+ }
518
+ catch (err) {
519
+ const message = err instanceof Error ? err.message : String(err);
520
+ if (message.includes("PROJECT-DEFINITION not found")) {
521
+ process.stderr.write(`\u274c ${message}\n`);
522
+ const pdRel = relative(projectRoot, projectDefinitionPath(projectRoot));
523
+ process.stderr.write(` Recovery: run \`task setup\` to generate ${pdRel}.\n`);
524
+ return 2;
525
+ }
526
+ process.stderr.write(`\u274c Config error: ${message}\n`);
527
+ return 2;
528
+ }
529
+ }
479
530
  /** Allow agent/bot merge by writing requireHumanMerge=false (#1193). */
480
531
  function runAllowBotMerge(args) {
481
532
  const projectRoot = pathResolve(args.projectRoot);
@@ -539,6 +590,9 @@ export function run(argv) {
539
590
  if (args.cmd === "enforce-branches" || args.cmd === "allow-direct-commits") {
540
591
  return runSet(args);
541
592
  }
593
+ if (args.cmd === "allow-destructive-gh-verbs" || args.cmd === "enforce-destructive-gh-verbs") {
594
+ return runDestructiveGhVerbs(args);
595
+ }
542
596
  if (args.cmd === "allow-bot-merge") {
543
597
  return runAllowBotMerge(args);
544
598
  }
@@ -3,6 +3,7 @@ interface ParsedArgs {
3
3
  mode?: "self-test" | "command" | "pre-push-stdin";
4
4
  command?: string;
5
5
  defaultBranches?: Set<string>;
6
+ projectRoot?: string;
6
7
  quiet?: boolean;
7
8
  error?: string;
8
9
  }
@@ -7,11 +7,13 @@
7
7
  * deft-ts preflight-gh --command "<gh ...>"
8
8
  * deft-ts preflight-gh --pre-push-stdin (reads from stdin)
9
9
  *
10
- * Thin shim -- delegates to @deftai/directive-core/preflight-gh.
10
+ * Thin shim -- delegates through @deftai/directive-core/preflight
11
+ * (re-exports the preflight-gh API; vitest already aliases that subpath).
12
+ * --project-root is consulted by evaluatePrePush / evaluateCommand (#4384).
11
13
  */
12
14
  import { createInterface } from "node:readline";
13
15
  import { fileURLToPath } from "node:url";
14
- import { DEFAULT_BRANCHES, ENV_BYPASS, evaluateCommand, runSelfTest, } from "@deftai/directive-core/preflight-gh";
16
+ import { DEFAULT_BRANCHES, evaluateCommand, evaluatePrePush, parsePrePushStdin, runSelfTest, } from "@deftai/directive-core/preflight";
15
17
  export function parseArgs(argv) {
16
18
  const parsed = { defaultBranches: new Set(DEFAULT_BRANCHES) };
17
19
  for (let i = 0; i < argv.length; i++) {
@@ -44,7 +46,11 @@ export function parseArgs(argv) {
44
46
  i++;
45
47
  }
46
48
  else if (arg === "--project-root") {
47
- // accepted for parity with preflight_branch.py; ignored here
49
+ const next = argv[i + 1];
50
+ if (next === undefined) {
51
+ return { ...parsed, error: "argument --project-root: expected one argument" };
52
+ }
53
+ parsed.projectRoot = next;
48
54
  i++;
49
55
  }
50
56
  else {
@@ -73,7 +79,9 @@ export function run(argv) {
73
79
  return code;
74
80
  }
75
81
  if (args.mode === "command" && args.command !== undefined) {
76
- const [code, msg] = evaluateCommand(args.command, branches);
82
+ const [code, msg] = evaluateCommand(args.command, branches, {
83
+ projectRoot: args.projectRoot,
84
+ });
77
85
  if (code === 0) {
78
86
  if (!quiet)
79
87
  process.stdout.write(`${msg}\n`);
@@ -84,85 +92,19 @@ export function run(argv) {
84
92
  return code;
85
93
  }
86
94
  if (args.mode === "pre-push-stdin") {
87
- return runPrePushStdin(branches, quiet);
95
+ return runPrePushStdin(branches, quiet, args.projectRoot);
88
96
  }
89
97
  process.stderr.write("preflight-gh: one of --self-test / --command / --pre-push-stdin required\n");
90
98
  return 2;
91
99
  }
92
- function parsePrePushLines(text) {
93
- return text
94
- .split("\n")
95
- .map((l) => l.trim())
96
- .filter((l) => l.length > 0)
97
- .flatMap((l) => {
98
- const parts = l.split(/\s+/);
99
- if (parts.length !== 4)
100
- return [];
101
- return [
102
- {
103
- localRef: parts[0] ?? "",
104
- localOid: parts[1] ?? "",
105
- remoteRef: parts[2] ?? "",
106
- remoteOid: parts[3] ?? "",
107
- },
108
- ];
109
- });
110
- }
111
- const ZERO_OID_RE = /^0+$/;
112
- function evaluatePrePush(refs, branches) {
113
- if (refs.length === 0) {
114
- return [0, "✓ deft destructive-gh-verb gate (pre-push): no refs in stdin -- nothing to gate."];
115
- }
116
- const branchesLower = new Set([...branches].map((b) => b.toLowerCase()));
117
- const blocked = [];
118
- for (const { localRef, localOid, remoteRef, remoteOid } of refs) {
119
- const branch = remoteRef.replace(/^refs\/heads\//, "");
120
- if (!branchesLower.has(branch.toLowerCase()))
121
- continue;
122
- if (ZERO_OID_RE.test(remoteOid)) {
123
- blocked.push(`create ${branch} (local=${localRef})`);
124
- }
125
- else if (ZERO_OID_RE.test(localOid)) {
126
- blocked.push(`delete ${branch}`);
127
- }
128
- else {
129
- blocked.push(`update ${branch} (local=${localRef})`);
130
- }
131
- }
132
- if (blocked.length === 0) {
133
- return [
134
- 0,
135
- "✓ deft destructive-gh-verb gate (pre-push): no pushes to default branches detected -- proceeding.",
136
- ];
137
- }
138
- const bypass = (process.env[ENV_BYPASS] ?? "").trim().toLowerCase();
139
- if (["1", "true", "yes", "on"].includes(bypass)) {
140
- return [
141
- 0,
142
- `⚠ deft destructive-gh-verb gate (pre-push): default-branch push detected (${blocked.join("; ")}) but ${ENV_BYPASS}=1 is set -- policy bypassed for this invocation.`,
143
- ];
144
- }
145
- return [
146
- 1,
147
- [
148
- "❌ deft destructive-gh-verb gate (pre-push): refusing to push directly to the default branch.",
149
- ` Detail: ${blocked.join("; ")}`,
150
- "",
151
- " How to proceed:",
152
- " • push to a feature branch and open a PR",
153
- ` • or set the env-var bypass for this shell: ${ENV_BYPASS}=1`,
154
- " See scm/github.md (## Destructive gh verbs (#1019)).",
155
- ].join("\n"),
156
- ];
157
- }
158
- function runPrePushStdin(branches, quiet) {
100
+ function runPrePushStdin(branches, quiet, projectRoot) {
159
101
  return new Promise((resolve) => {
160
102
  const lines = [];
161
103
  const rl = createInterface({ input: process.stdin });
162
104
  rl.on("line", (l) => lines.push(l));
163
105
  rl.on("close", () => {
164
- const refs = parsePrePushLines(lines.join("\n"));
165
- const [code, msg] = evaluatePrePush(refs, branches);
106
+ const refs = parsePrePushStdin(lines.join("\n"));
107
+ const [code, msg] = evaluatePrePush(refs, { branches, projectRoot });
166
108
  if (code === 0) {
167
109
  if (!quiet)
168
110
  process.stdout.write(`${msg}\n`);
@@ -5,6 +5,7 @@ export interface ParsedArgs {
5
5
  xbriefPath: string;
6
6
  actor: string;
7
7
  kind: string;
8
+ mintedVia: string;
8
9
  quiet: boolean;
9
10
  confirm: boolean;
10
11
  help?: boolean;
@@ -11,12 +11,18 @@ import { buildIntentConstraintRecord, extractIntentConstraintFromPlan, parseInte
11
11
  import { isDirectEntrypoint } from "./entrypoint.js";
12
12
  import { refuseMintWhileUatActive, refuseNonInteractiveMint, resolveHumanPresenceMintSeams, } from "./human-presence-mint.js";
13
13
  import { isPathInsideRoot } from "./scope-record-approved-scope.js";
14
+ const DEFAULT_MINTED_VIA = "scope:record-intent-constraint";
15
+ const ALLOWED_MINTED_VIA = new Set([DEFAULT_MINTED_VIA, "in-harness-ask"]);
14
16
  function usage() {
15
17
  return ("usage: scope:record-intent-constraint -- <xbrief-path> --actor <name> --confirm " +
16
- "[--kind operator] [--project-root <dir>] [--quiet]\n" +
18
+ "[--kind operator] [--minted-via in-harness-ask|scope:record-intent-constraint] " +
19
+ "[--project-root <dir>] [--quiet]\n" +
17
20
  ' Writes .deft/intent-constraint/<plan-id>.json from plan["x-directive/intentConstraint"].\n' +
18
- " --actor is display only and never authorizes mint. Mint requires a real TTY, " +
19
- "controlling terminal, --confirm, and typed phrase mint (#3110). Agent/CI shells refuse.");
21
+ " Attended mid-build (#5010): ask in parent chat; on yes the human runs this verb on a real TTY " +
22
+ "with --minted-via=in-harness-ask (attestation is not an agent-shell bypass). Commit the record on " +
23
+ "the delivery-branch merge base, then rebase the implementation branch — same-PR working-tree-only " +
24
+ "records are not authoritative. Unattended/C1: rewrite or park. --actor is display only. Mint still " +
25
+ "requires a real TTY, controlling terminal, --confirm, and typed phrase mint (#3110). Agent/CI shells refuse.");
20
26
  }
21
27
  function extractPlanId(payload) {
22
28
  if (payload === null || typeof payload !== "object" || Array.isArray(payload))
@@ -33,6 +39,7 @@ export function parseArgs(argv) {
33
39
  xbriefPath: "",
34
40
  actor: "",
35
41
  kind: "operator",
42
+ mintedVia: DEFAULT_MINTED_VIA,
36
43
  quiet: false,
37
44
  confirm: false,
38
45
  };
@@ -77,6 +84,16 @@ export function parseArgs(argv) {
77
84
  else if (arg?.startsWith("--kind=")) {
78
85
  parsed.kind = arg.slice("--kind=".length);
79
86
  }
87
+ else if (arg === "--minted-via") {
88
+ const value = argv[i + 1];
89
+ if (value === undefined)
90
+ return { ...parsed, error: "argument --minted-via: expected one argument" };
91
+ parsed.mintedVia = value;
92
+ i += 1;
93
+ }
94
+ else if (arg?.startsWith("--minted-via=")) {
95
+ parsed.mintedVia = arg.slice("--minted-via=".length);
96
+ }
80
97
  else if (arg?.startsWith("-")) {
81
98
  return { ...parsed, error: `unrecognized argument: ${arg}` };
82
99
  }
@@ -98,6 +115,13 @@ export function parseArgs(argv) {
98
115
  error: `argument --actor is required (human operator identity)\n${usage()}`,
99
116
  };
100
117
  }
118
+ if (!ALLOWED_MINTED_VIA.has(parsed.mintedVia.trim())) {
119
+ return {
120
+ ...parsed,
121
+ error: `argument --minted-via must be one of: ${[...ALLOWED_MINTED_VIA].join(", ")}\n${usage()}`,
122
+ };
123
+ }
124
+ parsed.mintedVia = parsed.mintedVia.trim();
101
125
  return parsed;
102
126
  }
103
127
  export function run(argv, seams = {}) {
@@ -159,7 +183,7 @@ export function run(argv, seams = {}) {
159
183
  kind: args.kind,
160
184
  actor: args.actor,
161
185
  mintedAt: new Date().toISOString().replace(/\.\d{3}Z$/, "Z"),
162
- mintedVia: "scope:record-intent-constraint",
186
+ mintedVia: args.mintedVia,
163
187
  },
164
188
  });
165
189
  if ("error" in rec) {
@@ -5,6 +5,7 @@ export interface ParsedArgs {
5
5
  xbriefPath: string;
6
6
  actor: string;
7
7
  kind: string;
8
+ mintedVia: string;
8
9
  quiet: boolean;
9
10
  confirm: boolean;
10
11
  help?: boolean;
@@ -11,12 +11,18 @@ import { buildObservableScopeRecord, extractObservableChangeFromPlan, parseObser
11
11
  import { isDirectEntrypoint } from "./entrypoint.js";
12
12
  import { refuseMintWhileUatActive, refuseNonInteractiveMint, resolveHumanPresenceMintSeams, } from "./human-presence-mint.js";
13
13
  import { isPathInsideRoot } from "./scope-record-approved-scope.js";
14
+ const DEFAULT_MINTED_VIA = "scope:record-observable-scope";
15
+ const ALLOWED_MINTED_VIA = new Set([DEFAULT_MINTED_VIA, "in-harness-ask"]);
14
16
  function usage() {
15
17
  return ("usage: scope:record-observable-scope -- <xbrief-path> --actor <name> --confirm " +
16
- "[--kind operator] [--project-root <dir>] [--quiet]\n" +
18
+ "[--kind operator] [--minted-via in-harness-ask|scope:record-observable-scope] " +
19
+ "[--project-root <dir>] [--quiet]\n" +
17
20
  ' Writes .deft/observable-scope/<plan-id>.json from plan["x-directive/observableChange"].\n' +
18
- " --actor is display only and never authorizes mint. Mint requires a real TTY, " +
19
- "controlling terminal, --confirm, and typed phrase mint (#3110). Agent/CI shells refuse.");
21
+ " Attended mid-build (#5010): ask in parent chat; on yes the human runs this verb on a real TTY " +
22
+ "with --minted-via=in-harness-ask (attestation is not an agent-shell bypass). Commit the record on " +
23
+ "the delivery-branch merge base, then rebase the implementation branch — same-PR working-tree-only " +
24
+ "records are not authoritative. Unattended/C1: rewrite or park. --actor is display only. Mint still " +
25
+ "requires a real TTY, controlling terminal, --confirm, and typed phrase mint (#3110). Agent/CI shells refuse.");
20
26
  }
21
27
  function extractPlanId(payload) {
22
28
  if (payload === null || typeof payload !== "object" || Array.isArray(payload))
@@ -33,6 +39,7 @@ export function parseArgs(argv) {
33
39
  xbriefPath: "",
34
40
  actor: "",
35
41
  kind: "operator",
42
+ mintedVia: DEFAULT_MINTED_VIA,
36
43
  quiet: false,
37
44
  confirm: false,
38
45
  };
@@ -77,6 +84,16 @@ export function parseArgs(argv) {
77
84
  else if (arg?.startsWith("--kind=")) {
78
85
  parsed.kind = arg.slice("--kind=".length);
79
86
  }
87
+ else if (arg === "--minted-via") {
88
+ const value = argv[i + 1];
89
+ if (value === undefined)
90
+ return { ...parsed, error: "argument --minted-via: expected one argument" };
91
+ parsed.mintedVia = value;
92
+ i += 1;
93
+ }
94
+ else if (arg?.startsWith("--minted-via=")) {
95
+ parsed.mintedVia = arg.slice("--minted-via=".length);
96
+ }
80
97
  else if (arg?.startsWith("-")) {
81
98
  return { ...parsed, error: `unrecognized argument: ${arg}` };
82
99
  }
@@ -98,6 +115,13 @@ export function parseArgs(argv) {
98
115
  error: `argument --actor is required (human operator identity)\n${usage()}`,
99
116
  };
100
117
  }
118
+ if (!ALLOWED_MINTED_VIA.has(parsed.mintedVia.trim())) {
119
+ return {
120
+ ...parsed,
121
+ error: `argument --minted-via must be one of: ${[...ALLOWED_MINTED_VIA].join(", ")}\n${usage()}`,
122
+ };
123
+ }
124
+ parsed.mintedVia = parsed.mintedVia.trim();
101
125
  return parsed;
102
126
  }
103
127
  export function run(argv, seams = {}) {
@@ -161,7 +185,7 @@ export function run(argv, seams = {}) {
161
185
  kind: args.kind,
162
186
  actor: args.actor,
163
187
  mintedAt: new Date().toISOString().replace(/\.\d{3}Z$/, "Z"),
164
- mintedVia: "scope:record-observable-scope",
188
+ mintedVia: args.mintedVia,
165
189
  },
166
190
  });
167
191
  if ("error" in rec) {
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env node
2
+ interface ParsedArgs {
3
+ projectRoot: string;
4
+ baseRef: string | null;
5
+ quiet: boolean;
6
+ error?: string;
7
+ }
8
+ export declare function parseArgs(argv: string[]): ParsedArgs;
9
+ export declare function run(argv: string[]): number;
10
+ export {};
11
+ //# sourceMappingURL=verify-class-checks.d.ts.map
@@ -0,0 +1,71 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * CLI for verify:class-checks (#4980 / #3145 class).
4
+ */
5
+ import { resolve } from "node:path";
6
+ import { classChecks } from "@deftai/directive-core";
7
+ import { isDirectEntrypoint } from "./entrypoint.js";
8
+ export function parseArgs(argv) {
9
+ const parsed = {
10
+ projectRoot: ".",
11
+ baseRef: null,
12
+ quiet: 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 === "--project-root") {
20
+ const value = argv[i + 1];
21
+ if (value === undefined) {
22
+ return { ...parsed, error: "argument --project-root: expected one argument" };
23
+ }
24
+ parsed.projectRoot = value;
25
+ i += 1;
26
+ }
27
+ else if (arg?.startsWith("--project-root=")) {
28
+ parsed.projectRoot = arg.slice("--project-root=".length);
29
+ }
30
+ else if (arg === "--base-ref") {
31
+ const value = argv[i + 1];
32
+ if (value === undefined) {
33
+ return { ...parsed, error: "argument --base-ref: expected one argument" };
34
+ }
35
+ parsed.baseRef = value;
36
+ i += 1;
37
+ }
38
+ else if (arg?.startsWith("--base-ref=")) {
39
+ parsed.baseRef = arg.slice("--base-ref=".length);
40
+ }
41
+ else {
42
+ return { ...parsed, error: `unrecognized argument: ${arg}` };
43
+ }
44
+ }
45
+ return parsed;
46
+ }
47
+ export function run(argv) {
48
+ const args = parseArgs(argv);
49
+ if (args.error !== undefined) {
50
+ process.stderr.write(`verify_class_checks: ${args.error}\n`);
51
+ return 2;
52
+ }
53
+ const projectRoot = resolve(args.projectRoot);
54
+ const result = classChecks.evaluateClassChecks(projectRoot, {
55
+ baseRef: args.baseRef,
56
+ quiet: args.quiet,
57
+ });
58
+ if (result.exitCode === 0) {
59
+ if (!args.quiet) {
60
+ process.stdout.write(`${result.message}\n`);
61
+ }
62
+ }
63
+ else {
64
+ process.stderr.write(`${result.message}\n`);
65
+ }
66
+ return result.exitCode;
67
+ }
68
+ if (isDirectEntrypoint(import.meta.url)) {
69
+ process.exit(run(process.argv.slice(2)));
70
+ }
71
+ //# sourceMappingURL=verify-class-checks.js.map
@@ -9,6 +9,11 @@ interface ParsedArgs {
9
9
  approach3: boolean;
10
10
  approach3Warned: boolean;
11
11
  emitJson: boolean;
12
+ /** When true, also run the #4882 merge-path arm observer (attested flags). */
13
+ mergePathArm: boolean;
14
+ liveWait: boolean;
15
+ explicitFinish: boolean;
16
+ stickyLease: boolean;
12
17
  help: boolean;
13
18
  error?: string;
14
19
  }
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import { resolve } from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
- import { evaluateReviewMonitorGate, REVIEW_MONITOR_HELP, verifyResultToJson, } from "@deftai/directive-core/review-monitor";
4
+ import { evaluateMergePathArm, } from "@deftai/directive-core/dist/pr-watch/main.js";
5
+ import { EXIT_CONFIG_ERROR, EXIT_NOT_READY, evaluateReviewMonitorGate, isTier1, REVIEW_MONITOR_HELP, verifyResultToJson, } from "@deftai/directive-core/review-monitor";
5
6
  const CALL_SITES = new Set([
6
7
  "solo",
7
8
  "swarm-phase5-6",
@@ -18,6 +19,10 @@ export function parseVerifyReviewMonitorArgs(argv) {
18
19
  approach3: false,
19
20
  approach3Warned: false,
20
21
  emitJson: false,
22
+ mergePathArm: false,
23
+ liveWait: false,
24
+ explicitFinish: false,
25
+ stickyLease: false,
21
26
  help: false,
22
27
  };
23
28
  for (let i = 0; i < argv.length; i += 1) {
@@ -28,6 +33,18 @@ export function parseVerifyReviewMonitorArgs(argv) {
28
33
  if (arg === "--json") {
29
34
  acc.emitJson = true;
30
35
  }
36
+ else if (arg === "--merge-path-arm") {
37
+ acc.mergePathArm = true;
38
+ }
39
+ else if (arg === "--live-wait") {
40
+ acc.liveWait = true;
41
+ }
42
+ else if (arg === "--explicit-finish") {
43
+ acc.explicitFinish = true;
44
+ }
45
+ else if (arg === "--sticky-lease") {
46
+ acc.stickyLease = true;
47
+ }
31
48
  else if (arg === "--approach3") {
32
49
  acc.approach3 = true;
33
50
  }
@@ -117,6 +134,13 @@ export function run(argv) {
117
134
  const args = parseVerifyReviewMonitorArgs(argv);
118
135
  if (args.help) {
119
136
  process.stdout.write(REVIEW_MONITOR_HELP);
137
+ process.stdout.write("\n#4882 merge-path arm observer (optional):\n" +
138
+ " --merge-path-arm Fail closed when neither live wait nor explicit finish\n" +
139
+ " --live-wait Attest a still-running phase-correct wait for this PR\n" +
140
+ " (Tier 1: bound to gate lease evidence for --pr)\n" +
141
+ " --explicit-finish Attest option-C BLOCKED/FAILED finish for this PR\n" +
142
+ " --sticky-lease Attest a fresh sticky lease (not sufficient alone)\n" +
143
+ " Prefer Approach 1 / native pr:watch; homemade line-parsed --json is not an arm.\n");
120
144
  return 0;
121
145
  }
122
146
  if (args.error !== undefined) {
@@ -129,6 +153,8 @@ export function run(argv) {
129
153
  process.stderr.write("Try: task verify:review-monitor -- --help\n");
130
154
  return 2;
131
155
  }
156
+ // Always evaluate the gate first so --project-root / repo config errors stay
157
+ // exit 2 even when --merge-path-arm would otherwise fail closed as unarmed.
132
158
  const result = evaluateReviewMonitorGate({
133
159
  pr: args.pr,
134
160
  projectRoot: resolve(args.projectRoot),
@@ -139,15 +165,69 @@ export function run(argv) {
139
165
  approach3Warned: args.approach3Warned,
140
166
  environ: process.env,
141
167
  });
168
+ let arm = null;
169
+ if (args.mergePathArm) {
170
+ // Bind --live-wait to gate-observed lease evidence on Tier 1 for this PR.
171
+ // Bare flags must not arm when Tier 1 requires a lease and none is present.
172
+ const leaseEvidence = result.monitorRecord !== null;
173
+ const liveBound = args.liveWait && (!isTier1(result.tier) || leaseEvidence);
174
+ arm = evaluateMergePathArm({
175
+ livePhaseCorrectWait: liveBound,
176
+ explicitFinish: args.explicitFinish,
177
+ stickyLeaseActive: args.stickyLease || leaseEvidence,
178
+ });
179
+ if (args.liveWait && !liveBound && !args.explicitFinish && !arm.armed) {
180
+ arm = {
181
+ armed: false,
182
+ reason: "unarmed_stand_down",
183
+ message: `unarmed stand-down: --live-wait attestation unbound to lease evidence ` +
184
+ `for PR #${args.pr} (Tier 1); sticky lease alone is not a live arm (#4882)`,
185
+ };
186
+ }
187
+ }
142
188
  if (args.emitJson) {
143
- process.stdout.write(`${JSON.stringify(verifyResultToJson(result), null, 2)}\n`);
189
+ const payload = verifyResultToJson(result);
190
+ if (arm !== null) {
191
+ payload.merge_path_arm = {
192
+ armed: arm.armed,
193
+ reason: arm.reason,
194
+ message: arm.message,
195
+ live_wait: args.liveWait,
196
+ explicit_finish: args.explicitFinish,
197
+ sticky_lease: args.stickyLease,
198
+ lease_evidence: result.monitorRecord !== null,
199
+ };
200
+ // Combined gate+arm: unarmed fails closed even when the monitor gate is ready.
201
+ if (!arm.armed && result.exitCode !== EXIT_CONFIG_ERROR) {
202
+ payload.ready = false;
203
+ payload.exit_code = EXIT_NOT_READY;
204
+ // Gate-ready → arm diagnosis; both failing → keep both messages.
205
+ payload.message = result.exitCode === 0 ? arm.message : `${result.message}\n${arm.message}`;
206
+ }
207
+ }
208
+ process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
209
+ }
210
+ else if (result.exitCode === EXIT_CONFIG_ERROR) {
211
+ process.stderr.write(`${result.message}\n`);
212
+ }
213
+ else if (arm !== null && !arm.armed) {
214
+ process.stderr.write(`${arm.message}\n`);
144
215
  }
145
216
  else if (result.exitCode === 0) {
146
217
  process.stdout.write(`${result.message}\n`);
218
+ if (arm !== null) {
219
+ process.stdout.write(`${arm.message}\n`);
220
+ }
147
221
  }
148
222
  else {
149
223
  process.stderr.write(`${result.message}\n`);
150
224
  }
225
+ if (result.exitCode === EXIT_CONFIG_ERROR) {
226
+ return EXIT_CONFIG_ERROR;
227
+ }
228
+ if (arm !== null && !arm.armed) {
229
+ return EXIT_NOT_READY;
230
+ }
151
231
  return result.exitCode;
152
232
  }
153
233
  if (process.argv[1] !== undefined && fileURLToPath(import.meta.url) === process.argv[1]) {
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { resolve } from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
- import { emitBypassWarning, emitVerifyJson, formatCacheFreshDeferSoftPath, formatRitualRecoveryInstruction, verifySessionRitual, } from "@deftai/directive-core/session";
4
+ import { emitBypassWarning, emitVerifyJson, formatCacheFreshDeferSoftPath, occupancyAwareDenialMessage, verifySessionRitual, } from "@deftai/directive-core/session";
5
5
  function parsePosture(value) {
6
6
  if (value === "read-only")
7
7
  return "read-only";
@@ -121,8 +121,10 @@ export function run(argv, deps = {}) {
121
121
  }
122
122
  }
123
123
  else if (result.code === 1) {
124
- const recovery = formatRitualRecoveryInstruction(result.recoveryTier ?? "cold");
125
- const lines = [result.message, recovery];
124
+ const recoveryTier = result.recoveryTier ?? "cold";
125
+ const lines = [
126
+ occupancyAwareDenialMessage(projectRoot, result.message, { env: process.env }, recoveryTier),
127
+ ];
126
128
  if (isCacheFreshFailureMessage(result.message)) {
127
129
  lines.push(formatCacheFreshDeferSoftPath());
128
130
  }
@@ -0,0 +1,39 @@
1
+ /** Fallback when package.json cannot be read. Keep aligned with the bin map. */
2
+ export declare const PACKAGE_BIN_NAMES: readonly ["directive", "deft", "deft-ts", "deft-hook", "deft-verify-encoding"];
3
+ /** Node owns these shims. This package must not delete them. */
4
+ export declare const NODE_OWNED_SHIM_NAMES: readonly ["npx", "npm"];
5
+ export interface Ps1ShimRemoval {
6
+ readonly removed: readonly string[];
7
+ readonly failed: readonly string[];
8
+ }
9
+ export declare function packageIsUnderNodeModules(pkgDir: string): boolean;
10
+ /** bin-links layout. Valid only when {@link packageIsUnderNodeModules} is true. */
11
+ export declare function nodeModulesDir(pkgDir: string): string;
12
+ /** Same directory bin-links uses for this package root. */
13
+ export declare function linkerBinDir(opts: {
14
+ readonly pkgDir: string;
15
+ readonly global: boolean;
16
+ readonly platform: string;
17
+ }): string;
18
+ export declare function isGlobalInstall(env: Readonly<Record<string, string | undefined>>): boolean;
19
+ export declare function candidateBinDirs(opts: {
20
+ readonly pkgDir: string;
21
+ readonly env: Readonly<Record<string, string | undefined>>;
22
+ readonly platform: string;
23
+ }): string[];
24
+ export declare function removeGeneratedPs1Shims(opts: {
25
+ readonly binDir: string;
26
+ readonly binNames: readonly string[];
27
+ readonly platform: string;
28
+ readonly protectedNames?: readonly string[];
29
+ }): Ps1ShimRemoval;
30
+ /** True when cmd-shim text records a target path inside this install. */
31
+ export declare function cmdShimTargetsPackage(cmdText: string, binDir: string, pkgDir: string): boolean;
32
+ export declare function readPackageBinNames(packageJsonText: string): string[];
33
+ export declare function removeInstalledWindowsPs1Shims(opts: {
34
+ readonly pkgDir: string;
35
+ readonly env: Readonly<Record<string, string | undefined>>;
36
+ readonly platform: string;
37
+ readonly readText?: (path: string) => string;
38
+ }): Ps1ShimRemoval;
39
+ //# sourceMappingURL=windows-bin-ps1.d.ts.map
@@ -0,0 +1,224 @@
1
+ /**
2
+ * npm's Windows bin linker (cmd-shim) always writes a .ps1 beside the .cmd.
3
+ * PowerShell selects that .ps1 and stops when script execution is refused, so
4
+ * the bare name never reaches the .cmd. This runs from postinstall, after
5
+ * that link, and removes only this package's generated .ps1 files.
6
+ * A .ps1 is removed only when its sibling .cmd records a target inside this
7
+ * install, so an ambient PNPM_HOME from another install is left alone.
8
+ * It does not change npx.ps1, npm.ps1, or the operator's execution policy.
9
+ */
10
+ import { existsSync, readFileSync, realpathSync, unlinkSync } from "node:fs";
11
+ import { basename, dirname, join, win32 } from "node:path";
12
+ /** Fallback when package.json cannot be read. Keep aligned with the bin map. */
13
+ export const PACKAGE_BIN_NAMES = [
14
+ "directive",
15
+ "deft",
16
+ "deft-ts",
17
+ "deft-hook",
18
+ "deft-verify-encoding",
19
+ ];
20
+ /** Node owns these shims. This package must not delete them. */
21
+ export const NODE_OWNED_SHIM_NAMES = ["npx", "npm"];
22
+ export function packageIsUnderNodeModules(pkgDir) {
23
+ const parent = dirname(pkgDir);
24
+ if (basename(parent) === "node_modules")
25
+ return true;
26
+ return basename(dirname(parent)) === "node_modules";
27
+ }
28
+ /** bin-links layout. Valid only when {@link packageIsUnderNodeModules} is true. */
29
+ export function nodeModulesDir(pkgDir) {
30
+ const scopeOrNm = dirname(pkgDir);
31
+ if (basename(scopeOrNm) === "node_modules")
32
+ return scopeOrNm;
33
+ return dirname(scopeOrNm);
34
+ }
35
+ /** Same directory bin-links uses for this package root. */
36
+ export function linkerBinDir(opts) {
37
+ const nm = nodeModulesDir(opts.pkgDir);
38
+ if (!opts.global)
39
+ return join(nm, ".bin");
40
+ const prefix = dirname(nm);
41
+ if (opts.platform === "win32")
42
+ return prefix;
43
+ return join(prefix, "bin");
44
+ }
45
+ export function isGlobalInstall(env) {
46
+ return env.npm_config_global === "true";
47
+ }
48
+ function dirKey(dir, platform) {
49
+ const norm = dir.replace(/[\\/]+/g, "/");
50
+ if (platform === "win32")
51
+ return norm.toLowerCase();
52
+ return norm;
53
+ }
54
+ function ancestorBinDirs(pkgDir, stopDir, platform) {
55
+ const stop = dirKey(stopDir, platform);
56
+ const start = dirKey(pkgDir, platform);
57
+ const stopPrefix = stop.endsWith("/") ? stop : `${stop}/`;
58
+ if (start !== stop && !start.startsWith(stopPrefix)) {
59
+ return [join(stopDir, "node_modules", ".bin")];
60
+ }
61
+ const out = [];
62
+ let dir = pkgDir;
63
+ for (;;) {
64
+ out.push(join(dir, "node_modules", ".bin"));
65
+ if (dirKey(dir, platform) === stop)
66
+ break;
67
+ const parent = dirname(dir);
68
+ if (parent === dir)
69
+ break;
70
+ dir = parent;
71
+ }
72
+ return out;
73
+ }
74
+ export function candidateBinDirs(opts) {
75
+ const global = isGlobalInstall(opts.env);
76
+ const dirs = [];
77
+ if (packageIsUnderNodeModules(opts.pkgDir)) {
78
+ dirs.push(linkerBinDir({ pkgDir: opts.pkgDir, global, platform: opts.platform }));
79
+ }
80
+ if (global) {
81
+ const prefix = opts.env.npm_config_prefix;
82
+ if (prefix !== undefined && prefix.length > 0) {
83
+ dirs.push(opts.platform === "win32" ? prefix : join(prefix, "bin"));
84
+ }
85
+ // Candidate only. Removal still requires the .cmd to target this install.
86
+ const pnpmHome = opts.env.PNPM_HOME;
87
+ if (pnpmHome !== undefined && pnpmHome.length > 0)
88
+ dirs.push(pnpmHome);
89
+ }
90
+ else {
91
+ const localPrefix = opts.env.npm_config_local_prefix;
92
+ if (localPrefix !== undefined && localPrefix.length > 0) {
93
+ dirs.push(...ancestorBinDirs(opts.pkgDir, localPrefix, opts.platform));
94
+ }
95
+ }
96
+ const seen = new Set();
97
+ const out = [];
98
+ for (const dir of dirs) {
99
+ const key = dirKey(dir, opts.platform);
100
+ if (seen.has(key))
101
+ continue;
102
+ seen.add(key);
103
+ out.push(dir);
104
+ }
105
+ return out;
106
+ }
107
+ function isSafeBinName(name) {
108
+ if (name.length === 0)
109
+ return false;
110
+ if (name.includes("/") || name.includes("\\") || name.includes(".."))
111
+ return false;
112
+ return true;
113
+ }
114
+ export function removeGeneratedPs1Shims(opts) {
115
+ if (opts.platform !== "win32")
116
+ return { removed: [], failed: [] };
117
+ const protect = new Set((opts.protectedNames ?? NODE_OWNED_SHIM_NAMES).map((name) => name.toLowerCase()));
118
+ const removed = [];
119
+ const failed = [];
120
+ for (const name of opts.binNames) {
121
+ if (!isSafeBinName(name) || protect.has(name.toLowerCase()))
122
+ continue;
123
+ const ps1 = join(opts.binDir, `${name}.ps1`);
124
+ const cmd = join(opts.binDir, `${name}.cmd`);
125
+ if (!existsSync(cmd) || !existsSync(ps1))
126
+ continue;
127
+ try {
128
+ unlinkSync(ps1);
129
+ removed.push(ps1);
130
+ }
131
+ catch {
132
+ failed.push(ps1);
133
+ }
134
+ }
135
+ return { removed, failed };
136
+ }
137
+ function normalizedWinPath(p) {
138
+ const norm = win32.normalize(p).replace(/[\\/]+/g, "\\");
139
+ const trimmed = norm.length > 3 && norm.endsWith("\\") ? norm.slice(0, -1) : norm;
140
+ return trimmed.toLowerCase();
141
+ }
142
+ function pathIsInside(root, target) {
143
+ const rootKey = normalizedWinPath(root);
144
+ const targetKey = normalizedWinPath(target);
145
+ return targetKey === rootKey || targetKey.startsWith(`${rootKey}\\`);
146
+ }
147
+ /** cmd-shim text is Windows-shaped. realpath needs host separators or a junction never matches. */
148
+ function hostPath(p) {
149
+ return p.replace(/\\/g, "/");
150
+ }
151
+ function realpathIsInside(root, target) {
152
+ try {
153
+ return pathIsInside(realpathSync(hostPath(root)), realpathSync(hostPath(target)));
154
+ }
155
+ catch {
156
+ return false;
157
+ }
158
+ }
159
+ /** True when cmd-shim text records a target path inside this install. */
160
+ export function cmdShimTargetsPackage(cmdText, binDir, pkgDir) {
161
+ const re = /%(?:dp0%|~dp0)(?:\\+|\/+)([^"\r\n\s]+)/gi;
162
+ for (const match of cmdText.matchAll(re)) {
163
+ const rel = match[1];
164
+ if (rel === undefined || rel.length === 0)
165
+ continue;
166
+ const resolved = win32.normalize(win32.join(binDir, rel.replace(/\//g, "\\")));
167
+ if (pathIsInside(pkgDir, resolved) || realpathIsInside(pkgDir, resolved))
168
+ return true;
169
+ }
170
+ return false;
171
+ }
172
+ function cmdLinksCurrentInstall(binDir, name, pkgDir) {
173
+ if (!isSafeBinName(name))
174
+ return false;
175
+ let text;
176
+ try {
177
+ text = readFileSync(join(binDir, `${name}.cmd`), "utf8");
178
+ }
179
+ catch {
180
+ return false;
181
+ }
182
+ return cmdShimTargetsPackage(text, binDir, pkgDir);
183
+ }
184
+ export function readPackageBinNames(packageJsonText) {
185
+ let parsed;
186
+ try {
187
+ parsed = JSON.parse(packageJsonText);
188
+ }
189
+ catch {
190
+ return [];
191
+ }
192
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed))
193
+ return [];
194
+ const bin = parsed.bin;
195
+ if (bin === null || typeof bin !== "object" || Array.isArray(bin))
196
+ return [];
197
+ return Object.keys(bin);
198
+ }
199
+ export function removeInstalledWindowsPs1Shims(opts) {
200
+ if (opts.platform !== "win32")
201
+ return { removed: [], failed: [] };
202
+ const readText = opts.readText ?? ((path) => readFileSync(path, "utf8"));
203
+ let names;
204
+ try {
205
+ names = readPackageBinNames(readText(join(opts.pkgDir, "package.json")));
206
+ }
207
+ catch {
208
+ names = PACKAGE_BIN_NAMES;
209
+ }
210
+ const removed = [];
211
+ const failed = [];
212
+ for (const binDir of candidateBinDirs(opts)) {
213
+ const linked = names.filter((name) => cmdLinksCurrentInstall(binDir, name, opts.pkgDir));
214
+ const result = removeGeneratedPs1Shims({
215
+ binDir,
216
+ binNames: linked,
217
+ platform: opts.platform,
218
+ });
219
+ removed.push(...result.removed);
220
+ failed.push(...result.failed);
221
+ }
222
+ return { removed, failed };
223
+ }
224
+ //# sourceMappingURL=windows-bin-ps1.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deftai/directive",
3
- "version": "0.119.8",
3
+ "version": "0.119.9",
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/",
@@ -22,7 +22,8 @@
22
22
  },
23
23
  "files": [
24
24
  "dist/**/*.js",
25
- "dist/**/*.d.ts"
25
+ "dist/**/*.d.ts",
26
+ "scripts/remove-windows-ps1-shims.mjs"
26
27
  ],
27
28
  "repository": {
28
29
  "type": "git",
@@ -34,10 +35,11 @@
34
35
  "provenance": true
35
36
  },
36
37
  "dependencies": {
37
- "@deftai/directive-core": "^0.119.8",
38
- "@deftai/directive-content": "^0.119.8"
38
+ "@deftai/directive-core": "^0.119.9",
39
+ "@deftai/directive-content": "^0.119.9"
39
40
  },
40
41
  "scripts": {
41
- "build": "tsc -b"
42
+ "build": "tsc -b",
43
+ "postinstall": "node ./scripts/remove-windows-ps1-shims.mjs"
42
44
  }
43
45
  }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * postinstall entry (#4654). The published tarball includes dist/.
3
+ * A workspace install before tsc has no linked shims yet, so a missing
4
+ * dist file is a no-op rather than a failed install.
5
+ */
6
+ import { existsSync } from "node:fs";
7
+ import { dirname, join } from "node:path";
8
+ import { fileURLToPath, pathToFileURL } from "node:url";
9
+
10
+ const pkgDir = dirname(dirname(fileURLToPath(import.meta.url)));
11
+ const entry = join(pkgDir, "dist", "windows-bin-ps1.js");
12
+
13
+ if (existsSync(entry)) {
14
+ const mod = await import(pathToFileURL(entry).href);
15
+ const result = mod.removeInstalledWindowsPs1Shims({
16
+ pkgDir,
17
+ env: process.env,
18
+ platform: process.platform,
19
+ });
20
+ if (result.failed.length > 0) {
21
+ process.stderr.write(
22
+ `remove-windows-ps1-shims: could not remove ${result.failed.join(", ")}\n`,
23
+ );
24
+ process.exitCode = 1;
25
+ }
26
+ }