@deftai/directive-core 0.73.1 → 0.75.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/agents-md-budget/evaluate.d.ts +61 -0
- package/dist/agents-md-budget/evaluate.js +321 -19
- package/dist/agents-md-budget/index.d.ts +1 -0
- package/dist/agents-md-budget/index.js +1 -0
- package/dist/agents-md-budget/skill-frontmatter.d.ts +33 -0
- package/dist/agents-md-budget/skill-frontmatter.js +115 -0
- package/dist/check/orchestrator.d.ts +3 -0
- package/dist/check/orchestrator.js +2 -1
- package/dist/codebase/map.js +2 -0
- package/dist/content-contracts/skills/helpers.d.ts +0 -1
- package/dist/content-contracts/skills/helpers.js +1 -11
- package/dist/content-contracts/skills/skill-frontmatter.js +8 -1
- package/dist/content-contracts/standards/_helpers.js +4 -2
- package/dist/content-contracts/standards/_taskfile-helpers.d.ts +2 -0
- package/dist/content-contracts/standards/_taskfile-helpers.js +11 -5
- package/dist/doctor/checks.d.ts +9 -0
- package/dist/doctor/checks.js +67 -0
- package/dist/doctor/doctor-state.js +2 -1
- package/dist/doctor/main.js +2 -1
- package/dist/doctor/paths.js +2 -1
- package/dist/eval-health-relocation/evaluate.d.ts +71 -0
- package/dist/eval-health-relocation/evaluate.js +252 -0
- package/dist/eval-health-relocation/index.d.ts +2 -0
- package/dist/eval-health-relocation/index.js +2 -0
- package/dist/fs/projection-containment.d.ts +35 -0
- package/dist/fs/projection-containment.js +118 -0
- package/dist/init-deposit/scaffold.d.ts +21 -0
- package/dist/init-deposit/scaffold.js +86 -9
- package/dist/intake/issue-emit.js +2 -2
- package/dist/policy/agents-md-budget.d.ts +23 -0
- package/dist/policy/agents-md-budget.js +75 -4
- package/dist/pr-merge-readiness/constants.d.ts +4 -0
- package/dist/pr-merge-readiness/constants.js +4 -0
- package/dist/pr-merge-readiness/evaluate.js +3 -0
- package/dist/pr-merge-readiness/mergeability.d.ts +8 -6
- package/dist/pr-merge-readiness/mergeability.js +15 -10
- package/dist/pr-merge-readiness/output.js +12 -6
- package/dist/pr-merge-readiness/parse.d.ts +1 -0
- package/dist/pr-merge-readiness/parse.js +9 -1
- package/dist/pr-merge-readiness/types.d.ts +2 -0
- package/dist/preflight-cache/evaluate.d.ts +2 -0
- package/dist/preflight-cache/evaluate.js +14 -3
- package/dist/preflight-cache/index.d.ts +1 -1
- package/dist/preflight-cache/index.js +1 -1
- package/dist/release/constants.d.ts +2 -0
- package/dist/release/constants.js +2 -0
- package/dist/release/preflight.d.ts +2 -0
- package/dist/release/preflight.js +14 -1
- package/dist/release/spawn.js +5 -3
- package/dist/scope/decompose.js +4 -3
- package/dist/scope/index.d.ts +1 -0
- package/dist/scope/index.js +1 -0
- package/dist/scope/main.js +24 -1
- package/dist/scope/open-umbrella-warning.d.ts +12 -0
- package/dist/scope/open-umbrella-warning.js +403 -0
- package/dist/scope/vbrief-ref.js +3 -3
- package/dist/swarm/complete-cohort.js +6 -6
- package/dist/swarm/launch.js +2 -2
- package/dist/triage/bootstrap/gitignore.d.ts +1 -1
- package/dist/triage/bootstrap/gitignore.js +69 -58
- package/dist/triage/queue/cache.d.ts +14 -0
- package/dist/triage/queue/cache.js +61 -1
- package/dist/validate-content/validate-links.js +2 -2
- package/dist/value/readback.d.ts +1 -0
- package/dist/value/readback.js +5 -1
- package/dist/vbrief-reconcile/umbrellas.js +12 -11
- package/dist/verify-env/toolchain-check.js +20 -5
- package/dist/verify-env/verify-hooks-installed.d.ts +2 -0
- package/dist/verify-env/verify-hooks-installed.js +17 -15
- package/dist/xbrief-migrate/migrate-project.js +9 -0
- package/package.json +7 -3
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
2
|
-
import { join, resolve } from "node:path";
|
|
2
|
+
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
|
3
3
|
import { hasArtifactSuffix, resolveLifecycleRoot } from "../layout/resolve.js";
|
|
4
4
|
import { detectLifecycleFolder } from "../scope/decomposed-refs.js";
|
|
5
5
|
import { runTransition } from "../scope/transition.js";
|
|
@@ -31,13 +31,13 @@ function rel(path, projectRoot) {
|
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
33
|
function globResolve(pattern, projectRoot) {
|
|
34
|
-
const absPattern = pattern
|
|
34
|
+
const absPattern = isAbsolute(pattern) ? pattern : join(projectRoot, pattern);
|
|
35
35
|
if (!absPattern.includes("*")) {
|
|
36
36
|
return existsSync(absPattern) ? [resolve(absPattern)] : [];
|
|
37
37
|
}
|
|
38
|
-
|
|
39
|
-
const dir =
|
|
40
|
-
const glob =
|
|
38
|
+
// Use path module to split directory/glob parts — platform-safe on both POSIX and Windows.
|
|
39
|
+
const dir = dirname(absPattern);
|
|
40
|
+
const glob = basename(absPattern);
|
|
41
41
|
if (!existsSync(dir)) {
|
|
42
42
|
return [];
|
|
43
43
|
}
|
|
@@ -59,7 +59,7 @@ export function resolveCohortPaths(positional, cohortGlobs, projectRoot) {
|
|
|
59
59
|
resolved.push(rp);
|
|
60
60
|
};
|
|
61
61
|
for (const raw of positional) {
|
|
62
|
-
const candidate = raw
|
|
62
|
+
const candidate = isAbsolute(raw) ? raw : join(projectRoot, raw);
|
|
63
63
|
if (!existsSync(candidate)) {
|
|
64
64
|
errors.push(`path does not exist: ${raw}`);
|
|
65
65
|
continue;
|
package/dist/swarm/launch.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { basename, join, resolve } from "node:path";
|
|
2
|
+
import { basename, isAbsolute, join, resolve } from "node:path";
|
|
3
3
|
import { inferGithubAuthMode } from "../intake/github-auth-modes.js";
|
|
4
4
|
import { getPlatformCapabilities } from "../intake/platform-capabilities.js";
|
|
5
5
|
import { hasArtifactSuffix, resolveLifecycleFolder, stripArtifactSuffix, } from "../layout/resolve.js";
|
|
@@ -166,7 +166,7 @@ export function looksLikePath(token) {
|
|
|
166
166
|
}
|
|
167
167
|
function resolveOne(token, projectRoot, idMap, issueMap) {
|
|
168
168
|
if (looksLikePath(token)) {
|
|
169
|
-
const candidate = token
|
|
169
|
+
const candidate = isAbsolute(token) ? token : join(projectRoot, token);
|
|
170
170
|
if (!existsSync(candidate)) {
|
|
171
171
|
return {
|
|
172
172
|
story: null,
|
|
@@ -8,7 +8,7 @@ export declare function gitignoreTriageCacheEntries(projectRoot: string): readon
|
|
|
8
8
|
export declare function gitattributesTriageCacheGlob(projectRoot: string): string;
|
|
9
9
|
export declare const GITATTRIBUTES_EVAL_RULE = "vbrief/.triage-cache/*.jsonl merge=union";
|
|
10
10
|
export declare const FORBIDDEN_BLANKET_EVAL_LINES: readonly string[];
|
|
11
|
-
export declare const EVAL_README_BODY = "# `vbrief/.triage-cache/`
|
|
11
|
+
export declare const EVAL_README_BODY = "# `vbrief/.triage-cache/` \u2014 triage working-set files\n\nThis directory holds JSON-lines logs and scratch files that Deft triage and\nslicing workflows emit. Deft configures your repo's `.gitignore` and\n`.gitattributes` so some files stay local while team-shared records can be\ncommitted.\n\n## What lives here\n\n| File | Committed? | Notes |\n| --- | --- | --- |\n| `slices.jsonl` | Yes | Team-shared cohort records from slicing skills. New teammates use prior cohort outputs to spot orphans and avoid re-slicing the same scope. |\n| `candidates.jsonl` | No | Your local triage accept / defer / reject stream. Re-create on a fresh clone with `deft triage:bootstrap`. |\n| `summary-history.jsonl` | No | Local history of `deft triage:summary` output; not required for day-to-day work. |\n| `scope-lifecycle.jsonl` | No | Local audit trail for scope demotions (`deft scope:demote`). Each operator's stream stays on their machine. |\n| `decompositions/` | No | Draft story-decomposition scratch. Produced child story xBRIEFs live in lifecycle folders via `deft scope:decompose`. |\n| `doctor-state.json` | No | Per-clone throttle state for `deft doctor` re-probe timing. |\n\nPaths listed as \"No\" above are added to `.gitignore` during bootstrap; anything\nnot listed remains committable by default. The selective ignore entries live in\nthe repo-root `.gitignore` (`vbrief/.triage-cache/candidates.jsonl`,\n`vbrief/.triage-cache/summary-history.jsonl`, `vbrief/.triage-cache/scope-lifecycle.jsonl`,\n`vbrief/.triage-cache/decompositions/`, and `vbrief/.triage-cache/doctor-state.json`).\n\n## Fresh clone\n\nIf `candidates.jsonl` is missing, run:\n\n```\ndeft triage:bootstrap\n```\n\nBootstrap rebuilds the local candidates log without altering committed\n`slices.jsonl`.\n\n## Merge behavior for `*.jsonl`\n\nThe repo-root `.gitattributes` may declare:\n\n```\nvbrief/.triage-cache/*.jsonl merge=union\n```\n\nThe `union` merge driver concatenates both sides' appended lines on auto-merge,\nso parallel append-only edits to the same JSON-lines file rebase without manual\nconflict surgery. It does not dedupe semantically similar records \u2014 downstream\nreaders should tolerate duplicate-looking entries.\n\n## See also\n\n- `.gitignore` \u2014 selective ignore rules for operator-private files\n- `.gitattributes` \u2014 merge driver for committed JSON-lines logs\n";
|
|
12
12
|
/** Layout-aware triage-cache README body for the active lifecycle tree (#2344 / #2349). */
|
|
13
13
|
export declare function generateTriageCacheReadmeBody(projectRoot: string): string;
|
|
14
14
|
/** Strip an inline `# ...` comment from a gitignore line. */
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { dirname, relative } from "node:path";
|
|
3
|
+
import { assertProjectionContained, ProjectionContainmentError, } from "../../fs/projection-containment.js";
|
|
3
4
|
import { MIGRATED_ARTIFACT_DIR, resolveLifecycleLayout } from "../../layout/resolve.js";
|
|
4
5
|
import { resolveCandidatesLogPath, resolveTriageCachePath, TRIAGE_CACHE_DIR_NAME, triageCacheRelPath, } from "../cache-path.js";
|
|
5
6
|
/** POSIX-style display path for `absPath` relative to `projectRoot` (#2109). */
|
|
@@ -85,78 +86,58 @@ const GITATTRIBUTES_EVAL_RATIONALE = "\n# Append-only JSON-lines logs under vbri
|
|
|
85
86
|
"# without manual conflict surgery. Note: merge=union does NOT dedupe; see\n" +
|
|
86
87
|
"# vbrief/.triage-cache/README.md for the operator-facing semantics.\n";
|
|
87
88
|
const EVAL_ENTRIES_RATIONALE_SENTINEL = "# vbrief/.triage-cache/ tracking governance (#1144, N4 of #1119).";
|
|
88
|
-
export const EVAL_README_BODY = `# \`vbrief/.triage-cache/\`
|
|
89
|
+
export const EVAL_README_BODY = `# \`vbrief/.triage-cache/\` — triage working-set files
|
|
89
90
|
|
|
90
|
-
This directory holds
|
|
91
|
-
slicing
|
|
92
|
-
|
|
91
|
+
This directory holds JSON-lines logs and scratch files that Deft triage and
|
|
92
|
+
slicing workflows emit. Deft configures your repo's \`.gitignore\` and
|
|
93
|
+
\`.gitattributes\` so some files stay local while team-shared records can be
|
|
94
|
+
committed.
|
|
93
95
|
|
|
94
|
-
##
|
|
96
|
+
## What lives here
|
|
95
97
|
|
|
96
|
-
| File |
|
|
98
|
+
| File | Committed? | Notes |
|
|
97
99
|
| --- | --- | --- |
|
|
98
|
-
| \`slices.jsonl\` | Yes
|
|
99
|
-
| \`candidates.jsonl\` | No
|
|
100
|
-
| \`summary-history.jsonl\` | No
|
|
101
|
-
| \`scope-lifecycle.jsonl\` | No
|
|
102
|
-
| \`decompositions/\` | No
|
|
103
|
-
| \`doctor-state.json\` | No
|
|
100
|
+
| \`slices.jsonl\` | Yes | Team-shared cohort records from slicing skills. New teammates use prior cohort outputs to spot orphans and avoid re-slicing the same scope. |
|
|
101
|
+
| \`candidates.jsonl\` | No | Your local triage accept / defer / reject stream. Re-create on a fresh clone with \`deft triage:bootstrap\`. |
|
|
102
|
+
| \`summary-history.jsonl\` | No | Local history of \`deft triage:summary\` output; not required for day-to-day work. |
|
|
103
|
+
| \`scope-lifecycle.jsonl\` | No | Local audit trail for scope demotions (\`deft scope:demote\`). Each operator's stream stays on their machine. |
|
|
104
|
+
| \`decompositions/\` | No | Draft story-decomposition scratch. Produced child story xBRIEFs live in lifecycle folders via \`deft scope:decompose\`. |
|
|
105
|
+
| \`doctor-state.json\` | No | Per-clone throttle state for \`deft doctor\` re-probe timing. |
|
|
104
106
|
|
|
105
|
-
|
|
107
|
+
Paths listed as "No" above are added to \`.gitignore\` during bootstrap; anything
|
|
108
|
+
not listed remains committable by default. The selective ignore entries live in
|
|
109
|
+
the repo-root \`.gitignore\` (\`vbrief/.triage-cache/candidates.jsonl\`,
|
|
106
110
|
\`vbrief/.triage-cache/summary-history.jsonl\`, \`vbrief/.triage-cache/scope-lifecycle.jsonl\`,
|
|
107
|
-
\`vbrief/.triage-cache/decompositions/\`, and \`vbrief/.triage-cache/doctor-state.json\`).
|
|
108
|
-
not listed above remain committed by default.
|
|
111
|
+
\`vbrief/.triage-cache/decompositions/\`, and \`vbrief/.triage-cache/doctor-state.json\`).
|
|
109
112
|
|
|
110
|
-
## Fresh
|
|
113
|
+
## Fresh clone
|
|
111
114
|
|
|
112
|
-
|
|
113
|
-
is absent. Regenerate it with:
|
|
115
|
+
If \`candidates.jsonl\` is missing, run:
|
|
114
116
|
|
|
115
117
|
\`\`\`
|
|
116
|
-
|
|
118
|
+
deft triage:bootstrap
|
|
117
119
|
\`\`\`
|
|
118
120
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
\`slices.jsonl\`; cohort records remain a team-shared resource.
|
|
121
|
+
Bootstrap rebuilds the local candidates log without altering committed
|
|
122
|
+
\`slices.jsonl\`.
|
|
122
123
|
|
|
123
|
-
##
|
|
124
|
+
## Merge behavior for \`*.jsonl\`
|
|
124
125
|
|
|
125
|
-
The repo-root \`.gitattributes\`
|
|
126
|
+
The repo-root \`.gitattributes\` may declare:
|
|
126
127
|
|
|
127
128
|
\`\`\`
|
|
128
129
|
vbrief/.triage-cache/*.jsonl merge=union
|
|
129
130
|
\`\`\`
|
|
130
131
|
|
|
131
|
-
The \`union\` merge driver concatenates both sides' appended lines on
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
- **Concatenation, not set-union.** When two branches append DIFFERENT
|
|
137
|
-
records to the file, the merge driver concatenates both sides' lines
|
|
138
|
-
-- there is no smart deduplication of "semantically similar" records.
|
|
139
|
-
(Identical line-for-line appends collapse because git's three-way
|
|
140
|
-
merge sees them as the same change, but distinct records always
|
|
141
|
-
survive verbatim, even if a downstream reader would consider them
|
|
142
|
-
redundant.) The append-only writers in \`scripts/candidates_log.py\`
|
|
143
|
-
mint a fresh \`decision_id\` per call, so genuinely duplicate records
|
|
144
|
-
are not the expected case, but downstream readers MUST tolerate
|
|
145
|
-
multiple records describing the same logical decision.
|
|
146
|
-
- **Single-operator scope only.** This is the foundational rebase
|
|
147
|
-
ergonomic for the single-operator case (operator A rebases their
|
|
148
|
-
feature branch onto a master that grew while they were AFK).
|
|
149
|
-
Multi-operator merge-conflict resolution is explicitly out of scope per
|
|
150
|
-
#1119 R4 (tracked separately as M1-M4 in #1183).
|
|
132
|
+
The \`union\` merge driver concatenates both sides' appended lines on auto-merge,
|
|
133
|
+
so parallel append-only edits to the same JSON-lines file rebase without manual
|
|
134
|
+
conflict surgery. It does not dedupe semantically similar records — downstream
|
|
135
|
+
readers should tolerate duplicate-looking entries.
|
|
151
136
|
|
|
152
137
|
## See also
|
|
153
138
|
|
|
154
|
-
-
|
|
155
|
-
|
|
156
|
-
- \`.gitignore\` -- selective gitignore entries for the operator-private
|
|
157
|
-
files.
|
|
158
|
-
- \`.gitattributes\` -- the \`merge=union\` rule.
|
|
159
|
-
- \`scripts/candidates_log.py\` -- the writer for \`candidates.jsonl\`.
|
|
139
|
+
- \`.gitignore\` — selective ignore rules for operator-private files
|
|
140
|
+
- \`.gitattributes\` — merge driver for committed JSON-lines logs
|
|
160
141
|
`;
|
|
161
142
|
/** Layout-aware triage-cache README body for the active lifecycle tree (#2344 / #2349). */
|
|
162
143
|
export function generateTriageCacheReadmeBody(projectRoot) {
|
|
@@ -173,6 +154,12 @@ export function generateTriageCacheReadmeBody(projectRoot) {
|
|
|
173
154
|
function stepOutcome(name, ok, message, details = {}, error = null) {
|
|
174
155
|
return { name, ok, message, error, details };
|
|
175
156
|
}
|
|
157
|
+
function containmentFailure(stepName, err) {
|
|
158
|
+
const message = err instanceof ProjectionContainmentError
|
|
159
|
+
? err.message
|
|
160
|
+
: `projection path containment refused: ${String(err)}`;
|
|
161
|
+
return stepOutcome(stepName, false, message, {}, message);
|
|
162
|
+
}
|
|
176
163
|
/** Strip an inline `# ...` comment from a gitignore line. */
|
|
177
164
|
export function stripGitignoreInlineComment(line) {
|
|
178
165
|
const stripped = line.trim();
|
|
@@ -198,7 +185,13 @@ function isCommentedGitignoreLine(raw, gitignoreLine) {
|
|
|
198
185
|
body = body.slice(1);
|
|
199
186
|
return body === gitignoreLine;
|
|
200
187
|
}
|
|
201
|
-
function ensureGitignoreLine(gitignorePath, line, stepName, createIfMissing, rationaleBlock, optInMessage) {
|
|
188
|
+
function ensureGitignoreLine(projectRoot, gitignorePath, line, stepName, createIfMissing, rationaleBlock, optInMessage) {
|
|
189
|
+
try {
|
|
190
|
+
assertProjectionContained(projectRoot, gitignorePath);
|
|
191
|
+
}
|
|
192
|
+
catch (err) {
|
|
193
|
+
return containmentFailure(stepName, err);
|
|
194
|
+
}
|
|
202
195
|
if (!existsSync(gitignorePath)) {
|
|
203
196
|
if (!createIfMissing) {
|
|
204
197
|
return stepOutcome(stepName, false, `.gitignore not present after the prior gitignore step; ${line} not written -- re-run bootstrap to retry`, { created: false, appended: false, skipped: "no-gitignore" }, "prior gitignore step did not create .gitignore");
|
|
@@ -251,7 +244,7 @@ function ensureGitignoreLine(gitignorePath, line, stepName, createIfMissing, rat
|
|
|
251
244
|
}
|
|
252
245
|
/** Append `.deft-cache/` to `.gitignore` when absent. */
|
|
253
246
|
export function stepEnsureGitignoreEntry(projectRoot) {
|
|
254
|
-
return ensureGitignoreLine(`${projectRoot}/.gitignore`, GITIGNORE_LINE, "ensure_gitignore_entry", true, DEFT_CACHE_RATIONALE, `${GITIGNORE_LINE} is commented out (operator has opted in to commit the cache per docs/privacy-nfr.md NFR-2; not re-adding)`);
|
|
247
|
+
return ensureGitignoreLine(projectRoot, `${projectRoot}/.gitignore`, GITIGNORE_LINE, "ensure_gitignore_entry", true, DEFT_CACHE_RATIONALE, `${GITIGNORE_LINE} is commented out (operator has opted in to commit the cache per docs/privacy-nfr.md NFR-2; not re-adding)`);
|
|
255
248
|
}
|
|
256
249
|
function formatBlanketWarning(blanketPresent) {
|
|
257
250
|
if (!blanketPresent)
|
|
@@ -274,7 +267,14 @@ function gitattributesHasEvalMergeUnion(body, glob) {
|
|
|
274
267
|
}
|
|
275
268
|
return false;
|
|
276
269
|
}
|
|
277
|
-
function ensureGitignoreSelectiveEntries(
|
|
270
|
+
function ensureGitignoreSelectiveEntries(projectRoot, stepName, entries) {
|
|
271
|
+
const gitignorePath = `${projectRoot}/.gitignore`;
|
|
272
|
+
try {
|
|
273
|
+
assertProjectionContained(projectRoot, gitignorePath);
|
|
274
|
+
}
|
|
275
|
+
catch (err) {
|
|
276
|
+
return containmentFailure(stepName, err);
|
|
277
|
+
}
|
|
278
278
|
let existing;
|
|
279
279
|
try {
|
|
280
280
|
existing = readFileSync(gitignorePath, { encoding: "utf8" });
|
|
@@ -316,7 +316,14 @@ function ensureGitignoreSelectiveEntries(gitignorePath, stepName, entries) {
|
|
|
316
316
|
rationale_already_present: rationaleAlreadyPresent,
|
|
317
317
|
});
|
|
318
318
|
}
|
|
319
|
-
function ensureGitattributesMergeUnion(
|
|
319
|
+
function ensureGitattributesMergeUnion(projectRoot, stepName, glob, ruleLine) {
|
|
320
|
+
const gitattributesPath = `${projectRoot}/.gitattributes`;
|
|
321
|
+
try {
|
|
322
|
+
assertProjectionContained(projectRoot, gitattributesPath);
|
|
323
|
+
}
|
|
324
|
+
catch (err) {
|
|
325
|
+
return containmentFailure(stepName, err);
|
|
326
|
+
}
|
|
320
327
|
if (existsSync(gitattributesPath)) {
|
|
321
328
|
let existing;
|
|
322
329
|
try {
|
|
@@ -358,6 +365,12 @@ function ensureGitattributesMergeUnion(gitattributesPath, stepName, glob, ruleLi
|
|
|
358
365
|
}
|
|
359
366
|
function ensureEvalReadme(options) {
|
|
360
367
|
const { projectRoot, readmePath, readmeRel, stepName } = options;
|
|
368
|
+
try {
|
|
369
|
+
assertProjectionContained(projectRoot, readmePath);
|
|
370
|
+
}
|
|
371
|
+
catch (err) {
|
|
372
|
+
return containmentFailure(stepName, err);
|
|
373
|
+
}
|
|
361
374
|
try {
|
|
362
375
|
readFileSync(readmePath, { encoding: "utf8" });
|
|
363
376
|
return stepOutcome(stepName, true, `${readmeRel} already present (no-op)`, {
|
|
@@ -381,8 +394,6 @@ function ensureEvalReadme(options) {
|
|
|
381
394
|
}
|
|
382
395
|
/** Ensure the #1144 hybrid policy is encoded in the repo (idempotent). */
|
|
383
396
|
export function stepEnsureGitignoreEvalEntries(projectRoot) {
|
|
384
|
-
const gitignorePath = `${projectRoot}/.gitignore`;
|
|
385
|
-
const gitattributesPath = `${projectRoot}/.gitattributes`;
|
|
386
397
|
// Layout-aware (#2109): resolve the README under the active lifecycle `.eval`
|
|
387
398
|
// dir (xbrief/ when migrated, else vbrief/) instead of a hardcoded vbrief/ path.
|
|
388
399
|
const readmePath = resolveTriageCachePath(projectRoot, "README.md");
|
|
@@ -392,13 +403,13 @@ export function stepEnsureGitignoreEvalEntries(projectRoot) {
|
|
|
392
403
|
const ruleLine = `${glob} merge=union`;
|
|
393
404
|
const stepName = "ensure_gitignore_eval_entries";
|
|
394
405
|
const details = {};
|
|
395
|
-
const giResult = ensureGitignoreSelectiveEntries(
|
|
406
|
+
const giResult = ensureGitignoreSelectiveEntries(projectRoot, stepName, entries);
|
|
396
407
|
if (!giResult.ok) {
|
|
397
408
|
Object.assign(details, giResult.details);
|
|
398
409
|
return stepOutcome(stepName, false, giResult.message, details, giResult.error ?? null);
|
|
399
410
|
}
|
|
400
411
|
Object.assign(details, giResult.details);
|
|
401
|
-
const gaResult = ensureGitattributesMergeUnion(
|
|
412
|
+
const gaResult = ensureGitattributesMergeUnion(projectRoot, stepName, glob, ruleLine);
|
|
402
413
|
if (!gaResult.ok) {
|
|
403
414
|
Object.assign(details, gaResult.details);
|
|
404
415
|
return stepOutcome(stepName, false, gaResult.message, details, gaResult.error ?? null);
|
|
@@ -1,4 +1,18 @@
|
|
|
1
1
|
import type { CachedIssue } from "./types.js";
|
|
2
|
+
/** Neutral title shown when the scanner fences injection-shaped title text. */
|
|
3
|
+
export declare const QUARANTINED_TITLE_PLACEHOLDER = "[quarantined title]";
|
|
4
|
+
/**
|
|
5
|
+
* Read `meta.json` `scan_result.passed` for a cache entry.
|
|
6
|
+
* Returns `false` when the scanner hard-failed, `true` when it passed, and
|
|
7
|
+
* `null` when meta is missing/unreadable (legacy or incomplete entries).
|
|
8
|
+
*/
|
|
9
|
+
export declare function readCacheScanPassed(entryDir: string): boolean | null;
|
|
10
|
+
/**
|
|
11
|
+
* Sanitize a queue title through the cache scanner.
|
|
12
|
+
* Returns `null` when the title hard-fails (omit the issue); otherwise a
|
|
13
|
+
* display title that never carries unfenced injection-shaped attacker text.
|
|
14
|
+
*/
|
|
15
|
+
export declare function sanitizeQueueTitle(rawTitle: string): string | null;
|
|
2
16
|
/** Read slices.jsonl records. */
|
|
3
17
|
export declare function resolveSlicesLogPath(options?: {
|
|
4
18
|
readonly slicesLogPath?: string | null;
|
|
@@ -1,10 +1,58 @@
|
|
|
1
1
|
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
2
2
|
import { join, resolve } from "node:path";
|
|
3
|
+
import { scan } from "../../cache/scanner.js";
|
|
3
4
|
import { resolveTriageCachePath } from "../cache-path.js";
|
|
4
5
|
import { extractAuthor, extractMilestone } from "../scope-drift/cache-walker.js";
|
|
5
6
|
import { CACHE_DIR_NAME, CACHE_SOURCE_GITHUB_ISSUE } from "./constants.js";
|
|
6
7
|
import { hasActiveScopeIgnores, isRawIssueScopeIgnored, resolveScopeIgnores, } from "./scope-ignores-filter.js";
|
|
7
8
|
import { blockedByIssueNumber, rankByIssueNumber } from "./scope-walk.js";
|
|
9
|
+
/** Neutral title shown when the scanner fences injection-shaped title text. */
|
|
10
|
+
export const QUARANTINED_TITLE_PLACEHOLDER = "[quarantined title]";
|
|
11
|
+
/**
|
|
12
|
+
* Read `meta.json` `scan_result.passed` for a cache entry.
|
|
13
|
+
* Returns `false` when the scanner hard-failed, `true` when it passed, and
|
|
14
|
+
* `null` when meta is missing/unreadable (legacy or incomplete entries).
|
|
15
|
+
*/
|
|
16
|
+
export function readCacheScanPassed(entryDir) {
|
|
17
|
+
const metaPath = join(entryDir, "meta.json");
|
|
18
|
+
if (!existsSync(metaPath)) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
try {
|
|
22
|
+
const parsed = JSON.parse(readFileSync(metaPath, { encoding: "utf8" }));
|
|
23
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
const scanResult = parsed.scan_result;
|
|
27
|
+
if (typeof scanResult !== "object" || scanResult === null) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
const passed = scanResult.passed;
|
|
31
|
+
return typeof passed === "boolean" ? passed : null;
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Sanitize a queue title through the cache scanner.
|
|
39
|
+
* Returns `null` when the title hard-fails (omit the issue); otherwise a
|
|
40
|
+
* display title that never carries unfenced injection-shaped attacker text.
|
|
41
|
+
*/
|
|
42
|
+
export function sanitizeQueueTitle(rawTitle) {
|
|
43
|
+
const result = scan(rawTitle);
|
|
44
|
+
if (!result.passed) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
if (result.flags.some((flag) => flag.category === "injection-heading")) {
|
|
48
|
+
return QUARANTINED_TITLE_PLACEHOLDER;
|
|
49
|
+
}
|
|
50
|
+
if (result.flags.some((flag) => flag.category === "invisible-unicode")) {
|
|
51
|
+
const stripped = result.transformed_content.replace(/\r?\n+$/u, "");
|
|
52
|
+
return stripped.length > 0 ? stripped : QUARANTINED_TITLE_PLACEHOLDER;
|
|
53
|
+
}
|
|
54
|
+
return rawTitle;
|
|
55
|
+
}
|
|
8
56
|
function cachedState(issue) {
|
|
9
57
|
if (issue === undefined) {
|
|
10
58
|
return "";
|
|
@@ -158,9 +206,21 @@ export function loadCachedIssues(repo, options) {
|
|
|
158
206
|
if (filterScopeIgnores && isRawIssueScopeIgnored(payload, scopeIgnores)) {
|
|
159
207
|
continue;
|
|
160
208
|
}
|
|
209
|
+
// Fail closed on scanner hard-fail: cachePut keeps raw.json but suppresses
|
|
210
|
+
// content.md when scan_result.passed is false. The queue must not promote
|
|
211
|
+
// those titles into the mandatory triage:queue agent context.
|
|
212
|
+
const scanPassed = readCacheScanPassed(entryDir);
|
|
213
|
+
if (scanPassed === false) {
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
const rawTitle = typeof payload.title === "string" ? payload.title : "";
|
|
217
|
+
const safeTitle = sanitizeQueueTitle(rawTitle);
|
|
218
|
+
if (safeTitle === null) {
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
161
221
|
issues.push({
|
|
162
222
|
number: n,
|
|
163
|
-
title:
|
|
223
|
+
title: safeTitle,
|
|
164
224
|
state,
|
|
165
225
|
labels: parseLabels(payload.labels),
|
|
166
226
|
author: extractAuthor(payload),
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
2
|
-
import { join, resolve } from "node:path";
|
|
2
|
+
import { join, relative, resolve, sep } from "node:path";
|
|
3
3
|
import { extractLinkTargets, shouldSkipLinkTarget } from "./link-parser.js";
|
|
4
4
|
const EXCLUDE_DIRS = new Set([
|
|
5
5
|
".git",
|
|
@@ -55,7 +55,7 @@ function findBrokenLinks(cwd) {
|
|
|
55
55
|
catch {
|
|
56
56
|
continue;
|
|
57
57
|
}
|
|
58
|
-
const rel = md.startsWith(
|
|
58
|
+
const rel = md.startsWith(root + sep) ? relative(root, md) : md;
|
|
59
59
|
const lines = text.split("\n");
|
|
60
60
|
for (let i = 0; i < lines.length; i += 1) {
|
|
61
61
|
const line = lines[i] ?? "";
|
package/dist/value/readback.d.ts
CHANGED
package/dist/value/readback.js
CHANGED
|
@@ -415,7 +415,11 @@ export function runValueShow(options) {
|
|
|
415
415
|
};
|
|
416
416
|
}
|
|
417
417
|
const windowMs = parseWindowMs(options.window);
|
|
418
|
-
const trend = computeValueShowTrend(root, {
|
|
418
|
+
const trend = computeValueShowTrend(root, {
|
|
419
|
+
windowMs,
|
|
420
|
+
logPath: options.logPath,
|
|
421
|
+
now: options.now,
|
|
422
|
+
});
|
|
419
423
|
const empty = trend.total === 0;
|
|
420
424
|
if (options.format === "json") {
|
|
421
425
|
return {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
2
2
|
import { basename, join, resolve } from "node:path";
|
|
3
3
|
import { referenceTypeMatches } from "@deftai/directive-types";
|
|
4
|
+
import { createIssueComment, editIssueCommentBody, GitHubBodyError, } from "../intake/github-body.js";
|
|
4
5
|
import { hasArtifactSuffix, resolveLifecycleRoot, stripArtifactSuffix } from "../layout/resolve.js";
|
|
5
6
|
import { call } from "../scm/call.js";
|
|
6
7
|
import { extractIssueRef } from "../triage/reconcile/parse-uri.js";
|
|
@@ -229,22 +230,22 @@ export class ScmUmbrellaClient {
|
|
|
229
230
|
return comments;
|
|
230
231
|
}
|
|
231
232
|
editComment(repo, commentId, body) {
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
233
|
+
try {
|
|
234
|
+
editIssueCommentBody(repo, commentId, { body });
|
|
235
|
+
}
|
|
236
|
+
catch (exc) {
|
|
237
|
+
const message = exc instanceof GitHubBodyError ? exc.message : String(exc);
|
|
238
|
+
throw new UmbrellaScmError(`edit comment ${commentId} (${repo}) failed: ${message}`);
|
|
235
239
|
}
|
|
236
240
|
}
|
|
237
241
|
createComment(repo, issueNumber, body) {
|
|
238
|
-
const proc = call(SCM_SOURCE, "api", ["-X", "POST", `repos/${repo}/issues/${issueNumber}/comments`, "--input", "-"], { input: JSON.stringify({ body }) });
|
|
239
|
-
if (proc.returncode !== 0) {
|
|
240
|
-
throw new UmbrellaScmError(`create comment #${issueNumber} (${repo}) failed: ${(proc.stderr || "").trim()}`);
|
|
241
|
-
}
|
|
242
242
|
try {
|
|
243
|
-
const
|
|
244
|
-
return typeof
|
|
243
|
+
const created = createIssueComment(repo, issueNumber, { body });
|
|
244
|
+
return typeof created.id === "number" ? created.id : null;
|
|
245
245
|
}
|
|
246
|
-
catch {
|
|
247
|
-
|
|
246
|
+
catch (exc) {
|
|
247
|
+
const message = exc instanceof GitHubBodyError ? exc.message : String(exc);
|
|
248
|
+
throw new UmbrellaScmError(`create comment #${issueNumber} (${repo}) failed: ${message}`);
|
|
248
249
|
}
|
|
249
250
|
}
|
|
250
251
|
}
|
|
@@ -20,16 +20,31 @@ export const CONSUMER_TOOLS = [
|
|
|
20
20
|
export const TOOLS = MAINTAINER_TOOLS;
|
|
21
21
|
const DEFAULT_TIMEOUT_MS = 10_000;
|
|
22
22
|
export function defaultCommandRunner(command, timeoutMs) {
|
|
23
|
+
const bin = command[0] ?? "";
|
|
24
|
+
const args = command.slice(1);
|
|
25
|
+
const trySpawn = (shell) => childProcess.execFileSync(bin, args, {
|
|
26
|
+
encoding: "utf8",
|
|
27
|
+
timeout: timeoutMs,
|
|
28
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
29
|
+
shell,
|
|
30
|
+
});
|
|
23
31
|
try {
|
|
24
|
-
const stdout =
|
|
25
|
-
encoding: "utf8",
|
|
26
|
-
timeout: timeoutMs,
|
|
27
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
28
|
-
});
|
|
32
|
+
const stdout = trySpawn(false);
|
|
29
33
|
return { returncode: 0, stdout: typeof stdout === "string" ? stdout : "", stderr: "" };
|
|
30
34
|
}
|
|
31
35
|
catch (err) {
|
|
32
36
|
const e = err;
|
|
37
|
+
// win32: npm global shims are `.cmd` and need shell/PATHEXT (#2467 / #2415).
|
|
38
|
+
if (e.code === "ENOENT" && process.platform === "win32") {
|
|
39
|
+
try {
|
|
40
|
+
const stdout = trySpawn(true);
|
|
41
|
+
return { returncode: 0, stdout: typeof stdout === "string" ? stdout : "", stderr: "" };
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
// Shell still failed — binary is absent (cmd.exe often exits 1, not ENOENT).
|
|
45
|
+
return { error: "not-found", message: "" };
|
|
46
|
+
}
|
|
47
|
+
}
|
|
33
48
|
if (e.code === "ENOENT") {
|
|
34
49
|
return { error: "not-found", message: "" };
|
|
35
50
|
}
|
|
@@ -5,6 +5,7 @@ export interface EvaluateResult {
|
|
|
5
5
|
readonly stream: OutputStream;
|
|
6
6
|
}
|
|
7
7
|
export declare const REQUIRED_HOOKS: readonly ["pre-commit", "pre-push"];
|
|
8
|
+
export declare const REQUIRED_HOOK_SUPPORT_FILES: readonly ["_deft-run.sh"];
|
|
8
9
|
/** Substrings each hook must contain when it dispatches through the deft CLI (#2049). */
|
|
9
10
|
export declare const PRE_COMMIT_DEFT_COMMANDS: readonly ["verify:branch", "verify:encoding"];
|
|
10
11
|
export declare const PRE_PUSH_DEFT_COMMANDS: readonly ["preflight-gh"];
|
|
@@ -15,6 +16,7 @@ export type GitConfigReader = (projectRoot: string) => {
|
|
|
15
16
|
export interface EvaluateOptions {
|
|
16
17
|
readonly gitConfigReader?: GitConfigReader;
|
|
17
18
|
readonly platform?: NodeJS.Platform;
|
|
19
|
+
readonly hookExecutable?: (hookPath: string) => boolean;
|
|
18
20
|
}
|
|
19
21
|
/** Drop shell ``#`` comment lines before pattern scans (#2049 shipped-hook false positives). */
|
|
20
22
|
export declare function stripShellCommentLines(content: string): string;
|
|
@@ -2,6 +2,7 @@ import * as childProcess from "node:child_process";
|
|
|
2
2
|
import { accessSync, constants, readFileSync, statSync } from "node:fs";
|
|
3
3
|
import { join, resolve } from "node:path";
|
|
4
4
|
export const REQUIRED_HOOKS = ["pre-commit", "pre-push"];
|
|
5
|
+
export const REQUIRED_HOOK_SUPPORT_FILES = ["_deft-run.sh"];
|
|
5
6
|
/** Substrings each hook must contain when it dispatches through the deft CLI (#2049). */
|
|
6
7
|
export const PRE_COMMIT_DEFT_COMMANDS = ["verify:branch", "verify:encoding"];
|
|
7
8
|
export const PRE_PUSH_DEFT_COMMANDS = ["preflight-gh"];
|
|
@@ -52,7 +53,7 @@ function isDirectory(path) {
|
|
|
52
53
|
function isPosix(platform) {
|
|
53
54
|
return platform !== "win32";
|
|
54
55
|
}
|
|
55
|
-
function
|
|
56
|
+
function defaultHookExecutable(hookPath) {
|
|
56
57
|
try {
|
|
57
58
|
accessSync(hookPath, constants.X_OK);
|
|
58
59
|
return true;
|
|
@@ -112,6 +113,7 @@ export function evaluate(projectRoot, options = {}) {
|
|
|
112
113
|
const root = resolve(projectRoot);
|
|
113
114
|
const gitReader = options.gitConfigReader ?? defaultGitConfigReader;
|
|
114
115
|
const platform = options.platform ?? process.platform;
|
|
116
|
+
const hookExecutable = options.hookExecutable ?? defaultHookExecutable;
|
|
115
117
|
if (!isDirectory(root)) {
|
|
116
118
|
return {
|
|
117
119
|
code: 2,
|
|
@@ -173,29 +175,29 @@ export function evaluate(projectRoot, options = {}) {
|
|
|
173
175
|
};
|
|
174
176
|
}
|
|
175
177
|
}
|
|
178
|
+
const contentIssues = [];
|
|
179
|
+
const missingSupport = REQUIRED_HOOK_SUPPORT_FILES.filter((h) => !isFile(join(hooksDir, h)));
|
|
180
|
+
if (missingSupport.length > 0) {
|
|
181
|
+
contentIssues.push(`${hooksDir} is missing ${missingSupport.join(", ")} helper(s); pre-commit/pre-push source them at runtime`);
|
|
182
|
+
}
|
|
176
183
|
const preCommitIssue = validateHookContent("pre-commit", readHookContent(join(hooksDir, "pre-commit")), PRE_COMMIT_DEFT_COMMANDS);
|
|
177
184
|
if (preCommitIssue) {
|
|
178
|
-
|
|
179
|
-
code: 1,
|
|
180
|
-
message: `❌ deft hooks wired but NON-FUNCTIONAL: ${preCommitIssue} (#2049).\n` +
|
|
181
|
-
" Recovery: re-run the deft installer / `task setup` to refresh .githooks/.",
|
|
182
|
-
stream: "stderr",
|
|
183
|
-
};
|
|
185
|
+
contentIssues.push(`${preCommitIssue} (#2049)`);
|
|
184
186
|
}
|
|
185
187
|
const prePushContent = readHookContent(join(hooksDir, "pre-push"));
|
|
186
188
|
const prePushIssue = validateHookContent("pre-push", prePushContent, PRE_PUSH_DEFT_COMMANDS);
|
|
187
189
|
if (prePushIssue) {
|
|
188
|
-
|
|
189
|
-
code: 1,
|
|
190
|
-
message: `❌ deft hooks wired but NON-FUNCTIONAL: ${prePushIssue} (#2049).\n` +
|
|
191
|
-
" Recovery: re-run the deft installer / `task setup` to refresh .githooks/.",
|
|
192
|
-
stream: "stderr",
|
|
193
|
-
};
|
|
190
|
+
contentIssues.push(`${prePushIssue} (#2049)`);
|
|
194
191
|
}
|
|
195
192
|
if (prePushContent && prePushInvokesVerifyBranch(prePushContent)) {
|
|
193
|
+
contentIssues.push("pre-push must not invoke verify:branch (#1814)");
|
|
194
|
+
}
|
|
195
|
+
if (contentIssues.length > 0) {
|
|
196
196
|
return {
|
|
197
197
|
code: 1,
|
|
198
|
-
message: "❌ deft hooks wired but NON-FUNCTIONAL
|
|
198
|
+
message: "❌ deft hooks wired but NON-FUNCTIONAL:\n" +
|
|
199
|
+
contentIssues.map((issue) => ` - ${issue.replace(/\r?\n/g, " ")}`).join("\n") +
|
|
200
|
+
"\n" +
|
|
199
201
|
" Recovery: re-run the deft installer / `task setup` to refresh .githooks/.",
|
|
200
202
|
stream: "stderr",
|
|
201
203
|
};
|
|
@@ -203,7 +205,7 @@ export function evaluate(projectRoot, options = {}) {
|
|
|
203
205
|
return {
|
|
204
206
|
code: 0,
|
|
205
207
|
message: `✓ deft hooks installed and functional: core.hooksPath=${hooksPath}, ` +
|
|
206
|
-
`hooks ${REQUIRED_HOOKS.join(", ")} present and dispatch via deft CLI (#2049).`,
|
|
208
|
+
`hooks ${REQUIRED_HOOKS.join(", ")} plus ${REQUIRED_HOOK_SUPPORT_FILES.join(", ")} present and dispatch via deft CLI (#2049).`,
|
|
207
209
|
stream: "stdout",
|
|
208
210
|
};
|
|
209
211
|
}
|
|
@@ -205,6 +205,15 @@ export function runXbriefMigration(args, _io) {
|
|
|
205
205
|
};
|
|
206
206
|
}
|
|
207
207
|
if (!isDirectory(legacyDir)) {
|
|
208
|
+
// Already on canonical xbrief/ with no vbrief/ tree — residual markers (e.g. a
|
|
209
|
+
// stale deposited v0.6 schema under xbrief/schemas/) are refreshed by update,
|
|
210
|
+
// not migrate:xbrief (#2368).
|
|
211
|
+
if (convergence.xbriefHasContent && !convergence.vbriefPresent) {
|
|
212
|
+
return {
|
|
213
|
+
kind: "noop",
|
|
214
|
+
message: "Project is already on the xbrief layout — residual legacy markers (e.g. a stale deposited schema) cannot be cleared by migrate:xbrief when no vbrief/ tree remains. Run `directive update` to refresh deposited schema files.",
|
|
215
|
+
};
|
|
216
|
+
}
|
|
208
217
|
return {
|
|
209
218
|
kind: "config",
|
|
210
219
|
message: `Legacy markers detected but '${LEGACY_ARTIFACT_DIR}/' directory is missing.`,
|