@deftai/directive-core 0.86.0 → 0.88.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cache/scanner.d.ts +11 -1
- package/dist/cache/scanner.js +29 -4
- package/dist/check/gate-lists.js +2 -0
- package/dist/content-contracts/skills/helpers.d.ts +10 -0
- package/dist/content-contracts/skills/helpers.js +35 -0
- package/dist/deposit/copy-tree.d.ts +19 -1
- package/dist/deposit/copy-tree.js +134 -5
- package/dist/doctor/main.d.ts +6 -5
- package/dist/doctor/main.js +80 -18
- package/dist/doctor/taskfile.d.ts +8 -0
- package/dist/doctor/taskfile.js +19 -0
- package/dist/fs/projection-containment.d.ts +18 -0
- package/dist/fs/projection-containment.js +40 -0
- package/dist/hooks/dispatcher.d.ts +34 -2
- package/dist/hooks/dispatcher.js +234 -21
- package/dist/hooks/tools.d.ts +31 -0
- package/dist/hooks/tools.js +74 -0
- package/dist/init-deposit/agent-hooks.d.ts +1 -1
- package/dist/init-deposit/agent-hooks.js +38 -2
- package/dist/init-deposit/hygiene.d.ts +16 -0
- package/dist/init-deposit/hygiene.js +26 -0
- package/dist/init-deposit/init-dispatch.js +28 -0
- package/dist/init-deposit/prettierignore.js +2 -2
- package/dist/init-deposit/refresh.js +38 -7
- package/dist/init-deposit/scaffold.js +7 -3
- package/dist/init-deposit/xbrief-projections.js +6 -6
- package/dist/intake/issue-emit.d.ts +45 -2
- package/dist/intake/issue-emit.js +420 -17
- package/dist/intake/issue-ingest.js +65 -6
- package/dist/packs/pack-render.d.ts +33 -0
- package/dist/packs/pack-render.js +155 -9
- package/dist/packs/quarantine-ext.d.ts +10 -0
- package/dist/packs/quarantine-ext.js +26 -2
- package/dist/platform/platform-capabilities.js +3 -0
- package/dist/policy/index.d.ts +1 -0
- package/dist/policy/index.js +1 -0
- package/dist/policy/no-deft-directive.d.ts +59 -0
- package/dist/policy/no-deft-directive.js +103 -0
- package/dist/policy/org-force-on-migration.d.ts +52 -0
- package/dist/policy/org-force-on-migration.js +260 -22
- package/dist/policy/runtime-authority.d.ts +41 -0
- package/dist/policy/runtime-authority.js +274 -0
- package/dist/review-monitor/constants.js +3 -2
- package/dist/review-monitor/tier-detection.d.ts +6 -2
- package/dist/review-monitor/tier-detection.js +27 -2
- package/dist/scope/transition.js +43 -0
- package/dist/session/release-availability.d.ts +2 -0
- package/dist/session/release-availability.js +23 -8
- package/dist/session/session-start-hook.d.ts +3 -0
- package/dist/session/session-start-hook.js +15 -0
- package/dist/session/session-start.js +30 -0
- package/dist/swarm/routing-set-cli.js +5 -10
- package/dist/swarm/routing.d.ts +3 -2
- package/dist/swarm/routing.js +16 -4
- package/dist/triage/help/registry-data.d.ts +7 -7
- package/dist/triage/help/registry-data.js +15 -6
- package/dist/triage/queue/index.d.ts +1 -0
- package/dist/triage/queue/index.js +1 -0
- package/dist/triage/queue/show.d.ts +69 -0
- package/dist/triage/queue/show.js +293 -0
- package/dist/triage/scope/cli.js +3 -0
- package/dist/triage/scope/coverage.d.ts +2 -0
- package/dist/triage/scope/coverage.js +18 -3
- package/dist/verify-source/cursor-tier1.js +7 -2
- package/dist/verify-source/index.d.ts +1 -0
- package/dist/verify-source/index.js +1 -0
- package/dist/verify-source/openclaw-tier1.d.ts +37 -0
- package/dist/verify-source/openclaw-tier1.js +105 -0
- package/dist/xbrief-migrate/migrate-project.js +9 -5
- package/package.json +4 -3
package/dist/swarm/routing.js
CHANGED
|
@@ -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
|
|
139
|
-
readonly refs: "(D11 / #1128)";
|
|
140
|
-
readonly description: "Per-issue read-only detail (cached upstream payload + latest triage decision + audit timeline).
|
|
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
|
|
213
|
-
refs: "(D11 / #1128)",
|
|
214
|
-
description: "Per-issue read-only detail (cached upstream payload + latest triage decision + audit timeline).
|
|
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: [
|
|
221
|
-
|
|
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": {
|
|
@@ -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
|
|
@@ -0,0 +1,293 @@
|
|
|
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
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
9
|
+
import { join, resolve } from "node:path";
|
|
10
|
+
import { CACHE_DIR_NAME, CACHE_SOURCE_GITHUB_ISSUE } from "./constants.js";
|
|
11
|
+
function parseLabels(raw) {
|
|
12
|
+
if (!Array.isArray(raw)) {
|
|
13
|
+
return [];
|
|
14
|
+
}
|
|
15
|
+
const labels = [];
|
|
16
|
+
for (const item of raw) {
|
|
17
|
+
if (typeof item === "object" && item !== null) {
|
|
18
|
+
const name = item.name;
|
|
19
|
+
if (typeof name === "string") {
|
|
20
|
+
labels.push(name);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
else if (typeof item === "string") {
|
|
24
|
+
labels.push(item);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return labels;
|
|
28
|
+
}
|
|
29
|
+
/** Collapse CR/LF so cached attacker text cannot break markdown bullets (P2). */
|
|
30
|
+
export function oneLine(value) {
|
|
31
|
+
return value.replace(/\r?\n/gu, " ").trim();
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Resolve a safe issue link. Always construct the canonical github.com path for
|
|
35
|
+
* `owner/name#N` rather than trusting payload URL substrings (CodeQL
|
|
36
|
+
* incomplete-url-substring-sanitization).
|
|
37
|
+
*/
|
|
38
|
+
export function resolveIssueHtmlUrl(repo, number) {
|
|
39
|
+
return `https://github.com/${repo}/issues/${number}`;
|
|
40
|
+
}
|
|
41
|
+
/** Load a single cached issue (include closed) or null on miss. */
|
|
42
|
+
export function loadCachedIssueDetail(repo, number, options = {
|
|
43
|
+
projectRoot: process.cwd(),
|
|
44
|
+
}) {
|
|
45
|
+
if (!repo.includes("/")) {
|
|
46
|
+
throw new Error(`repo must be 'owner/name'; got '${repo}'`);
|
|
47
|
+
}
|
|
48
|
+
const parts = repo.split("/", 2);
|
|
49
|
+
const owner = parts[0];
|
|
50
|
+
const name = parts[1];
|
|
51
|
+
if (owner === undefined || name === undefined || owner.length === 0 || name.length === 0) {
|
|
52
|
+
throw new Error(`repo must be 'owner/name'; got '${repo}'`);
|
|
53
|
+
}
|
|
54
|
+
const source = options.source ?? CACHE_SOURCE_GITHUB_ISSUE;
|
|
55
|
+
const cacheBase = options.cacheRoot !== null && options.cacheRoot !== undefined && options.cacheRoot.length > 0
|
|
56
|
+
? resolve(options.cacheRoot)
|
|
57
|
+
: join(resolve(options.projectRoot), CACHE_DIR_NAME);
|
|
58
|
+
const entryDir = join(cacheBase, source, owner, name, String(number));
|
|
59
|
+
const rawPath = join(entryDir, "raw.json");
|
|
60
|
+
if (!existsSync(rawPath)) {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
let payload;
|
|
64
|
+
try {
|
|
65
|
+
const parsed = JSON.parse(readFileSync(rawPath, { encoding: "utf8" }));
|
|
66
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
payload = parsed;
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
const n = typeof payload.number === "number" ? payload.number : number;
|
|
75
|
+
const stateRaw = payload.state ?? "open";
|
|
76
|
+
const state = typeof stateRaw === "string" ? stateRaw.toLowerCase() : "open";
|
|
77
|
+
const title = typeof payload.title === "string" ? payload.title : "";
|
|
78
|
+
const body = typeof payload.body === "string" ? payload.body : "";
|
|
79
|
+
const updatedAt = typeof payload.updated_at === "string"
|
|
80
|
+
? payload.updated_at
|
|
81
|
+
: typeof payload.updatedAt === "string"
|
|
82
|
+
? payload.updatedAt
|
|
83
|
+
: "";
|
|
84
|
+
return {
|
|
85
|
+
number: n,
|
|
86
|
+
title,
|
|
87
|
+
state,
|
|
88
|
+
labels: parseLabels(payload.labels),
|
|
89
|
+
updatedAt,
|
|
90
|
+
body,
|
|
91
|
+
htmlUrl: resolveIssueHtmlUrl(repo, n),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
/** Default triage:show text (audit/cache oriented). */
|
|
95
|
+
export function renderShow(options) {
|
|
96
|
+
const lines = [];
|
|
97
|
+
lines.push(`triage:show -- ${options.repo}#${options.number}`);
|
|
98
|
+
if (options.issue === null) {
|
|
99
|
+
lines.push("");
|
|
100
|
+
lines.push(" (issue not present in local cache)");
|
|
101
|
+
lines.push(" Run `task triage:bootstrap` to populate, or check the repo slug.");
|
|
102
|
+
return lines.join("\n");
|
|
103
|
+
}
|
|
104
|
+
const labels = options.issue.labels.map(oneLine);
|
|
105
|
+
lines.push(` title: ${oneLine(options.issue.title)}`);
|
|
106
|
+
lines.push(` state: ${oneLine(options.issue.state)}`);
|
|
107
|
+
lines.push(` labels: ${labels.length > 0 ? labels.join(", ") : "<none>"}`);
|
|
108
|
+
lines.push(` updated_at: ${oneLine(options.issue.updatedAt)}`);
|
|
109
|
+
lines.push("");
|
|
110
|
+
lines.push(` active xBRIEF reference: ${options.inActiveXbrief ? "yes" : "no"}`);
|
|
111
|
+
if (options.latestDecision !== null) {
|
|
112
|
+
const d = options.latestDecision;
|
|
113
|
+
lines.push(` latest decision: ${oneLine(String(d.decision ?? "?"))} at ${oneLine(String(d.timestamp ?? "?"))} by ${oneLine(String(d.actor ?? "?"))}`);
|
|
114
|
+
if (typeof d.reason === "string" && d.reason.length > 0) {
|
|
115
|
+
lines.push(` reason: ${oneLine(d.reason)}`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
lines.push(" latest decision: <none -- untriaged>");
|
|
120
|
+
}
|
|
121
|
+
if (options.history.length > 0) {
|
|
122
|
+
lines.push("");
|
|
123
|
+
lines.push(` history (${options.history.length} entries, oldest first):`);
|
|
124
|
+
for (const entry of options.history) {
|
|
125
|
+
const decision = oneLine(String(entry.decision ?? "?")).padEnd(14);
|
|
126
|
+
lines.push(` - ${oneLine(String(entry.timestamp ?? "?"))} ${decision} by ${oneLine(String(entry.actor ?? "?"))}`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return lines.join("\n");
|
|
130
|
+
}
|
|
131
|
+
/** Collapse blank lines and trim for summary extraction. */
|
|
132
|
+
function normalizeBody(body) {
|
|
133
|
+
return body.replace(/\r\n/g, "\n").replace(/\r/g, "\n").trim();
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Extract a short problem/context summary (2–5 lines) from issue body.
|
|
137
|
+
* Prefers text before the first `##` section; falls back to leading paragraphs.
|
|
138
|
+
*/
|
|
139
|
+
export function extractBodySummary(body, maxLines = 5) {
|
|
140
|
+
const text = normalizeBody(body);
|
|
141
|
+
if (text.length === 0) {
|
|
142
|
+
return "(thin body / no summary)";
|
|
143
|
+
}
|
|
144
|
+
// Drop leading H1/title lines
|
|
145
|
+
let rest = text.replace(/^#[^\n]*\n+/u, "");
|
|
146
|
+
// Prefer content before first ## heading (often Summary/Description)
|
|
147
|
+
const firstSection = rest.search(/^##\s+/mu);
|
|
148
|
+
if (firstSection > 0) {
|
|
149
|
+
rest = rest.slice(0, firstSection).trim();
|
|
150
|
+
}
|
|
151
|
+
else if (firstSection === 0) {
|
|
152
|
+
// Body starts with ## — take the first section body
|
|
153
|
+
const next = rest.search(/\n##\s+/u);
|
|
154
|
+
const block = next === -1 ? rest : rest.slice(0, next);
|
|
155
|
+
rest = block.replace(/^##[^\n]*\n?/u, "").trim();
|
|
156
|
+
}
|
|
157
|
+
const paragraphs = rest
|
|
158
|
+
.split(/\n{2,}/u)
|
|
159
|
+
.map((p) => p.replace(/\s+/gu, " ").trim())
|
|
160
|
+
.filter((p) => p.length > 0 && !p.startsWith("#"));
|
|
161
|
+
if (paragraphs.length === 0) {
|
|
162
|
+
const lines = rest
|
|
163
|
+
.split("\n")
|
|
164
|
+
.map((l) => l.trim())
|
|
165
|
+
.filter((l) => l.length > 0 && !l.startsWith("#"));
|
|
166
|
+
if (lines.length === 0) {
|
|
167
|
+
return "(thin body / no summary)";
|
|
168
|
+
}
|
|
169
|
+
return lines.slice(0, maxLines).join("\n");
|
|
170
|
+
}
|
|
171
|
+
const out = [];
|
|
172
|
+
for (const para of paragraphs) {
|
|
173
|
+
if (out.length >= maxLines)
|
|
174
|
+
break;
|
|
175
|
+
// Soft-wrap long paragraphs into ~100-char lines for pasteability
|
|
176
|
+
if (para.length <= 120) {
|
|
177
|
+
out.push(para);
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
const words = para.split(/\s+/u);
|
|
181
|
+
let line = "";
|
|
182
|
+
for (const w of words) {
|
|
183
|
+
const next = line.length === 0 ? w : `${line} ${w}`;
|
|
184
|
+
if (next.length > 100 && line.length > 0) {
|
|
185
|
+
out.push(line);
|
|
186
|
+
line = w;
|
|
187
|
+
if (out.length >= maxLines)
|
|
188
|
+
break;
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
line = next;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
if (line.length > 0 && out.length < maxLines) {
|
|
195
|
+
out.push(line);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return out.slice(0, maxLines).join("\n");
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Extract acceptance-criteria bullets from body, or a thin-body note.
|
|
203
|
+
* Looks for AC / Acceptance headings and checkbox / bullet lists under them.
|
|
204
|
+
*/
|
|
205
|
+
export function extractAcceptanceCriteria(body) {
|
|
206
|
+
const text = normalizeBody(body);
|
|
207
|
+
if (text.length === 0) {
|
|
208
|
+
return [];
|
|
209
|
+
}
|
|
210
|
+
const headingRe = /^#{1,3}\s*(?:acceptance\s*criteria|acceptance|ac\b|success\s*criteria)[^\n]*$/imu;
|
|
211
|
+
const match = headingRe.exec(text);
|
|
212
|
+
if (match === null || match.index === undefined) {
|
|
213
|
+
// Loose: checkbox list near top without a heading
|
|
214
|
+
const loose = [];
|
|
215
|
+
for (const line of text.split("\n")) {
|
|
216
|
+
const m = /^\s*[-*]\s*\[[ xX]\]\s+(.+)$/u.exec(line);
|
|
217
|
+
if (m?.[1] !== undefined) {
|
|
218
|
+
loose.push(m[1].trim());
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return loose.slice(0, 12);
|
|
222
|
+
}
|
|
223
|
+
const after = text.slice(match.index + match[0].length);
|
|
224
|
+
// Stop only at a next ## (H2) peer section — keep ### subsections (A/B/C…) inside AC.
|
|
225
|
+
const nextPeer = after.search(/\n##\s+\S/u);
|
|
226
|
+
const section = nextPeer === -1 ? after : after.slice(0, nextPeer);
|
|
227
|
+
const bullets = [];
|
|
228
|
+
for (const line of section.split("\n")) {
|
|
229
|
+
const checkbox = /^\s*[-*]\s*\[[ xX]\]\s+(.+)$/u.exec(line);
|
|
230
|
+
if (checkbox?.[1] !== undefined) {
|
|
231
|
+
bullets.push(checkbox[1].trim());
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
// Prefer checkboxes under AC; plain bullets are also accepted
|
|
235
|
+
const bullet = /^\s*[-*]\s+(.+)$/u.exec(line);
|
|
236
|
+
if (bullet?.[1] !== undefined) {
|
|
237
|
+
// Skip nested subsection-only markers that are just headings-as-bullets
|
|
238
|
+
bullets.push(bullet[1].trim());
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
const numbered = /^\s*\d+[.)]\s+(.+)$/u.exec(line);
|
|
242
|
+
if (numbered?.[1] !== undefined) {
|
|
243
|
+
bullets.push(numbered[1].trim());
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
return bullets.slice(0, 12);
|
|
247
|
+
}
|
|
248
|
+
/** Operator-facing pasteable brief backbone for Phase 3 decisions (#2890). */
|
|
249
|
+
export function renderOperatorBrief(options) {
|
|
250
|
+
const lines = [];
|
|
251
|
+
lines.push(`triage:show --format=operator -- ${options.repo}#${options.number}`);
|
|
252
|
+
if (options.issue === null) {
|
|
253
|
+
lines.push("");
|
|
254
|
+
lines.push(" (issue not present in local cache)");
|
|
255
|
+
lines.push(" Run `task triage:bootstrap` / re-sync per Phase 0, or check the repo slug.");
|
|
256
|
+
return lines.join("\n");
|
|
257
|
+
}
|
|
258
|
+
const issue = options.issue;
|
|
259
|
+
const link = issue.htmlUrl ?? resolveIssueHtmlUrl(options.repo, options.number);
|
|
260
|
+
const labels = issue.labels.length > 0 ? issue.labels.map(oneLine).join(", ") : "<none>";
|
|
261
|
+
lines.push(`#${options.number} ${oneLine(issue.title)}`);
|
|
262
|
+
lines.push(`link: ${oneLine(link)}`);
|
|
263
|
+
lines.push(`labels: ${labels}`);
|
|
264
|
+
lines.push("");
|
|
265
|
+
lines.push("summary:");
|
|
266
|
+
for (const line of extractBodySummary(issue.body).split("\n")) {
|
|
267
|
+
lines.push(` ${oneLine(line)}`);
|
|
268
|
+
}
|
|
269
|
+
lines.push("");
|
|
270
|
+
const ac = extractAcceptanceCriteria(issue.body);
|
|
271
|
+
if (ac.length === 0) {
|
|
272
|
+
lines.push("acceptance criteria: (thin body / no AC)");
|
|
273
|
+
}
|
|
274
|
+
else {
|
|
275
|
+
lines.push("acceptance criteria:");
|
|
276
|
+
for (const item of ac) {
|
|
277
|
+
lines.push(` - ${oneLine(item)}`);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
lines.push("");
|
|
281
|
+
if (options.latestDecision !== null) {
|
|
282
|
+
const d = options.latestDecision;
|
|
283
|
+
lines.push(`latest decision: ${oneLine(String(d.decision ?? "?"))} at ${oneLine(String(d.timestamp ?? "?"))} by ${oneLine(String(d.actor ?? "?"))}`);
|
|
284
|
+
}
|
|
285
|
+
else {
|
|
286
|
+
lines.push("latest decision: <none -- untriaged>");
|
|
287
|
+
}
|
|
288
|
+
lines.push(`active xBRIEF: ${options.inActiveXbrief ? "yes" : "no"}`);
|
|
289
|
+
lines.push("");
|
|
290
|
+
lines.push("lean: (agent-owned — not filled by triage:show)");
|
|
291
|
+
return lines.join("\n");
|
|
292
|
+
}
|
|
293
|
+
//# sourceMappingURL=show.js.map
|
package/dist/triage/scope/cli.js
CHANGED
|
@@ -232,9 +232,12 @@ export function runCliCapture(argv) {
|
|
|
232
232
|
cacheRoot: args.cacheRoot !== undefined ? resolve(args.cacheRoot) : undefined,
|
|
233
233
|
});
|
|
234
234
|
const subHash = subscriptionHash(rules);
|
|
235
|
+
// When --cache-root is outside the project tree, contain against that root
|
|
236
|
+
// instead of projectRoot (tests and operators may redirect the cache) (#2869).
|
|
235
237
|
const record = writeCoverageDenominator(path, {
|
|
236
238
|
count: args.count,
|
|
237
239
|
subscriptionHashValue: subHash,
|
|
240
|
+
projectRoot: args.cacheRoot !== undefined ? resolve(args.cacheRoot) : projectRoot,
|
|
238
241
|
});
|
|
239
242
|
stdout.push(`triage:scope: wrote coverage denominator count=${record.count} ` +
|
|
240
243
|
`subscription-hash=${record.subscriptionHash} path=${path}\n`);
|
|
@@ -14,6 +14,8 @@ export declare function writeCoverageDenominator(path: string, options: {
|
|
|
14
14
|
count: number;
|
|
15
15
|
subscriptionHashValue: string;
|
|
16
16
|
fetchedAt?: Date;
|
|
17
|
+
/** Project (or cache) root for symlink containment (#2869). */
|
|
18
|
+
projectRoot?: string;
|
|
17
19
|
}): CoverageRecord;
|
|
18
20
|
export declare function readCoverageDenominator(path: string, options: {
|
|
19
21
|
currentHash: string;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { join, resolve } from "node:path";
|
|
2
|
+
import { dirname, join, resolve } from "node:path";
|
|
3
|
+
import { assertWriteTargetSafe } from "../../fs/projection-containment.js";
|
|
3
4
|
import { CACHE_DIR_NAME, COVERAGE_FILENAME, DEFAULT_COVERAGE_TTL_HOURS, ENV_COVERAGE_TTL_HOURS, } from "./constants.js";
|
|
4
5
|
import { parseIso, utcIso, utcNow } from "./time.js";
|
|
5
6
|
export function coveragePath(source, repo, options = {}) {
|
|
@@ -29,14 +30,28 @@ export function writeCoverageDenominator(path, options) {
|
|
|
29
30
|
if (!options.subscriptionHashValue) {
|
|
30
31
|
throw new Error("subscription_hash_value must be a non-empty string");
|
|
31
32
|
}
|
|
33
|
+
const absPath = resolve(path);
|
|
34
|
+
// Production CLI always passes projectRoot. When omitted (unit tests with a
|
|
35
|
+
// bare cacheRoot), walk up to the deepest existing ancestor so containment
|
|
36
|
+
// can realpath the root before mkdir creates intermediate dirs (#2869).
|
|
37
|
+
let containmentRoot = options.projectRoot !== undefined ? resolve(options.projectRoot) : dirname(absPath);
|
|
38
|
+
if (options.projectRoot === undefined) {
|
|
39
|
+
while (!existsSync(containmentRoot)) {
|
|
40
|
+
const parent = dirname(containmentRoot);
|
|
41
|
+
if (parent === containmentRoot)
|
|
42
|
+
break;
|
|
43
|
+
containmentRoot = parent;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
assertWriteTargetSafe(containmentRoot, absPath);
|
|
32
47
|
const stamp = utcIso(options.fetchedAt ?? null);
|
|
33
|
-
mkdirSync(
|
|
48
|
+
mkdirSync(dirname(absPath), { recursive: true });
|
|
34
49
|
const payload = {
|
|
35
50
|
count: options.count,
|
|
36
51
|
fetched_at: stamp,
|
|
37
52
|
subscription_hash: options.subscriptionHashValue,
|
|
38
53
|
};
|
|
39
|
-
writeFileSync(
|
|
54
|
+
writeFileSync(absPath, `${JSON.stringify(payload, Object.keys(payload).sort())}\n`, "utf8");
|
|
40
55
|
return {
|
|
41
56
|
count: options.count,
|
|
42
57
|
fetchedAt: stamp,
|
|
@@ -3,14 +3,19 @@ import { join, resolve } from "node:path";
|
|
|
3
3
|
export const CURSOR_TIER1_TARGETS = [
|
|
4
4
|
{
|
|
5
5
|
path: "content/skills/deft-directive-swarm/SKILL.md",
|
|
6
|
-
label: "swarm Phase 3 capability matrix",
|
|
6
|
+
label: "swarm Phase 3 capability matrix (thin skill)",
|
|
7
7
|
markers: [
|
|
8
8
|
"Probe for the Cursor `Task` tool",
|
|
9
9
|
"cursor-composer",
|
|
10
10
|
"cursor-cloud-agent",
|
|
11
|
-
"
|
|
11
|
+
"host-cursor.md",
|
|
12
12
|
],
|
|
13
13
|
},
|
|
14
|
+
{
|
|
15
|
+
path: "content/skills/deft-directive-swarm/references/host-cursor.md",
|
|
16
|
+
label: "swarm Cursor host adapter",
|
|
17
|
+
markers: ["Step 2e: Cursor Launch", "cursor-composer", "Task"],
|
|
18
|
+
},
|
|
14
19
|
{
|
|
15
20
|
path: "content/skills/deft-directive-review-cycle/SKILL.md",
|
|
16
21
|
label: "review-cycle monitoring tier selection",
|
|
@@ -3,6 +3,7 @@ export * from "./code-structure-validate.js";
|
|
|
3
3
|
export * from "./content-manifest.js";
|
|
4
4
|
export { CANONICAL_SCHEMA_REL, type ContractDriftOptions, type ContractDriftResult, evaluateContractDrift, PUBLISHED_SCHEMA_REL, } from "./contract-drift.js";
|
|
5
5
|
export * from "./cursor-tier1.js";
|
|
6
|
+
export * from "./openclaw-tier1.js";
|
|
6
7
|
export * from "./python-call-scan.js";
|
|
7
8
|
export * from "./rule-ownership-lint.js";
|
|
8
9
|
export * from "./scm-boundary.js";
|
|
@@ -3,6 +3,7 @@ export * from "./code-structure-validate.js";
|
|
|
3
3
|
export * from "./content-manifest.js";
|
|
4
4
|
export { CANONICAL_SCHEMA_REL, evaluateContractDrift, PUBLISHED_SCHEMA_REL, } from "./contract-drift.js";
|
|
5
5
|
export * from "./cursor-tier1.js";
|
|
6
|
+
export * from "./openclaw-tier1.js";
|
|
6
7
|
export * from "./python-call-scan.js";
|
|
7
8
|
export * from "./rule-ownership-lint.js";
|
|
8
9
|
export * from "./scm-boundary.js";
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic gate: assert that OpenClaw is enumerated as a Tier-1 descriptor
|
|
3
|
+
* in the swarm Phase 3 capability matrix and that routing accepts openclaw as a
|
|
4
|
+
* dispatch_provider (#2875). Without this gate the "OpenClaw -> Tier 1" mapping
|
|
5
|
+
* is prose-trusted; a doc edit that drops the sessions_spawn descriptor would
|
|
6
|
+
* silently re-open misclassification (OpenClaw falling through to grok-build or
|
|
7
|
+
* generic-terminal).
|
|
8
|
+
*
|
|
9
|
+
* Review-cycle skill markers for OpenClaw Approach 1 are owned by sibling #2876
|
|
10
|
+
* and are intentionally out of scope for this gate.
|
|
11
|
+
*/
|
|
12
|
+
export interface OpenclawTier1Target {
|
|
13
|
+
/** Project-root-relative path to the surface file. */
|
|
14
|
+
readonly path: string;
|
|
15
|
+
/** Human label for the surface, used in failure output. */
|
|
16
|
+
readonly label: string;
|
|
17
|
+
/** Whitespace-normalized substrings that MUST all be present. */
|
|
18
|
+
readonly markers: readonly string[];
|
|
19
|
+
}
|
|
20
|
+
export declare const OPENCLAW_TIER1_TARGETS: readonly OpenclawTier1Target[];
|
|
21
|
+
export interface OpenclawTier1Finding {
|
|
22
|
+
readonly path: string;
|
|
23
|
+
readonly label: string;
|
|
24
|
+
readonly missingMarkers: readonly string[];
|
|
25
|
+
}
|
|
26
|
+
export interface OpenclawTier1Result {
|
|
27
|
+
readonly code: 0 | 1 | 2;
|
|
28
|
+
readonly findings: readonly OpenclawTier1Finding[];
|
|
29
|
+
readonly message: string;
|
|
30
|
+
readonly stream: "stdout" | "stderr";
|
|
31
|
+
}
|
|
32
|
+
export interface OpenclawTier1Options {
|
|
33
|
+
readonly targets?: readonly OpenclawTier1Target[];
|
|
34
|
+
readonly quiet?: boolean;
|
|
35
|
+
}
|
|
36
|
+
export declare function evaluateOpenclawTier1(projectRoot: string, options?: OpenclawTier1Options): OpenclawTier1Result;
|
|
37
|
+
//# sourceMappingURL=openclaw-tier1.d.ts.map
|