@deftai/directive-core 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.
Files changed (50) hide show
  1. package/dist/check/gate-lists.js +1 -0
  2. package/dist/doctor/constants.d.ts +2 -2
  3. package/dist/doctor/constants.js +2 -2
  4. package/dist/doctor/main.d.ts +8 -6
  5. package/dist/doctor/main.js +102 -36
  6. package/dist/doctor/taskfile.d.ts +8 -0
  7. package/dist/doctor/taskfile.js +19 -0
  8. package/dist/doctor/types.d.ts +3 -0
  9. package/dist/hooks/dispatcher.d.ts +21 -1
  10. package/dist/hooks/dispatcher.js +85 -15
  11. package/dist/init-deposit/agent-hooks.js +14 -6
  12. package/dist/init-deposit/gitignore.js +5 -0
  13. package/dist/intake/issue-emit.d.ts +45 -2
  14. package/dist/intake/issue-emit.js +420 -17
  15. package/dist/intake/issue-ingest.js +54 -4
  16. package/dist/platform/platform-capabilities.js +3 -0
  17. package/dist/policy/org-force-on-migration.js +2 -0
  18. package/dist/render/framework-commands.d.ts +1 -1
  19. package/dist/render/framework-commands.js +6 -6
  20. package/dist/render/roadmap-render.d.ts +5 -1
  21. package/dist/render/roadmap-render.js +20 -2
  22. package/dist/render/rule-map.js +5 -0
  23. package/dist/review-monitor/constants.js +3 -2
  24. package/dist/review-monitor/tier-detection.d.ts +6 -2
  25. package/dist/review-monitor/tier-detection.js +27 -2
  26. package/dist/scope/transition.js +43 -0
  27. package/dist/session/release-availability.d.ts +2 -0
  28. package/dist/session/release-availability.js +23 -8
  29. package/dist/swarm/routing-set-cli.js +5 -10
  30. package/dist/swarm/routing.d.ts +3 -2
  31. package/dist/swarm/routing.js +16 -4
  32. package/dist/triage/help/registry-data.d.ts +7 -7
  33. package/dist/triage/help/registry-data.js +15 -6
  34. package/dist/triage/queue/index.d.ts +1 -0
  35. package/dist/triage/queue/index.js +1 -0
  36. package/dist/triage/queue/show.d.ts +69 -0
  37. package/dist/triage/queue/show.js +293 -0
  38. package/dist/triage/scope/cli.js +3 -0
  39. package/dist/triage/scope/coverage.d.ts +2 -0
  40. package/dist/triage/scope/coverage.js +18 -3
  41. package/dist/verify-env/agent-hooks-live-probe.d.ts +32 -0
  42. package/dist/verify-env/agent-hooks-live-probe.js +216 -0
  43. package/dist/verify-env/index.d.ts +1 -0
  44. package/dist/verify-env/index.js +1 -0
  45. package/dist/verify-source/index.d.ts +1 -0
  46. package/dist/verify-source/index.js +1 -0
  47. package/dist/verify-source/openclaw-tier1.d.ts +37 -0
  48. package/dist/verify-source/openclaw-tier1.js +100 -0
  49. package/dist/xbrief-migrate/migrate-project.js +35 -22
  50. package/package.json +3 -3
@@ -1,6 +1,7 @@
1
1
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { join, resolve } from "node:path";
3
3
  import { readCorePackageVersion } from "../engine-version.js";
4
+ import { assertWriteTargetSafe } from "../fs/projection-containment.js";
4
5
  import { atomicWriteProjectDefinition, projectDefinitionMutationLock, } from "../vbrief-build/project-definition-io.js";
5
6
  import { migrateLegacyPolicyKey, PLAN_POLICY_KEY, readPlanPolicy } from "./plan-extensions.js";
6
7
  import { appendAuditLog, loadProjectDefinition, projectDefinitionPath } from "./resolve.js";
@@ -56,6 +57,7 @@ export function readOrgForceOnMarker(projectRoot) {
56
57
  }
57
58
  function writeOrgForceOnMarker(projectRoot, marker) {
58
59
  const path = markerPath(projectRoot);
60
+ assertWriteTargetSafe(projectRoot, path);
59
61
  mkdirSync(join(path, ".."), { recursive: true });
60
62
  writeFileSync(path, `${JSON.stringify(marker, null, 2)}\n`, "utf8");
61
63
  }
@@ -25,7 +25,7 @@ export declare function formatFrameworkCommand(args: readonly string[], options?
25
25
  surface?: string;
26
26
  taskPrefix?: string | null;
27
27
  }): string;
28
- export declare function cmdCoreValidate(argv: readonly string[]): number;
28
+ export declare function cmdCoreValidate(argv: readonly string[], root?: string): number;
29
29
  export declare function cmdCoreLint(_argv: readonly string[]): number;
30
30
  export declare function cmdCoreTest(_argv: readonly string[]): number;
31
31
  export declare function resolveFrameworkRoot(): string;
@@ -154,7 +154,7 @@ export function formatFrameworkCommand(args, options = {}) {
154
154
  }
155
155
  return [surface, ...parts].join(" ");
156
156
  }
157
- const EXCLUDE_PARTS = new Set([".git", "backup"]);
157
+ const EXCLUDE_PARTS = new Set([".git", "backup", "node_modules", ".deft-scratch"]);
158
158
  function collectMarkdownFiles(root) {
159
159
  const out = [];
160
160
  const walk = (dir, parts) => {
@@ -189,12 +189,12 @@ function collectMarkdownFiles(root) {
189
189
  walk(root, []);
190
190
  return out.sort();
191
191
  }
192
- export function cmdCoreValidate(argv) {
192
+ export function cmdCoreValidate(argv, root = ".") {
193
193
  if (argv.length > 0) {
194
194
  process.stderr.write(`error: core:validate does not accept arguments: ${argv.join(" ")}\n`);
195
195
  return 2;
196
196
  }
197
- const files = collectMarkdownFiles(".");
197
+ const files = collectMarkdownFiles(root);
198
198
  for (const path of files)
199
199
  process.stdout.write(`✓ ${path}\n`);
200
200
  process.stdout.write(`✓ All ${files.length} markdown files validated\n`);
@@ -271,7 +271,7 @@ function runBuildDistArgv(argv) {
271
271
  return result.status ?? 1;
272
272
  }
273
273
  const TS_INLINE = {
274
- "framework_commands:_cmd_core_validate": (argv) => cmdCoreValidate(argv),
274
+ "framework_commands:_cmd_core_validate": (argv, cwd) => cmdCoreValidate(argv, cwd),
275
275
  "doctor:cmd_doctor": (argv) => cmdDoctor(argv),
276
276
  "build_dist:main": (argv) => runBuildDistArgv(argv),
277
277
  };
@@ -349,7 +349,7 @@ function invokeEntrypoint(entrypoint, argv, cwd, frameworkRoot, noArgv, capture)
349
349
  };
350
350
  let code;
351
351
  try {
352
- code = inline(argv);
352
+ code = inline(argv, cwd);
353
353
  }
354
354
  finally {
355
355
  process.stdout.write = prevOut;
@@ -357,7 +357,7 @@ function invokeEntrypoint(entrypoint, argv, cwd, frameworkRoot, noArgv, capture)
357
357
  }
358
358
  return { code, stdout: chunks.out, stderr: chunks.err };
359
359
  }
360
- return { code: inline(argv), stdout: "", stderr: "" };
360
+ return { code: inline(argv, cwd), stdout: "", stderr: "" };
361
361
  }
362
362
  const verb = ENTRYPOINT_VERB[entrypoint];
363
363
  if (verb) {
@@ -3,7 +3,11 @@ export declare function renderRoadmapToBuffer(pendingDir: string, completedDir?:
3
3
  /** @deprecated Prefer ``renderRoadmapToBuffer`` — kept for existing imports and parity harnesses. */
4
4
  export declare function generateRoadmapContent(pendingDir: string, completedDir?: string): string;
5
5
  export type RenderRoadmapResult = readonly [boolean, string];
6
- export declare function renderRoadmap(pendingDir: string, outPath: string, completedDir?: string): RenderRoadmapResult;
6
+ export type RenderRoadmapOptions = {
7
+ completedDir?: string;
8
+ projectRoot?: string;
9
+ };
10
+ export declare function renderRoadmap(pendingDir: string, outPath: string, completedDirOrOptions?: string | RenderRoadmapOptions): RenderRoadmapResult;
7
11
  export declare function checkDrift(pendingDir: string, roadmapPath: string, completedDir?: string): RenderRoadmapResult;
8
12
  /** CLI entry (mirrors ``scripts/roadmap_render.main``). */
9
13
  export declare function main(argv: readonly string[]): number;
@@ -1,5 +1,6 @@
1
1
  import { existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { dirname, join, resolve } from "node:path";
3
+ import { assertWriteTargetSafe } from "../fs/projection-containment.js";
3
4
  import { hasArtifactSuffix, resolveLifecycleRoot } from "../layout/resolve.js";
4
5
  import { MIGRATOR_METADATA_KEY, ROADMAP_BANNER } from "./constants.js";
5
6
  import { phaseSortKey } from "./text-utils.js";
@@ -351,9 +352,22 @@ export function renderRoadmapToBuffer(pendingDir, completedDir) {
351
352
  export function generateRoadmapContent(pendingDir, completedDir) {
352
353
  return renderRoadmapToBuffer(pendingDir, completedDir);
353
354
  }
354
- export function renderRoadmap(pendingDir, outPath, completedDir) {
355
+ export function renderRoadmap(pendingDir, outPath, completedDirOrOptions) {
356
+ let completedDir;
357
+ let projectRoot;
358
+ if (typeof completedDirOrOptions === "string") {
359
+ completedDir = completedDirOrOptions;
360
+ }
361
+ else if (completedDirOrOptions !== undefined) {
362
+ completedDir = completedDirOrOptions.completedDir;
363
+ projectRoot = completedDirOrOptions.projectRoot;
364
+ }
355
365
  try {
356
366
  const content = renderRoadmapToBuffer(pendingDir, completedDir);
367
+ // Trust boundary is the project root — never dirname(outPath), which follows a
368
+ // diverted parent symlink and would make containment pass outside the checkout.
369
+ const projectDir = projectRoot !== undefined ? resolve(projectRoot) : resolve(pendingDir, "..", "..");
370
+ assertWriteTargetSafe(projectDir, resolve(outPath));
357
371
  writeFileSync(outPath, content, "utf8");
358
372
  return [true, `✓ Rendered ROADMAP.md to ${outPath}`];
359
373
  }
@@ -415,7 +429,11 @@ export function main(argv) {
415
429
  process.stdout.write(`${msg}\n`);
416
430
  return ok ? 0 : 1;
417
431
  }
418
- const [ok, msg] = renderRoadmap(pendingDir, outPath);
432
+ // When --project-root is set, use it; otherwise renderRoadmap derives from pendingDir
433
+ // (…/xbrief|vbrief/pending → project root). Never use dirname(outPath).
434
+ const [ok, msg] = renderRoadmap(pendingDir, outPath, {
435
+ projectRoot: projectRoot !== undefined ? resolve(projectRoot) : undefined,
436
+ });
419
437
  process.stdout.write(`${msg}\n`);
420
438
  return ok ? 0 : 1;
421
439
  }
@@ -13,6 +13,7 @@
13
13
  import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
14
14
  import { basename, dirname, join, relative, resolve } from "node:path";
15
15
  import { fileURLToPath } from "node:url";
16
+ import { assertWriteTargetSafe } from "../fs/projection-containment.js";
16
17
  import { TEMPLATE } from "./rule-map-template.js";
17
18
  const MARKER_RE = /^\s*[-*]\s*([!~⊗≉?])\s/;
18
19
  const MARKER_LABEL = {
@@ -684,6 +685,10 @@ export function main(argv) {
684
685
  process.stdout.write("ok: docs/RULE-MAP.md is up to date\n");
685
686
  return 0;
686
687
  }
688
+ // Preflight both destinations before either write so a later HTML refusal
689
+ // cannot leave RULE-MAP.md updated while docs/rule-map/index.html stays stale.
690
+ assertWriteTargetSafe(repo, mdPath);
691
+ assertWriteTargetSafe(repo, htmlPath);
687
692
  mkdirSync(dirname(mdPath), { recursive: true });
688
693
  writeFileSync(mdPath, md, "utf8");
689
694
  mkdirSync(htmlDir, { recursive: true });
@@ -40,7 +40,7 @@ export const REVIEW_MONITOR_HELP = "usage: task verify:review-monitor -- --pr <N
40
40
  "\n" +
41
41
  "Claim a lease after spawning Approach 1:\n" +
42
42
  " task review-monitor:register -- --pr <N> --monitor-agent-id <id> \\\n" +
43
- " --platform-primitive cursor-task|spawn_subagent|start_agent \\\n" +
43
+ " --platform-primitive cursor-task|spawn_subagent|start_agent|sessions_spawn \\\n" +
44
44
  " [--head-sha SHA] [--repo OWNER/REPO] [--force]\n" +
45
45
  "\n" +
46
46
  "Release when done:\n" +
@@ -54,7 +54,8 @@ export const REGISTER_HELP = "usage: task review-monitor:register -- --pr <N> --
54
54
  "required:\n" +
55
55
  " --pr N Pull request number\n" +
56
56
  " --monitor-agent-id ID Stable poller agent id / Task handle\n" +
57
- " --platform-primitive P start_agent | spawn_subagent | cursor-task\n" +
57
+ " --platform-primitive P start_agent | spawn_subagent | cursor-task |\n" +
58
+ " sessions_spawn | openclaw-sessions-spawn (#2876)\n" +
58
59
  "\n" +
59
60
  "options:\n" +
60
61
  " --repo OWNER/REPO Repository (default: origin / DEFT_TRIAGE_REPO)\n" +
@@ -1,5 +1,9 @@
1
1
  import { MONITORING_TIER_1, MONITORING_TIER_2, MONITORING_TIER_3 } from "./constants.js";
2
- export type PlatformPrimitive = "start_agent" | "spawn_subagent" | "cursor-task";
2
+ /** Canonical Approach-1 platform primitives for review-monitor register/verify (#2655 / #2876). */
3
+ export type PlatformPrimitive = "start_agent" | "spawn_subagent" | "cursor-task" | "sessions_spawn" | "openclaw-sessions-spawn";
4
+ /** Accepted `--platform-primitive` values (register CLI + help text). */
5
+ export declare const PLATFORM_PRIMITIVES: readonly PlatformPrimitive[];
6
+ export declare const PLATFORM_PRIMITIVE_SET: Set<string>;
3
7
  export interface MonitoringTierProbe {
4
8
  readonly tier: typeof MONITORING_TIER_1 | typeof MONITORING_TIER_2 | typeof MONITORING_TIER_3;
5
9
  readonly primitive: PlatformPrimitive | null;
@@ -7,7 +11,7 @@ export interface MonitoringTierProbe {
7
11
  }
8
12
  /**
9
13
  * Inline Tier-1 detection aligned with the swarm Phase 3 / review-cycle matrix
10
- * (#1877 / #2655). Prefer `task platform:capabilities` when available (#1357);
14
+ * (#1877 / #2655 / #2876). Prefer `task platform:capabilities` when available (#1357);
11
15
  * this probe does not block MVP.
12
16
  */
13
17
  export declare function probeMonitoringTier(environ?: NodeJS.ProcessEnv): MonitoringTierProbe;
@@ -1,12 +1,24 @@
1
1
  import { MONITORING_TIER_1, MONITORING_TIER_2, MONITORING_TIER_3 } from "./constants.js";
2
2
  const TRUTHY = new Set(["1", "true", "yes", "on"]);
3
+ /** Accepted `--platform-primitive` values (register CLI + help text). */
4
+ export const PLATFORM_PRIMITIVES = [
5
+ "start_agent",
6
+ "spawn_subagent",
7
+ "cursor-task",
8
+ "sessions_spawn",
9
+ "openclaw-sessions-spawn",
10
+ ];
11
+ export const PLATFORM_PRIMITIVE_SET = new Set(PLATFORM_PRIMITIVES);
3
12
  function envTruthy(environ, name) {
4
13
  return TRUTHY.has((environ[name] ?? "").trim().toLowerCase());
5
14
  }
6
15
  function probeOverride(environ) {
7
16
  const raw = (environ.DEFT_MONITOR_TIER ?? environ.DEFT_MONITOR_TIER_OVERRIDE ?? "").trim();
8
17
  if (raw === "1" || raw.toLowerCase() === "tier1") {
9
- const primitive = environ.DEFT_MONITOR_TIER1_PRIMITIVE ?? "cursor-task";
18
+ const requested = (environ.DEFT_MONITOR_TIER1_PRIMITIVE ?? "cursor-task").trim();
19
+ const primitive = PLATFORM_PRIMITIVE_SET.has(requested)
20
+ ? requested
21
+ : "cursor-task";
10
22
  return { tier: MONITORING_TIER_1, primitive, descriptor: "override-tier1" };
11
23
  }
12
24
  if (raw === "3" || raw.toLowerCase() === "tier3") {
@@ -16,7 +28,7 @@ function probeOverride(environ) {
16
28
  }
17
29
  /**
18
30
  * Inline Tier-1 detection aligned with the swarm Phase 3 / review-cycle matrix
19
- * (#1877 / #2655). Prefer `task platform:capabilities` when available (#1357);
31
+ * (#1877 / #2655 / #2876). Prefer `task platform:capabilities` when available (#1357);
20
32
  * this probe does not block MVP.
21
33
  */
22
34
  export function probeMonitoringTier(environ = process.env) {
@@ -41,6 +53,19 @@ export function probeMonitoringTier(environ = process.env) {
41
53
  };
42
54
  }
43
55
  const runtime = (environ.DEFT_AGENT_RUNTIME ?? "").trim().toLowerCase();
56
+ // OpenClaw: sessions_spawn is the Tier-1 Approach 1 primitive (#2876).
57
+ // Alias openclaw-sessions-spawn accepted on register for explicit naming.
58
+ if (envTruthy(environ, "DEFT_PROBE_SESSIONS_SPAWN") ||
59
+ envTruthy(environ, "DEFT_HAS_SESSIONS_SPAWN") ||
60
+ envTruthy(environ, "DEFT_PROBE_OPENCLAW") ||
61
+ envTruthy(environ, "OPENCLAW") ||
62
+ runtime === "openclaw" ||
63
+ runtime === "openclaw-sessions-spawn") {
64
+ const alias = (environ.DEFT_MONITOR_TIER1_PRIMITIVE ?? "").trim() === "openclaw-sessions-spawn"
65
+ ? "openclaw-sessions-spawn"
66
+ : "sessions_spawn";
67
+ return { tier: MONITORING_TIER_1, primitive: alias, descriptor: "openclaw" };
68
+ }
44
69
  if (envTruthy(environ, "DEFT_PROBE_GROK_BUILD") ||
45
70
  envTruthy(environ, "GROK_BUILD") ||
46
71
  runtime === "grok-build") {
@@ -11,6 +11,43 @@ import { detectLifecycleFolder, updateDecomposedChildBackReferences, updateDecom
11
11
  import { syncProjectDefinitionAfterScopeMove } from "./project-definition-sync.js";
12
12
  import { syncSpecificationAfterScopeMove } from "./specification-sync.js";
13
13
  import { utcNowIso } from "./vbrief-json.js";
14
+ /** Item statuses that still represent unfinished work and should advance on terminal transitions (#2862). */
15
+ const NON_TERMINAL_ITEM_STATUSES = new Set(["pending", "proposed", "running"]);
16
+ /** Terminal lifecycle actions that reconcile the brief's own plan.items (#2862). */
17
+ const OWN_ITEMS_RECONCILE_ACTIONS = new Set(["complete", "fail", "cancel"]);
18
+ /**
19
+ * Advance non-terminal plan.items / subItems to the terminal target status.
20
+ * Leaves cancelled / failed / completed / other non-pending-proposed-running items alone (#2862).
21
+ */
22
+ function advanceNonTerminalOwnItems(items, targetStatus) {
23
+ if (!Array.isArray(items)) {
24
+ return;
25
+ }
26
+ for (const item of items) {
27
+ if (item === null || typeof item !== "object" || Array.isArray(item)) {
28
+ continue;
29
+ }
30
+ const obj = item;
31
+ const status = String(obj.status ?? "");
32
+ if (NON_TERMINAL_ITEM_STATUSES.has(status)) {
33
+ obj.status = targetStatus;
34
+ }
35
+ advanceNonTerminalOwnItems(obj.subItems, targetStatus);
36
+ advanceNonTerminalOwnItems(obj.items, targetStatus);
37
+ }
38
+ }
39
+ /**
40
+ * Refresh the document envelope `updated` stamp to match `plan.updated`.
41
+ * Stamps whichever of xBRIEFInfo (v0.8) / vBRIEFInfo (v0.6) is present — never creates one (#2862 / #2346).
42
+ */
43
+ function stampEnvelopeUpdated(data, nowIso) {
44
+ for (const key of ["xBRIEFInfo", "vBRIEFInfo"]) {
45
+ const env = data[key];
46
+ if (typeof env === "object" && env !== null && !Array.isArray(env)) {
47
+ env.updated = nowIso;
48
+ }
49
+ }
50
+ }
14
51
  export function runTransition(action, filePath, now = new Date()) {
15
52
  if (!(action in TRANSITIONS)) {
16
53
  const valid = Object.keys(TRANSITIONS).sort().join(", ");
@@ -96,6 +133,12 @@ export function runTransition(action, filePath, now = new Date()) {
96
133
  const nowIso = utcNowIso(now);
97
134
  planObj.status = targetStatus;
98
135
  planObj.updated = nowIso;
136
+ // Keep the envelope clock aligned with plan.updated on every mutating transition (#2862).
137
+ stampEnvelopeUpdated(data, nowIso);
138
+ // Reconcile the completing brief's own plan.items (mirrors #1527 / #2566 registry sync) (#2862).
139
+ if (OWN_ITEMS_RECONCILE_ACTIONS.has(act)) {
140
+ advanceNonTerminalOwnItems(planObj.items, targetStatus);
141
+ }
99
142
  if (act === "complete") {
100
143
  stampCompletionMetadata(planObj, projectRoot, nowIso);
101
144
  }
@@ -1,3 +1,5 @@
1
+ /** Display/back-compat constant; resolution flows through resolveTriageCachePath (#2869). */
2
+ export declare const STATE_RELATIVE_PATH: string;
1
3
  export interface ReleaseAvailabilityProbeOptions {
2
4
  readonly now?: Date;
3
5
  readonly env?: NodeJS.ProcessEnv;
@@ -4,9 +4,15 @@ import { dirname, join } from "node:path";
4
4
  import { locateManifest, parseInstallManifest } from "../doctor/manifest.js";
5
5
  import { runningInsideDeftRepo } from "../doctor/paths.js";
6
6
  import { evaluateReleaseAvailability } from "../doctor/release-availability.js";
7
+ import { resolveTriageCachePath } from "../triage/cache-path.js";
7
8
  const THROTTLE_MS = 24 * 60 * 60 * 1000;
8
9
  const PUBLIC_NPM_REGISTRY = "https://registry.npmjs.org/";
9
- const STATE_RELATIVE_PATH = join("xbrief", ".triage-cache", "release-availability-state.json");
10
+ const STATE_FILE_NAME = "release-availability-state.json";
11
+ /** Display/back-compat constant; resolution flows through resolveTriageCachePath (#2869). */
12
+ export const STATE_RELATIVE_PATH = join("xbrief", ".triage-cache", STATE_FILE_NAME);
13
+ function resolveReleaseAvailabilityStatePath(projectRoot) {
14
+ return resolveTriageCachePath(projectRoot, STATE_FILE_NAME);
15
+ }
10
16
  function defaultReadText(path) {
11
17
  try {
12
18
  return readFileSync(path, "utf8");
@@ -91,18 +97,27 @@ export function probeSessionReleaseAvailability(projectRoot, options = {}) {
91
97
  const availability = evaluateReleaseAvailability(installed, npmResult.ok ? npmResult.version : null);
92
98
  if (availability.status !== "available")
93
99
  return { lines };
94
- const statePath = join(projectRoot, STATE_RELATIVE_PATH);
95
- const state = parseState((options.readState ?? defaultReadText)(statePath));
100
+ let statePath = null;
101
+ try {
102
+ statePath = resolveReleaseAvailabilityStatePath(projectRoot);
103
+ }
104
+ catch {
105
+ // Symlink-escaping triage-cache path: skip throttle state; still emit advisory (#2869).
106
+ statePath = null;
107
+ }
108
+ const state = parseState(statePath !== null ? (options.readState ?? defaultReadText)(statePath) : null);
96
109
  const now = options.now ?? new Date();
97
110
  if (isThrottled(state, availability.latestVersion, now))
98
111
  return { lines: [] };
99
112
  const message = `[deft release] Newer Directive release available: v${availability.latestVersion} ` +
100
113
  `(installed v${availability.installedVersion}). Run \`npm i -g @deftai/directive@latest\`.`;
101
- try {
102
- (options.writeState ?? defaultWriteState)(statePath, `${JSON.stringify({ latestVersion: availability.latestVersion, notifiedAt: now.toISOString() }, null, 2)}\n`);
103
- }
104
- catch {
105
- // The advisory remains useful if its best-effort throttle state cannot persist.
114
+ if (statePath !== null) {
115
+ try {
116
+ (options.writeState ?? defaultWriteState)(statePath, `${JSON.stringify({ latestVersion: availability.latestVersion, notifiedAt: now.toISOString() }, null, 2)}\n`);
117
+ }
118
+ catch {
119
+ // The advisory remains useful if its best-effort throttle state cannot persist.
120
+ }
106
121
  }
107
122
  return { lines: [...lines, message] };
108
123
  }
@@ -2,9 +2,8 @@
2
2
  import { resolve } from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
4
  import { PROJECTION_CONTAINMENT_REFUSED_EXIT_CODE, ProjectionContainmentError, } from "../fs/projection-containment.js";
5
- import { getPlatformCapabilities } from "../intake/platform-capabilities.js";
6
5
  import { EXIT_CONFIG_ERROR, EXIT_OK } from "./constants.js";
7
- import { dispatchProviderFromRuntime, HARNESS_BOUND_PROVIDERS, ROUTING_MODE_HARNESS_DEFAULT, ROUTING_MODE_PINNED, resolveRoutingPath, SWARM_WORKER_ROLES, writeModelDecision, } from "./routing.js";
6
+ import { HARNESS_BOUND_PROVIDERS, ROUTING_MODE_HARNESS_DEFAULT, ROUTING_MODE_PINNED, resolveDispatchProvider, resolveRoutingPath, SWARM_WORKER_ROLES, writeModelDecision, } from "./routing.js";
8
7
  export function routingSetMain(argv = process.argv.slice(2)) {
9
8
  let projectRoot = ".";
10
9
  let provider = null;
@@ -47,14 +46,10 @@ export function routingSetMain(argv = process.argv.slice(2)) {
47
46
  }
48
47
  let resolvedProvider = provider;
49
48
  if (resolvedProvider === null || resolvedProvider.length === 0) {
50
- let runtimeMode = "";
51
- try {
52
- runtimeMode = getPlatformCapabilities().runtimeMode;
53
- }
54
- catch {
55
- runtimeMode = "";
56
- }
57
- resolvedProvider = dispatchProviderFromRuntime(runtimeMode);
49
+ // Same key as launch + verify:routing: OPENCLAW / sessions_spawn → openclaw
50
+ // (#2875 Greptile P1). Do not map via runtimeMode alone — OpenClaw-only envs
51
+ // are often local-unsandboxed while still dispatching under provider openclaw.
52
+ resolvedProvider = resolveDispatchProvider(process.env);
58
53
  }
59
54
  if (harnessDefault) {
60
55
  if (model !== null) {
@@ -9,7 +9,7 @@ export declare const ROUTING_MODE_HARNESS_DEFAULT = "harness-default";
9
9
  export declare const ROUTING_FILENAME = "routing.local.json";
10
10
  /** Providers whose model is harness-bound -- deft cannot pin or verify a slug. */
11
11
  export declare const HARNESS_BOUND_PROVIDERS: Set<string>;
12
- /** Providers whose per-role model must be decided before sub-agent dispatch (#1739 / #1877). */
12
+ /** Providers whose per-role model must be decided before sub-agent dispatch (#1739 / #1877 / #2875). */
13
13
  export declare const ROUTING_GATED_DISPATCH_PROVIDERS: Set<string>;
14
14
  export interface RouteDecision {
15
15
  model: string | null;
@@ -54,7 +54,8 @@ export declare function dispatchProviderFromRuntime(runtimeMode: string): string
54
54
  * Resolve the `dispatch_provider` routing key from the active runtime envelope.
55
55
  * Separate from `runtime_mode` (#1557): Cursor sessions may carry
56
56
  * `runtime_mode=cloud-headless` for gh-auth purposes but route under provider
57
- * `cursor` for model selection (#1877).
57
+ * `cursor` for model selection (#1877). OpenClaw routes under `openclaw` when
58
+ * `sessions_spawn` / OPENCLAW signals are present (#2875).
58
59
  */
59
60
  export declare function resolveDispatchProvider(environ?: NodeJS.ProcessEnv): string;
60
61
  /**
@@ -32,8 +32,8 @@ export const ROUTING_MODE_HARNESS_DEFAULT = "harness-default";
32
32
  export const ROUTING_FILENAME = "routing.local.json";
33
33
  /** Providers whose model is harness-bound -- deft cannot pin or verify a slug. */
34
34
  export const HARNESS_BOUND_PROVIDERS = new Set(["grok"]);
35
- /** Providers whose per-role model must be decided before sub-agent dispatch (#1739 / #1877). */
36
- export const ROUTING_GATED_DISPATCH_PROVIDERS = new Set(["cursor", "grok"]);
35
+ /** Providers whose per-role model must be decided before sub-agent dispatch (#1739 / #1877 / #2875). */
36
+ export const ROUTING_GATED_DISPATCH_PROVIDERS = new Set(["cursor", "grok", "openclaw"]);
37
37
  const TRUTHY_ENV = new Set(["1", "true", "yes", "on"]);
38
38
  function envTruthy(environ, name) {
39
39
  return TRUTHY_ENV.has((environ[name] ?? "").trim().toLowerCase());
@@ -149,6 +149,9 @@ export function dispatchProviderFromRuntime(runtimeMode) {
149
149
  if (normalized.length === 0) {
150
150
  return "unknown";
151
151
  }
152
+ if (normalized.includes("openclaw")) {
153
+ return "openclaw";
154
+ }
152
155
  if (normalized.includes("grok")) {
153
156
  return "grok";
154
157
  }
@@ -161,13 +164,20 @@ export function dispatchProviderFromRuntime(runtimeMode) {
161
164
  * Resolve the `dispatch_provider` routing key from the active runtime envelope.
162
165
  * Separate from `runtime_mode` (#1557): Cursor sessions may carry
163
166
  * `runtime_mode=cloud-headless` for gh-auth purposes but route under provider
164
- * `cursor` for model selection (#1877).
167
+ * `cursor` for model selection (#1877). OpenClaw routes under `openclaw` when
168
+ * `sessions_spawn` / OPENCLAW signals are present (#2875).
165
169
  */
166
170
  export function resolveDispatchProvider(environ = process.env) {
167
171
  if (envTruthy(environ, "CURSOR_COMPOSER") || envTruthy(environ, "CURSOR_AGENT")) {
168
172
  return "cursor";
169
173
  }
170
174
  const runtime = (environ.DEFT_AGENT_RUNTIME ?? "").trim().toLowerCase();
175
+ if (envTruthy(environ, "OPENCLAW") ||
176
+ envTruthy(environ, "DEFT_HAS_SESSIONS_SPAWN") ||
177
+ envTruthy(environ, "DEFT_PROBE_SESSIONS_SPAWN") ||
178
+ runtime === "openclaw") {
179
+ return "openclaw";
180
+ }
171
181
  if (envTruthy(environ, "GROK_BUILD") || runtime === "grok-build") {
172
182
  return "grok";
173
183
  }
@@ -178,7 +188,9 @@ export function resolveDispatchProvider(environ = process.env) {
178
188
  envTruthy(environ, "BUILDKITE") ||
179
189
  (envTruthy(environ, "CI") &&
180
190
  !envTruthy(environ, "CURSOR_COMPOSER") &&
181
- !envTruthy(environ, "CURSOR_AGENT"))) {
191
+ !envTruthy(environ, "CURSOR_AGENT") &&
192
+ !envTruthy(environ, "OPENCLAW") &&
193
+ !envTruthy(environ, "DEFT_HAS_SESSIONS_SPAWN"))) {
182
194
  return "cloud-headless";
183
195
  }
184
196
  return "unknown";
@@ -135,13 +135,13 @@ export declare const registryData: {
135
135
  };
136
136
  readonly "task triage:show": {
137
137
  readonly name: "task triage:show";
138
- readonly summary: "Per-issue detail with optional drift diff";
139
- readonly refs: "(D11 / #1128)";
140
- readonly description: "Per-issue read-only detail (cached upstream payload + latest triage decision + audit timeline). Useful before running triage:accept / triage:defer to confirm context.";
141
- readonly usage: "task triage:show -- <N> [--repo=owner/name]";
142
- readonly flags: readonly [readonly ["<N>", "(required)", "Issue number (positional)."], readonly ["--repo owner/name", "(git remote)", "Explicit repo override."]];
143
- readonly examples: readonly ["task triage:show -- 42", "task triage:show -- 42 --repo deftai/directive"];
144
- readonly see_also: readonly ["task triage:queue", "task triage:status", "#1119 / D11"];
138
+ readonly summary: "Per-issue detail + optional operator brief";
139
+ readonly refs: "(D11 / #1128, #2890)";
140
+ readonly description: "Per-issue read-only detail (cached upstream payload + latest triage decision + audit timeline). --format=operator emits a pasteable Phase 3 candidate brief backbone (title/link/labels/summary/AC/latest decision/active-xBRIEF); agent still owns lean. Exit 0 on hit, 1 on cache miss.";
141
+ readonly usage: "task triage:show -- <N> [--format=default|operator] [--repo=owner/name]";
142
+ readonly flags: readonly [readonly ["<N>", "(required)", "Issue number (positional)."], readonly ["--format default|operator", "default", "default = audit/cache detail; operator = Phase 3 pasteable brief (#2890)."], readonly ["--repo owner/name", "(git remote)", "Explicit repo override."]];
143
+ readonly examples: readonly ["task triage:show -- 42", "task triage:show -- 42 --format=operator", "task triage:show -- 42 --repo deftai/directive"];
144
+ readonly see_also: readonly ["task triage:queue", "task triage:status", "#1119 / D11", "#2890"];
145
145
  readonly placeholder: false;
146
146
  };
147
147
  readonly "task triage:scope": {
@@ -209,16 +209,25 @@ export const registryData = {
209
209
  },
210
210
  "task triage:show": {
211
211
  name: "task triage:show",
212
- summary: "Per-issue detail with optional drift diff",
213
- refs: "(D11 / #1128)",
214
- description: "Per-issue read-only detail (cached upstream payload + latest triage decision + audit timeline). Useful before running triage:accept / triage:defer to confirm context.",
215
- usage: "task triage:show -- <N> [--repo=owner/name]",
212
+ summary: "Per-issue detail + optional operator brief",
213
+ refs: "(D11 / #1128, #2890)",
214
+ description: "Per-issue read-only detail (cached upstream payload + latest triage decision + audit timeline). --format=operator emits a pasteable Phase 3 candidate brief backbone (title/link/labels/summary/AC/latest decision/active-xBRIEF); agent still owns lean. Exit 0 on hit, 1 on cache miss.",
215
+ usage: "task triage:show -- <N> [--format=default|operator] [--repo=owner/name]",
216
216
  flags: [
217
217
  ["<N>", "(required)", "Issue number (positional)."],
218
+ [
219
+ "--format default|operator",
220
+ "default",
221
+ "default = audit/cache detail; operator = Phase 3 pasteable brief (#2890).",
222
+ ],
218
223
  ["--repo owner/name", "(git remote)", "Explicit repo override."],
219
224
  ],
220
- examples: ["task triage:show -- 42", "task triage:show -- 42 --repo deftai/directive"],
221
- see_also: ["task triage:queue", "task triage:status", "#1119 / D11"],
225
+ examples: [
226
+ "task triage:show -- 42",
227
+ "task triage:show -- 42 --format=operator",
228
+ "task triage:show -- 42 --repo deftai/directive",
229
+ ],
230
+ see_also: ["task triage:queue", "task triage:status", "#1119 / D11", "#2890"],
222
231
  placeholder: false,
223
232
  },
224
233
  "task triage:scope": {
@@ -10,5 +10,6 @@ export * from "./repo.js";
10
10
  export * from "./scope-ignores-filter.js";
11
11
  export * from "./scope-walk.js";
12
12
  export * from "./selection.js";
13
+ export * from "./show.js";
13
14
  export * from "./types.js";
14
15
  //# sourceMappingURL=index.d.ts.map
@@ -10,5 +10,6 @@ export * from "./repo.js";
10
10
  export * from "./scope-ignores-filter.js";
11
11
  export * from "./scope-walk.js";
12
12
  export * from "./selection.js";
13
+ export * from "./show.js";
13
14
  export * from "./types.js";
14
15
  //# sourceMappingURL=index.js.map
@@ -0,0 +1,69 @@
1
+ /**
2
+ * triage:show default + operator brief renderers (#1128 / #2890).
3
+ *
4
+ * Default format mirrors the pre-Python-removal `render_show` surface.
5
+ * `--format=operator` emits a pasteable Phase 3 candidate brief backbone;
6
+ * the agent still owns lean (not invented here).
7
+ */
8
+ /** Loose audit row for show renderers (actions + queue shapes both work). */
9
+ export type ShowAuditRow = {
10
+ readonly decision?: string;
11
+ readonly timestamp?: string;
12
+ readonly actor?: string;
13
+ readonly reason?: string;
14
+ readonly issue_number?: number;
15
+ readonly repo?: string;
16
+ };
17
+ /** One cached issue with body fields needed by show/operator formats. */
18
+ export interface CachedIssueDetail {
19
+ readonly number: number;
20
+ readonly title: string;
21
+ readonly state: string;
22
+ readonly labels: readonly string[];
23
+ readonly updatedAt: string;
24
+ readonly body: string;
25
+ readonly htmlUrl: string | null;
26
+ }
27
+ /** Collapse CR/LF so cached attacker text cannot break markdown bullets (P2). */
28
+ export declare function oneLine(value: string): string;
29
+ /**
30
+ * Resolve a safe issue link. Always construct the canonical github.com path for
31
+ * `owner/name#N` rather than trusting payload URL substrings (CodeQL
32
+ * incomplete-url-substring-sanitization).
33
+ */
34
+ export declare function resolveIssueHtmlUrl(repo: string, number: number): string;
35
+ /** Load a single cached issue (include closed) or null on miss. */
36
+ export declare function loadCachedIssueDetail(repo: string, number: number, options?: {
37
+ readonly projectRoot: string;
38
+ /** Absolute/relative path to `.deft-cache` root (CLI `--cache-root`). */
39
+ readonly cacheRoot?: string | null;
40
+ readonly source?: string;
41
+ }): CachedIssueDetail | null;
42
+ /** Default triage:show text (audit/cache oriented). */
43
+ export declare function renderShow(options: {
44
+ readonly issue: CachedIssueDetail | null;
45
+ readonly repo: string;
46
+ readonly number: number;
47
+ readonly latestDecision: ShowAuditRow | null;
48
+ readonly history: readonly ShowAuditRow[];
49
+ readonly inActiveXbrief: boolean;
50
+ }): string;
51
+ /**
52
+ * Extract a short problem/context summary (2–5 lines) from issue body.
53
+ * Prefers text before the first `##` section; falls back to leading paragraphs.
54
+ */
55
+ export declare function extractBodySummary(body: string, maxLines?: number): string;
56
+ /**
57
+ * Extract acceptance-criteria bullets from body, or a thin-body note.
58
+ * Looks for AC / Acceptance headings and checkbox / bullet lists under them.
59
+ */
60
+ export declare function extractAcceptanceCriteria(body: string): readonly string[];
61
+ /** Operator-facing pasteable brief backbone for Phase 3 decisions (#2890). */
62
+ export declare function renderOperatorBrief(options: {
63
+ readonly issue: CachedIssueDetail | null;
64
+ readonly repo: string;
65
+ readonly number: number;
66
+ readonly latestDecision: ShowAuditRow | null;
67
+ readonly inActiveXbrief: boolean;
68
+ }): string;
69
+ //# sourceMappingURL=show.d.ts.map