@deftai/directive 0.85.0 → 0.87.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.
@@ -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", "pr-closing-keywords", "pr-merge-readiness", "pr-monitor", "pr-protected-issues", "pr-wait-mergeable", "pr-watch", "preflight-cache", "preflight-gh", "probe-session", "release", "release-e2e", "release-publish", "release-rollback", "scope-lifecycle", "lifecycle-event", "session-start", "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", "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-content-manifest", "verify-skill-external-fetch-gate", "verify-contract-drift", "verify-cursor-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-subagent-alive", "review-monitor-register", "review-monitor-release", "verify-tools", "verify-wip-cap", "verify-orphan-active", "verify-agents-md-budget", "verify-agents-md-advisory", "verify-eval-health-relocation", "verify-eval-triggers-relocation", "eval-health", "eval-run", "eval-report", "eval-triggers"];
13
+ export declare const CLI_MODULE_VERBS: readonly ["agents-refresh", "cache", "check", "capacity-backfill", "capacity-show", "codebase-default-extractor", "codebase-map", "codebase-map-fresh", "codebase-projection-registry", "codebase-provider", "doctor", "install-upgrade", "install-uninstall", "migrate-preflight", "migrate-xbrief", "migrate-category-b", "framework-check-updates", "hook-dispatch", "umbrella-current-shape", "changelog-check", "change-init", "commit-lint", "coverage-hotspots", "policy", "pr-closing-keywords", "pr-merge-readiness", "pr-monitor", "pr-protected-issues", "pr-wait-mergeable", "pr-watch", "preflight-cache", "preflight-gh", "probe-session", "release", "release-e2e", "release-publish", "release-rollback", "scope-lifecycle", "lifecycle-event", "session-start", "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", "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-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-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
15
  export declare const CORE_MODULE_VERBS: readonly ["scm", "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"];
16
16
  /** Colon aliases for triage-actions (mirrors cli-router SUBCOMMAND_ROUTES). */
package/dist/dispatch.js CHANGED
@@ -8,10 +8,12 @@ import { existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, readSync
8
8
  import { homedir, tmpdir } from "node:os";
9
9
  import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
10
10
  import { engineInfo, userConfig } from "@deftai/directive-core";
11
+ import { assertWriteTargetSafe } from "@deftai/directive-core/dist/fs/projection-containment.js";
11
12
  import { parseInitArgv, runInitDepositCli } from "@deftai/directive-core/init-deposit";
12
13
  import { appendAuditLog, disclosureLine, migrateLegacyPolicyKey, PLAN_POLICY_KEY, policyColonInvocation, policySetInvocation, projectDefinitionPath, resolvePolicy, resolveWipCap, setPolicy, } from "@deftai/directive-core/policy";
13
14
  import { defaultWhich } from "@deftai/directive-core/scm";
14
15
  import { KNOWN_SUBAGENT_BACKEND_IDS, probeSubagentBackends, resolveSwarmSubagentBackend, } from "@deftai/directive-core/swarm";
16
+ import { atomicWriteProjectDefinition } from "@deftai/directive-core/vbrief-build";
15
17
  const HANDLER_KEYS = [
16
18
  "run",
17
19
  "main",
@@ -105,6 +107,7 @@ export const CLI_MODULE_VERBS = [
105
107
  "verify-skill-external-fetch-gate",
106
108
  "verify-contract-drift",
107
109
  "verify-cursor-tier1",
110
+ "verify-openclaw-tier1",
108
111
  "verify-go-freeze",
109
112
  "verify-scm-boundary",
110
113
  "verify-session-ritual",
@@ -250,6 +253,7 @@ export const VERB_ALIASES = {
250
253
  "verify:skill-external-fetch-gate": "verify-skill-external-fetch-gate",
251
254
  "verify:contract-drift": "verify-contract-drift",
252
255
  "verify:cursor-tier1": "verify-cursor-tier1",
256
+ "verify:openclaw-tier1": "verify-openclaw-tier1",
253
257
  "verify:go-freeze": "verify-go-freeze",
254
258
  "verify:bridge-drift": "verify-bridge-drift",
255
259
  "verify:scm-boundary": "verify-scm-boundary",
@@ -309,6 +313,7 @@ const SUBDIR_CLI_STEMS = {
309
313
  "verify-skill-external-fetch-gate": "verify-source-cli/verify-skill-external-fetch-gate",
310
314
  "verify-contract-drift": "verify-source-cli/verify-contract-drift",
311
315
  "verify-cursor-tier1": "verify-source-cli/verify-cursor-tier1",
316
+ "verify-openclaw-tier1": "verify-source-cli/verify-openclaw-tier1",
312
317
  "verify-scm-boundary": "verify-source-cli/verify-scm-boundary",
313
318
  "verify-xbrief-drift": "verify-source-cli/verify-xbrief-drift",
314
319
  "verify-go-freeze": "gates-cli/verify-go-freeze",
@@ -2035,7 +2040,8 @@ function writeWipCap(projectRoot, cap, actor, note) {
2035
2040
  const { path, data, policyBlock } = loadProjectDefinitionForWrite(projectRoot);
2036
2041
  const previous = policyBlock.wipCap;
2037
2042
  policyBlock.wipCap = cap;
2038
- writeFileSync(path, `${JSON.stringify(data, null, 2)}\n`, "utf8");
2043
+ assertWriteTargetSafe(projectRoot, path);
2044
+ atomicWriteProjectDefinition(path, data);
2039
2045
  const changed = previous !== cap;
2040
2046
  const parts = [`actor=${actor}`, `wipCap=${cap}`, `previous=${pyRepr(previous)}`];
2041
2047
  if (note)
@@ -2049,7 +2055,8 @@ function writeSubagentBackend(projectRoot, backendId, actor, note) {
2049
2055
  const { path, data, policyBlock } = loadProjectDefinitionForWrite(projectRoot);
2050
2056
  const previous = policyBlock.swarmSubagentBackend;
2051
2057
  policyBlock.swarmSubagentBackend = backendId;
2052
- writeFileSync(path, `${JSON.stringify(data, null, 2)}\n`, "utf8");
2058
+ assertWriteTargetSafe(projectRoot, path);
2059
+ atomicWriteProjectDefinition(path, data);
2053
2060
  const changed = previous !== backendId;
2054
2061
  const parts = [
2055
2062
  `actor=${actor}`,
@@ -0,0 +1,6 @@
1
+ /**
2
+ * True when this module is the process entrypoint, including npm bin symlinks.
3
+ * Node passes the symlink path in argv[1]; compare via realpath on both sides.
4
+ */
5
+ export declare function isDirectEntrypoint(moduleUrl: string | URL): boolean;
6
+ //# sourceMappingURL=entrypoint.d.ts.map
@@ -0,0 +1,19 @@
1
+ import { realpathSync } from "node:fs";
2
+ import { fileURLToPath } from "node:url";
3
+ /**
4
+ * True when this module is the process entrypoint, including npm bin symlinks.
5
+ * Node passes the symlink path in argv[1]; compare via realpath on both sides.
6
+ */
7
+ export function isDirectEntrypoint(moduleUrl) {
8
+ const argv1 = process.argv[1];
9
+ if (argv1 === undefined)
10
+ return false;
11
+ try {
12
+ const modulePath = fileURLToPath(moduleUrl);
13
+ return realpathSync(argv1) === realpathSync(modulePath);
14
+ }
15
+ catch {
16
+ return false;
17
+ }
18
+ }
19
+ //# sourceMappingURL=entrypoint.js.map
package/dist/hook-bin.js CHANGED
@@ -1,11 +1,11 @@
1
1
  #!/usr/bin/env node
2
- import { fileURLToPath } from "node:url";
2
+ import { isDirectEntrypoint } from "./entrypoint.js";
3
3
  import { run } from "./hook-dispatch.js";
4
4
  // Keep PreToolUse off the general CLI router's cold path.
5
5
  export function main(argv = process.argv.slice(2)) {
6
6
  return run(argv);
7
7
  }
8
- if (process.argv[1] !== undefined && fileURLToPath(import.meta.url) === process.argv[1]) {
8
+ if (isDirectEntrypoint(import.meta.url)) {
9
9
  process.exit(main());
10
10
  }
11
11
  //# sourceMappingURL=hook-bin.js.map
@@ -11,13 +11,64 @@ export interface HookDispatchCliSeams {
11
11
  readonly writeOut?: (text: string) => void;
12
12
  readonly writeErr?: (text: string) => void;
13
13
  readonly cwd?: () => string;
14
+ /**
15
+ * Sleep between empty-stdin retry polls (#2864). Injected in tests so delayed
16
+ * payloads can be simulated without real wall-clock waits.
17
+ */
18
+ readonly sleepMs?: (ms: number) => void;
19
+ /**
20
+ * Empty-stdin retry budget in ms (default {@link DEFAULT_STDIN_EMPTY_RETRY_MS}).
21
+ * Set `0` to disable retries (single read). Tests for true-empty paths often
22
+ * set this to 0 for speed; delayed-payload tests set a positive budget.
23
+ */
24
+ readonly stdinEmptyRetryMs?: number;
25
+ /** Clock for empty-stdin deadline; defaults to `Date.now` (tests inject synthetic clocks). */
26
+ readonly nowMs?: () => number;
14
27
  }
28
+ /**
29
+ * How long to re-poll stdin after an empty first read before concluding the host
30
+ * sent nothing (#2864). Keeps well under Cursor's deposited `timeout: 5` while
31
+ * covering short non-blocking delivery races (reporter matrix rows K/L).
32
+ */
33
+ export declare const DEFAULT_STDIN_EMPTY_RETRY_MS = 250;
34
+ /** Interval between empty-stdin re-polls while the retry budget remains. */
35
+ export declare const STDIN_EMPTY_RETRY_INTERVAL_MS = 25;
36
+ /**
37
+ * Read hook stdin with a short empty-read retry budget (#2864).
38
+ *
39
+ * A single `readFileSync(0)` on a non-blocking descriptor can return zero bytes
40
+ * before the host has attached a writer. That is indistinguishable from a true
41
+ * empty payload if we never re-poll. We re-read for up to `emptyRetryMs`; true
42
+ * empty (every poll empty through the budget) still yields empty for the deny
43
+ * path. Delayed payloads that arrive within the budget are accepted.
44
+ */
45
+ export declare function readStdinHardened(readOnce: () => string, options?: {
46
+ emptyRetryMs?: number;
47
+ sleepMs?: (ms: number) => void;
48
+ nowMs?: () => number;
49
+ }): string;
15
50
  export declare function parseArgs(argv: readonly string[]): ParsedArgs;
16
51
  export interface ParsedPayload {
17
52
  readonly payload: unknown;
18
53
  readonly context: HookPayloadContext;
19
54
  }
20
55
  export declare function parsePayload(raw: string): ParsedPayload;
56
+ /**
57
+ * Provider-neutral hook dispatch CLI entry (`deft hook:dispatch` / `deft-hook`).
58
+ *
59
+ * ## Exit-code contract (#2864)
60
+ *
61
+ * | Exit | Meaning |
62
+ * |------|---------|
63
+ * | `0` | A host decision was rendered to stdout (allow **or** deny). Hosts must parse stdout; exit status does **not** encode the verdict. |
64
+ * | `2` | Argv / configuration error (unsupported host/event, missing flags, unrecognized args). |
65
+ *
66
+ * This process never returns `1` for a rendered verdict. Cursor `failClosed`
67
+ * deposits that report "exit code 1" are therefore process-level failures
68
+ * (crash, signal kill under host timeout, spawn environment), not `run()` deny
69
+ * paths. Empty-stdin denies still exit `0` with a Cursor JSON body carrying
70
+ * `code: "stdin-empty"`.
71
+ */
21
72
  export declare function run(argv: string[], seams?: HookDispatchCliSeams): number;
22
73
  export {};
23
74
  //# sourceMappingURL=hook-dispatch.d.ts.map
@@ -3,6 +3,52 @@ import { readFileSync } from "node:fs";
3
3
  import { resolve } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { decideHook, hookPayloadTopLevelKeys, isHookEvent, isHookHost, normalizeHookProjectRoot, projectRootFromHookPayload, renderHostDecision, } from "@deftai/directive-core/hooks";
6
+ /**
7
+ * How long to re-poll stdin after an empty first read before concluding the host
8
+ * sent nothing (#2864). Keeps well under Cursor's deposited `timeout: 5` while
9
+ * covering short non-blocking delivery races (reporter matrix rows K/L).
10
+ */
11
+ export const DEFAULT_STDIN_EMPTY_RETRY_MS = 250;
12
+ /** Interval between empty-stdin re-polls while the retry budget remains. */
13
+ export const STDIN_EMPTY_RETRY_INTERVAL_MS = 25;
14
+ function defaultSleepMs(ms) {
15
+ if (ms <= 0)
16
+ return;
17
+ try {
18
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
19
+ }
20
+ catch {
21
+ const end = Date.now() + ms;
22
+ while (Date.now() < end) {
23
+ /* spin — last-resort fallback when Atomics.wait is unavailable */
24
+ }
25
+ }
26
+ }
27
+ /**
28
+ * Read hook stdin with a short empty-read retry budget (#2864).
29
+ *
30
+ * A single `readFileSync(0)` on a non-blocking descriptor can return zero bytes
31
+ * before the host has attached a writer. That is indistinguishable from a true
32
+ * empty payload if we never re-poll. We re-read for up to `emptyRetryMs`; true
33
+ * empty (every poll empty through the budget) still yields empty for the deny
34
+ * path. Delayed payloads that arrive within the budget are accepted.
35
+ */
36
+ export function readStdinHardened(readOnce, options = {}) {
37
+ const emptyRetryMs = options.emptyRetryMs ?? DEFAULT_STDIN_EMPTY_RETRY_MS;
38
+ const sleepMs = options.sleepMs ?? defaultSleepMs;
39
+ const nowMs = options.nowMs ?? (() => Date.now());
40
+ let raw = readOnce();
41
+ if (raw.trim().length > 0 || emptyRetryMs <= 0)
42
+ return raw;
43
+ const deadline = nowMs() + emptyRetryMs;
44
+ while (nowMs() < deadline) {
45
+ sleepMs(STDIN_EMPTY_RETRY_INTERVAL_MS);
46
+ raw = readOnce();
47
+ if (raw.trim().length > 0)
48
+ return raw;
49
+ }
50
+ return raw;
51
+ }
6
52
  function takeValue(argv, index, flag) {
7
53
  const token = argv[index];
8
54
  const prefix = `${flag}=`;
@@ -111,6 +157,22 @@ export function parsePayload(raw) {
111
157
  return { payload: {}, context: { parseFailed: true } };
112
158
  }
113
159
  }
160
+ /**
161
+ * Provider-neutral hook dispatch CLI entry (`deft hook:dispatch` / `deft-hook`).
162
+ *
163
+ * ## Exit-code contract (#2864)
164
+ *
165
+ * | Exit | Meaning |
166
+ * |------|---------|
167
+ * | `0` | A host decision was rendered to stdout (allow **or** deny). Hosts must parse stdout; exit status does **not** encode the verdict. |
168
+ * | `2` | Argv / configuration error (unsupported host/event, missing flags, unrecognized args). |
169
+ *
170
+ * This process never returns `1` for a rendered verdict. Cursor `failClosed`
171
+ * deposits that report "exit code 1" are therefore process-level failures
172
+ * (crash, signal kill under host timeout, spawn environment), not `run()` deny
173
+ * paths. Empty-stdin denies still exit `0` with a Cursor JSON body carrying
174
+ * `code: "stdin-empty"`.
175
+ */
114
176
  export function run(argv, seams = {}) {
115
177
  const args = parseArgs(argv);
116
178
  const writeOut = seams.writeOut ?? ((text) => process.stdout.write(text));
@@ -119,9 +181,14 @@ export function run(argv, seams = {}) {
119
181
  writeErr(`${args.error ?? "invalid hook-dispatch arguments"}\n`);
120
182
  return 2;
121
183
  }
122
- const readStdin = seams.readStdin ?? (() => readFileSync(0, "utf8"));
184
+ const readOnce = seams.readStdin ?? (() => readFileSync(0, "utf8"));
185
+ const rawStdin = readStdinHardened(readOnce, {
186
+ emptyRetryMs: seams.stdinEmptyRetryMs,
187
+ sleepMs: seams.sleepMs,
188
+ nowMs: seams.nowMs,
189
+ });
123
190
  const cwd = (seams.cwd ?? process.cwd)();
124
- const { payload, context: payloadContext } = parsePayload(readStdin());
191
+ const { payload, context: payloadContext } = parsePayload(rawStdin);
125
192
  const projectRoot = normalizeHookProjectRoot(args.projectRoot ? resolve(args.projectRoot) : projectRootFromHookPayload(payload, cwd));
126
193
  const decision = decideHook({
127
194
  host: args.host,
@@ -133,12 +200,16 @@ export function run(argv, seams = {}) {
133
200
  const rendered = renderHostDecision(args.host, decision);
134
201
  if (rendered.length > 0)
135
202
  writeOut(`${rendered}\n`);
136
- if (decision.code === "invalid-input" && args.host === "cursor") {
203
+ if ((decision.code === "invalid-input" || decision.code === "stdin-empty") &&
204
+ args.host === "cursor") {
137
205
  // Keys are already embedded in decision.message; stderr helps operators tailing logs.
138
206
  const keys = hookPayloadTopLevelKeys(payload);
139
207
  if (keys.length > 0) {
140
208
  writeErr(`Directive hook diagnostic: payload top-level keys: ${keys.join(", ")}\n`);
141
209
  }
210
+ else if (decision.code === "stdin-empty") {
211
+ writeErr("Directive hook diagnostic: stdin was empty after empty-read retry budget\n");
212
+ }
142
213
  }
143
214
  if (decision.code === "session-start-degraded")
144
215
  writeErr(`${decision.message}\n`);
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import { resolve } from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
- import { REGISTER_HELP, registerReviewMonitor, } from "@deftai/directive-core/review-monitor";
5
- const PRIMITIVES = new Set(["start_agent", "spawn_subagent", "cursor-task"]);
4
+ import { PLATFORM_PRIMITIVE_SET, REGISTER_HELP, registerReviewMonitor, } from "@deftai/directive-core/review-monitor";
5
+ const PRIMITIVES = PLATFORM_PRIMITIVE_SET;
6
6
  export function parseRegisterArgs(argv) {
7
7
  const acc = {
8
8
  pr: null,
@@ -1,23 +1,34 @@
1
1
  #!/usr/bin/env node
2
2
  import { type LiveOpenIssuesReader } from "@deftai/directive-core/dist/triage/queue/index.js";
3
- interface ParsedArgs {
3
+ export type ShowFormat = "default" | "operator";
4
+ interface CommonArgs {
4
5
  projectRoot: string;
5
6
  repo: string | null;
7
+ cacheRoot: string | null;
8
+ auditLog: string | null;
9
+ error?: string;
10
+ }
11
+ interface QueueArgs extends CommonArgs {
12
+ cmd: "queue";
6
13
  limit: number;
7
14
  includeBlocked: boolean;
8
15
  reconcile: boolean;
9
- cacheRoot: string | null;
10
- auditLog: string | null;
11
16
  slicesLog: string | null;
12
- error?: string;
13
17
  }
14
- /** Parse triage-queue CLI args for the queue subcommand. */
15
- export declare function parseArgs(argv: string[]): ParsedArgs;
18
+ interface ShowArgs extends CommonArgs {
19
+ cmd: "show";
20
+ number: number | null;
21
+ format: ShowFormat;
22
+ }
23
+ /** Parse triage-queue CLI args (queue subcommand — default). */
24
+ export declare function parseArgs(argv: string[]): QueueArgs;
25
+ /** Parse show subcommand args. */
26
+ export declare function parseShowArgs(argv: string[]): ShowArgs;
16
27
  /** Optional injection seam for `run` (tests supply a stub live-open reader). */
17
28
  export interface RunOptions {
18
29
  readonly liveOpenReader?: LiveOpenIssuesReader;
19
30
  }
20
- /** Run triage:queue and return process exit code. */
31
+ /** Run triage:queue or triage:show and return process exit code. */
21
32
  export declare function run(argv: string[], options?: RunOptions): number;
22
33
  export {};
23
34
  //# sourceMappingURL=triage-queue.d.ts.map
@@ -1,20 +1,88 @@
1
1
  #!/usr/bin/env node
2
+ /**
3
+ * triage:queue / triage:show CLI (#1128 / #2890).
4
+ *
5
+ * Subcommands:
6
+ * queue — ranked queue (default when first token is not show/audit)
7
+ * show — per-issue detail; --format=operator for Phase 3 brief backbone
8
+ *
9
+ * audit remains routed here by Taskfile; not reimplemented in this story.
10
+ */
2
11
  import { resolve } from "node:path";
3
12
  import { fileURLToPath } from "node:url";
4
13
  import { ensureTriageCacheHydrated } from "@deftai/directive-core/dist/cache/empty-populate.js";
14
+ import { findByIssue } from "@deftai/directive-core/dist/triage/actions/candidates-log.js";
5
15
  import { resolveTriageCachePath } from "@deftai/directive-core/dist/triage/cache-path.js";
6
- import { activeReferencedIssueNumbers, buildQueue, collectOrphanIssueNumbers, DEFAULT_QUEUE_LIMIT, loadCachedIssues, loadSliceRecords, readAuditEntries, reconcileLiveOpenState, renderQueue, resolveRankingLabels, resolveRepo, } from "@deftai/directive-core/dist/triage/queue/index.js";
16
+ import { activeReferencedIssueNumbers, buildQueue, collectOrphanIssueNumbers, DEFAULT_QUEUE_LIMIT, loadCachedIssueDetail, loadCachedIssues, loadSliceRecords, readAuditEntries, reconcileLiveOpenState, renderOperatorBrief, renderQueue, renderShow, resolveRankingLabels, resolveRepo, } from "@deftai/directive-core/dist/triage/queue/index.js";
7
17
  import { resolveScopeIgnores } from "@deftai/directive-core/dist/triage/scope-drift/index.js";
8
- /** Parse triage-queue CLI args for the queue subcommand. */
9
- export function parseArgs(argv) {
10
- const parsed = {
18
+ function baseArgs() {
19
+ return {
11
20
  projectRoot: process.env.DEFT_PROJECT_ROOT ?? ".",
12
21
  repo: process.env.DEFT_TRIAGE_REPO ?? null,
22
+ cacheRoot: null,
23
+ auditLog: null,
24
+ };
25
+ }
26
+ function parseCommonFlag(arg, argv, i, parsed) {
27
+ if (arg === "--project-root") {
28
+ const value = argv[i + 1];
29
+ if (value === undefined) {
30
+ return { consumed: 0, error: "argument --project-root: expected one argument" };
31
+ }
32
+ parsed.projectRoot = value;
33
+ return { consumed: 1 };
34
+ }
35
+ if (arg?.startsWith("--project-root=")) {
36
+ parsed.projectRoot = arg.slice("--project-root=".length);
37
+ return { consumed: 0 };
38
+ }
39
+ if (arg === "--repo") {
40
+ const value = argv[i + 1];
41
+ if (value === undefined) {
42
+ return { consumed: 0, error: "argument --repo: expected one argument" };
43
+ }
44
+ parsed.repo = value;
45
+ return { consumed: 1 };
46
+ }
47
+ if (arg?.startsWith("--repo=")) {
48
+ parsed.repo = arg.slice("--repo=".length);
49
+ return { consumed: 0 };
50
+ }
51
+ if (arg === "--cache-root") {
52
+ const value = argv[i + 1];
53
+ if (value === undefined) {
54
+ return { consumed: 0, error: "argument --cache-root: expected one argument" };
55
+ }
56
+ parsed.cacheRoot = value;
57
+ return { consumed: 1 };
58
+ }
59
+ if (arg?.startsWith("--cache-root=")) {
60
+ parsed.cacheRoot = arg.slice("--cache-root=".length);
61
+ return { consumed: 0 };
62
+ }
63
+ if (arg === "--audit-log") {
64
+ const value = argv[i + 1];
65
+ if (value === undefined) {
66
+ return { consumed: 0, error: "argument --audit-log: expected one argument" };
67
+ }
68
+ parsed.auditLog = value;
69
+ return { consumed: 1 };
70
+ }
71
+ if (arg?.startsWith("--audit-log=")) {
72
+ parsed.auditLog = arg.slice("--audit-log=".length);
73
+ return { consumed: 0 };
74
+ }
75
+ return null;
76
+ }
77
+ /** Parse triage-queue CLI args (queue subcommand — default). */
78
+ export function parseArgs(argv) {
79
+ const common = baseArgs();
80
+ const parsed = {
81
+ ...common,
82
+ cmd: "queue",
13
83
  limit: DEFAULT_QUEUE_LIMIT,
14
84
  includeBlocked: false,
15
85
  reconcile: true,
16
- cacheRoot: null,
17
- auditLog: null,
18
86
  slicesLog: null,
19
87
  };
20
88
  for (let i = 0; i < argv.length; i += 1) {
@@ -30,30 +98,12 @@ export function parseArgs(argv) {
30
98
  parsed.reconcile = false;
31
99
  continue;
32
100
  }
33
- if (arg === "--project-root") {
34
- const value = argv[i + 1];
35
- if (value === undefined) {
36
- return { ...parsed, error: "argument --project-root: expected one argument" };
101
+ const commonHit = parseCommonFlag(arg, argv, i, parsed);
102
+ if (commonHit !== null) {
103
+ if (commonHit.error !== undefined) {
104
+ return { ...parsed, error: commonHit.error };
37
105
  }
38
- parsed.projectRoot = value;
39
- i += 1;
40
- continue;
41
- }
42
- if (arg?.startsWith("--project-root=")) {
43
- parsed.projectRoot = arg.slice("--project-root=".length);
44
- continue;
45
- }
46
- if (arg === "--repo") {
47
- const value = argv[i + 1];
48
- if (value === undefined) {
49
- return { ...parsed, error: "argument --repo: expected one argument" };
50
- }
51
- parsed.repo = value;
52
- i += 1;
53
- continue;
54
- }
55
- if (arg?.startsWith("--repo=")) {
56
- parsed.repo = arg.slice("--repo=".length);
106
+ i += commonHit.consumed;
57
107
  continue;
58
108
  }
59
109
  if (arg === "--limit") {
@@ -78,54 +128,90 @@ export function parseArgs(argv) {
78
128
  parsed.limit = parsedLimit;
79
129
  continue;
80
130
  }
81
- if (arg === "--cache-root") {
131
+ if (arg === "--slices-log") {
82
132
  const value = argv[i + 1];
83
133
  if (value === undefined) {
84
- return { ...parsed, error: "argument --cache-root: expected one argument" };
134
+ return { ...parsed, error: "argument --slices-log: expected one argument" };
85
135
  }
86
- parsed.cacheRoot = value;
136
+ parsed.slicesLog = value;
87
137
  i += 1;
88
138
  continue;
89
139
  }
90
- if (arg?.startsWith("--cache-root=")) {
91
- parsed.cacheRoot = arg.slice("--cache-root=".length);
140
+ if (arg?.startsWith("--slices-log=")) {
141
+ parsed.slicesLog = arg.slice("--slices-log=".length);
92
142
  continue;
93
143
  }
94
- if (arg === "--audit-log") {
95
- const value = argv[i + 1];
96
- if (value === undefined) {
97
- return { ...parsed, error: "argument --audit-log: expected one argument" };
98
- }
99
- parsed.auditLog = value;
100
- i += 1;
101
- continue;
144
+ if (arg?.startsWith("-")) {
145
+ return { ...parsed, error: `unrecognized argument: ${arg}` };
102
146
  }
103
- if (arg?.startsWith("--audit-log=")) {
104
- parsed.auditLog = arg.slice("--audit-log=".length);
147
+ }
148
+ return parsed;
149
+ }
150
+ /** Parse show subcommand args. */
151
+ export function parseShowArgs(argv) {
152
+ const common = baseArgs();
153
+ const parsed = {
154
+ ...common,
155
+ cmd: "show",
156
+ number: null,
157
+ format: "default",
158
+ };
159
+ // Skip leading "show" token if present
160
+ let start = 0;
161
+ if (argv[0] === "show") {
162
+ start = 1;
163
+ }
164
+ for (let i = start; i < argv.length; i += 1) {
165
+ const arg = argv[i];
166
+ const commonHit = parseCommonFlag(arg, argv, i, parsed);
167
+ if (commonHit !== null) {
168
+ if (commonHit.error !== undefined) {
169
+ return { ...parsed, error: commonHit.error };
170
+ }
171
+ i += commonHit.consumed;
105
172
  continue;
106
173
  }
107
- if (arg === "--slices-log") {
174
+ if (arg === "--format") {
108
175
  const value = argv[i + 1];
109
176
  if (value === undefined) {
110
- return { ...parsed, error: "argument --slices-log: expected one argument" };
177
+ return { ...parsed, error: "argument --format: expected one argument" };
111
178
  }
112
- parsed.slicesLog = value;
179
+ if (value !== "default" && value !== "operator" && value !== "plain" && value !== "text") {
180
+ return {
181
+ ...parsed,
182
+ error: `argument --format: invalid choice: '${value}' (choose from default, operator)`,
183
+ };
184
+ }
185
+ parsed.format = value === "operator" ? "operator" : "default";
113
186
  i += 1;
114
187
  continue;
115
188
  }
116
- if (arg?.startsWith("--slices-log=")) {
117
- parsed.slicesLog = arg.slice("--slices-log=".length);
189
+ if (arg?.startsWith("--format=")) {
190
+ const value = arg.slice("--format=".length);
191
+ if (value !== "default" && value !== "operator" && value !== "plain" && value !== "text") {
192
+ return {
193
+ ...parsed,
194
+ error: `argument --format: invalid choice: '${value}' (choose from default, operator)`,
195
+ };
196
+ }
197
+ parsed.format = value === "operator" ? "operator" : "default";
118
198
  continue;
119
199
  }
120
200
  if (arg?.startsWith("-")) {
121
201
  return { ...parsed, error: `unrecognized argument: ${arg}` };
122
202
  }
203
+ // positional issue number — strict positive int (reject "42abc" / "42.5")
204
+ if (!/^[1-9]\d*$/u.test(arg ?? "")) {
205
+ return { ...parsed, error: `argument number: invalid int value: '${arg}'` };
206
+ }
207
+ parsed.number = Number(arg);
208
+ }
209
+ if (parsed.number === null && parsed.error === undefined) {
210
+ return { ...parsed, error: "triage:show: issue number is required (e.g. triage:show -- 42)" };
123
211
  }
124
212
  return parsed;
125
213
  }
126
- /** Run triage:queue and return process exit code. */
127
- export function run(argv, options = {}) {
128
- const args = parseArgs(argv);
214
+ function runQueue(args, options = {}) {
129
215
  if (args.error !== undefined) {
130
216
  process.stderr.write(`triage_queue: ${args.error}\n`);
131
217
  return 2;
@@ -138,10 +224,6 @@ export function run(argv, options = {}) {
138
224
  }
139
225
  ensureTriageCacheHydrated(projectRoot, { repo });
140
226
  const cachedForQueue = loadCachedIssues(repo, { projectRoot });
141
- // Reconcile the cached candidate set against live open/closed state so a
142
- // just-closed/merged issue never lingers in the queue as [untriaged] until
143
- // the cache refreshes (#2238). Fails open: a read error passes candidates
144
- // through unchanged rather than emptying the queue.
145
227
  const issuesForQueue = args.reconcile
146
228
  ? reconcileLiveOpenState(cachedForQueue, repo, options.liveOpenReader)
147
229
  : cachedForQueue;
@@ -177,6 +259,69 @@ export function run(argv, options = {}) {
177
259
  })}\n`);
178
260
  return 0;
179
261
  }
262
+ function runShow(args) {
263
+ if (args.error !== undefined) {
264
+ process.stderr.write(`triage:show: ${args.error}\n`);
265
+ return 2;
266
+ }
267
+ const projectRoot = resolve(args.projectRoot);
268
+ const repo = resolveRepo(args.repo, projectRoot);
269
+ if (repo === null) {
270
+ process.stderr.write("triage:show: --repo OWNER/NAME (or $DEFT_TRIAGE_REPO) is required.\n");
271
+ return 2;
272
+ }
273
+ const number = args.number;
274
+ if (number === null) {
275
+ process.stderr.write("triage:show: issue number is required\n");
276
+ return 2;
277
+ }
278
+ const cacheRoot = args.cacheRoot !== null && args.cacheRoot.length > 0 ? resolve(args.cacheRoot) : null;
279
+ ensureTriageCacheHydrated(projectRoot, {
280
+ repo,
281
+ ...(cacheRoot !== null ? { cacheRoot } : {}),
282
+ });
283
+ const issue = loadCachedIssueDetail(repo, number, {
284
+ projectRoot,
285
+ cacheRoot,
286
+ });
287
+ const logPath = args.auditLog ?? resolveTriageCachePath(projectRoot, "candidates.jsonl");
288
+ const history = findByIssue(number, repo, logPath)
289
+ .slice()
290
+ .sort((a, b) => String(a.timestamp ?? "").localeCompare(String(b.timestamp ?? "")));
291
+ const latest = history.length > 0 ? (history[history.length - 1] ?? null) : null;
292
+ const activeRefs = activeReferencedIssueNumbers(projectRoot);
293
+ const inActive = activeRefs.has(number);
294
+ if (args.format === "operator") {
295
+ process.stdout.write(`${renderOperatorBrief({
296
+ issue,
297
+ repo,
298
+ number,
299
+ latestDecision: latest,
300
+ inActiveXbrief: inActive,
301
+ })}\n`);
302
+ }
303
+ else {
304
+ process.stdout.write(`${renderShow({
305
+ issue,
306
+ repo,
307
+ number,
308
+ latestDecision: latest,
309
+ history,
310
+ inActiveXbrief: inActive,
311
+ })}\n`);
312
+ }
313
+ return issue !== null ? 0 : 1;
314
+ }
315
+ /** Run triage:queue or triage:show and return process exit code. */
316
+ export function run(argv, options = {}) {
317
+ const first = argv[0];
318
+ if (first === "show") {
319
+ return runShow(parseShowArgs(argv));
320
+ }
321
+ // Other subcommands (e.g. audit) keep prior queue-module routing until
322
+ // separately ported; only `show` is specialized here (#2890).
323
+ return runQueue(parseArgs(argv), options);
324
+ }
180
325
  if (process.argv[1] !== undefined && fileURLToPath(import.meta.url) === process.argv[1]) {
181
326
  process.exit(run(process.argv.slice(2)));
182
327
  }
@@ -2,6 +2,7 @@
2
2
  interface ParsedArgs {
3
3
  vbriefPath: string | null;
4
4
  emitJson: boolean;
5
+ help?: boolean;
5
6
  error?: string;
6
7
  }
7
8
  /** Parse vbrief-preflight / xbrief:preflight CLI args (#810 / #2449). */
@@ -1,6 +1,15 @@
1
1
  #!/usr/bin/env node
2
2
  import { fileURLToPath } from "node:url";
3
3
  import { emitJson, evaluate, PREFLIGHT_USAGE_HINT } from "@deftai/directive-core/preflight";
4
+ const HELP_TEXT = `usage: xbrief:preflight [--vbrief-path PATH | PATH] [--json] [--help]
5
+
6
+ Implementation-intent gate (#810): exits 0 only when the xBRIEF is in
7
+ xbrief/active/ (or legacy vbrief/active/) AND plan.status == 'running'.
8
+
9
+ Examples:
10
+ deft xbrief:preflight -- xbrief/active/<story>.xbrief.json
11
+ deft xbrief:preflight --vbrief-path xbrief/active/<story>.xbrief.json
12
+ `;
4
13
  const PATH_FLAG_NAMES = ["--vbrief-path", "--brief-path", "--xbrief-path"];
5
14
  function assignPathFlag(parsed, value) {
6
15
  if (value === undefined || value.length === 0) {
@@ -19,15 +28,30 @@ export function parseArgs(argv) {
19
28
  };
20
29
  for (let i = 0; i < argv.length; i += 1) {
21
30
  const arg = argv[i];
31
+ if (arg === "--") {
32
+ continue;
33
+ }
22
34
  if (arg === "--json") {
23
35
  parsed.emitJson = true;
24
36
  continue;
25
37
  }
26
38
  if (arg === "--help" || arg === "-h") {
27
- return parsed;
39
+ return { ...parsed, help: true };
28
40
  }
29
41
  if (PATH_FLAG_NAMES.includes(arg)) {
30
- const next = assignPathFlag(parsed, argv[i + 1]);
42
+ // Task wrappers bake `--vbrief-path {{.CLI_ARGS}}`, so
43
+ // `task xbrief:preflight -- --help` arrives as `--vbrief-path --help`.
44
+ const nextToken = argv[i + 1];
45
+ if (nextToken === "--help" || nextToken === "-h") {
46
+ return { ...parsed, help: true };
47
+ }
48
+ if (nextToken !== undefined && nextToken.startsWith("-")) {
49
+ return {
50
+ ...parsed,
51
+ error: "argument --vbrief-path: expected one argument",
52
+ };
53
+ }
54
+ const next = assignPathFlag(parsed, nextToken);
31
55
  if (next?.error !== undefined)
32
56
  return next;
33
57
  parsed.vbriefPath = next?.vbriefPath ?? null;
@@ -66,6 +90,10 @@ export function parseArgs(argv) {
66
90
  /** Run the gate and return the process exit code (parse errors -> 2). */
67
91
  export function run(argv) {
68
92
  const args = parseArgs(argv);
93
+ if (args.help) {
94
+ process.stdout.write(HELP_TEXT);
95
+ return 0;
96
+ }
69
97
  if (args.error !== undefined) {
70
98
  process.stderr.write(`preflight_implementation: ${args.error}\n`);
71
99
  return 2;
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { resolve } from "node:path";
3
- import { fileURLToPath } from "node:url";
4
3
  import { evaluate } from "@deftai/directive-core";
4
+ import { isDirectEntrypoint } from "./entrypoint.js";
5
5
  /** Parse the verify-encoding CLI args, mirroring the Python argparse surface. */
6
6
  export function parseArgs(argv) {
7
7
  const parsed = {
@@ -77,10 +77,7 @@ export function run(argv) {
77
77
  return result.exitCode;
78
78
  }
79
79
  // Only execute when invoked directly as a binary (not when imported in tests).
80
- // Normalize both sides via fileURLToPath so the guard fires on Windows too,
81
- // where process.argv[1] is a native backslash path and import.meta.url is a
82
- // forward-slash file:// URL -- a raw `file://${process.argv[1]}` never matches.
83
- if (process.argv[1] !== undefined && fileURLToPath(import.meta.url) === process.argv[1]) {
80
+ if (isDirectEntrypoint(import.meta.url)) {
84
81
  process.exit(run(process.argv.slice(2)));
85
82
  }
86
83
  //# sourceMappingURL=verify-encoding.js.map
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env node
2
+ interface ParsedArgs {
3
+ projectRoot: string;
4
+ quiet: boolean;
5
+ error?: string;
6
+ }
7
+ /** Parse verify-openclaw-tier1 CLI args (#2875). */
8
+ export declare function parseArgs(argv: string[]): ParsedArgs;
9
+ /** Run the gate and return the process exit code. */
10
+ export declare function run(argv: string[]): number;
11
+ export {};
12
+ //# sourceMappingURL=verify-openclaw-tier1.d.ts.map
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/env node
2
+ import { resolve } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { evaluateOpenclawTier1 } from "@deftai/directive-core/verify-source";
5
+ /** Parse verify-openclaw-tier1 CLI args (#2875). */
6
+ export function parseArgs(argv) {
7
+ const parsed = { projectRoot: ".", quiet: false };
8
+ for (let i = 0; i < argv.length; i += 1) {
9
+ const arg = argv[i];
10
+ if (arg === "--quiet") {
11
+ parsed.quiet = true;
12
+ }
13
+ else if (arg === "--project-root") {
14
+ const value = argv[i + 1];
15
+ if (value === undefined) {
16
+ return { ...parsed, error: "argument --project-root: expected one argument" };
17
+ }
18
+ parsed.projectRoot = value;
19
+ i += 1;
20
+ }
21
+ else if (arg?.startsWith("--project-root=")) {
22
+ parsed.projectRoot = arg.slice("--project-root=".length);
23
+ }
24
+ else {
25
+ return { ...parsed, error: `unrecognized argument: ${arg}` };
26
+ }
27
+ }
28
+ return parsed;
29
+ }
30
+ /** Run the gate and return the process exit code. */
31
+ export function run(argv) {
32
+ const args = parseArgs(argv);
33
+ if (args.error !== undefined) {
34
+ process.stderr.write(`verify_openclaw_tier1: ${args.error}\n`);
35
+ return 2;
36
+ }
37
+ const result = evaluateOpenclawTier1(resolve(args.projectRoot), { quiet: args.quiet });
38
+ if (result.code === 0) {
39
+ if (!args.quiet && result.message.length > 0) {
40
+ process.stdout.write(`${result.message}\n`);
41
+ }
42
+ }
43
+ else {
44
+ process.stderr.write(`${result.message}\n`);
45
+ }
46
+ return result.code;
47
+ }
48
+ if (process.argv[1] !== undefined && fileURLToPath(import.meta.url) === process.argv[1]) {
49
+ process.exit(run(process.argv.slice(2)));
50
+ }
51
+ //# sourceMappingURL=verify-openclaw-tier1.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deftai/directive",
3
- "version": "0.85.0",
3
+ "version": "0.87.0",
4
4
  "description": "Directive CLI — npm install path for the Deft Directive framework.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -32,8 +32,8 @@
32
32
  "provenance": true
33
33
  },
34
34
  "dependencies": {
35
- "@deftai/directive-core": "^0.85.0",
36
- "@deftai/directive-content": "^0.85.0"
35
+ "@deftai/directive-core": "^0.87.0",
36
+ "@deftai/directive-content": "^0.87.0"
37
37
  },
38
38
  "scripts": {
39
39
  "build": "tsc -b"