@deftai/directive-core 0.73.0 → 0.74.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 (58) hide show
  1. package/dist/branch/evaluate.js +2 -1
  2. package/dist/check/orchestrator.d.ts +3 -0
  3. package/dist/check/orchestrator.js +2 -1
  4. package/dist/codebase/map.js +2 -0
  5. package/dist/deposit/copy-tree.js +31 -7
  6. package/dist/doctor/checks.d.ts +9 -0
  7. package/dist/doctor/checks.js +67 -0
  8. package/dist/doctor/main.js +2 -1
  9. package/dist/fs/projection-containment.d.ts +35 -0
  10. package/dist/fs/projection-containment.js +118 -0
  11. package/dist/init-deposit/hygiene.d.ts +6 -0
  12. package/dist/init-deposit/hygiene.js +5 -1
  13. package/dist/init-deposit/refresh.d.ts +16 -2
  14. package/dist/init-deposit/refresh.js +144 -50
  15. package/dist/init-deposit/scaffold.d.ts +21 -0
  16. package/dist/init-deposit/scaffold.js +84 -10
  17. package/dist/policy/disclosure.d.ts +1 -0
  18. package/dist/policy/disclosure.js +1 -0
  19. package/dist/policy/index.d.ts +1 -0
  20. package/dist/policy/index.js +1 -0
  21. package/dist/policy/policy-invocation.d.ts +5 -0
  22. package/dist/policy/policy-invocation.js +9 -0
  23. package/dist/policy/resolve.js +4 -2
  24. package/dist/policy/value-feedback.js +6 -3
  25. package/dist/pr-merge-readiness/constants.d.ts +4 -0
  26. package/dist/pr-merge-readiness/constants.js +4 -0
  27. package/dist/pr-merge-readiness/evaluate.js +3 -0
  28. package/dist/pr-merge-readiness/mergeability.d.ts +8 -6
  29. package/dist/pr-merge-readiness/mergeability.js +15 -10
  30. package/dist/pr-merge-readiness/output.js +12 -6
  31. package/dist/pr-merge-readiness/parse.d.ts +1 -0
  32. package/dist/pr-merge-readiness/parse.js +9 -1
  33. package/dist/pr-merge-readiness/types.d.ts +2 -0
  34. package/dist/preflight-cache/evaluate.d.ts +2 -0
  35. package/dist/preflight-cache/evaluate.js +14 -3
  36. package/dist/preflight-cache/index.d.ts +1 -1
  37. package/dist/preflight-cache/index.js +1 -1
  38. package/dist/release/constants.d.ts +2 -0
  39. package/dist/release/constants.js +2 -0
  40. package/dist/release/preflight.d.ts +2 -0
  41. package/dist/release/preflight.js +14 -1
  42. package/dist/scope/index.d.ts +1 -0
  43. package/dist/scope/index.js +1 -0
  44. package/dist/scope/main.js +24 -1
  45. package/dist/scope/open-umbrella-warning.d.ts +12 -0
  46. package/dist/scope/open-umbrella-warning.js +403 -0
  47. package/dist/swarm/constants.d.ts +1 -1
  48. package/dist/swarm/constants.js +2 -1
  49. package/dist/swarm/subagent-backend.js +2 -1
  50. package/dist/triage/bootstrap/gitignore.d.ts +1 -1
  51. package/dist/triage/bootstrap/gitignore.js +69 -58
  52. package/dist/ts-check-lane/run-lane.d.ts +4 -0
  53. package/dist/ts-check-lane/run-lane.js +20 -6
  54. package/dist/value/feedback-file.js +3 -2
  55. package/dist/value/readback.js +3 -2
  56. package/dist/vbrief-reconcile/umbrellas.js +12 -11
  57. package/dist/xbrief-migrate/migrate-project.js +9 -0
  58. package/package.json +3 -3
@@ -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/\` -- triage working-set artefacts
89
+ export const EVAL_README_BODY = `# \`vbrief/.triage-cache/\` triage working-set files
89
90
 
90
- This directory holds the append-only JSON-lines logs that the triage and
91
- slicing skills emit. The framework governs which files in here are tracked
92
- by git versus gitignored using a **hybrid policy** (#1144, child of #1119).
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
- ## Tracking policy
96
+ ## What lives here
95
97
 
96
- | File | Tracked? | Why |
98
+ | File | Committed? | Notes |
97
99
  | --- | --- | --- |
98
- | \`slices.jsonl\` | Yes -- **committed** | Team-shared cohort records produced by slicing skills (D13 / #1132). New operators joining the team need to see prior cohort outputs to detect orphans and avoid re-slicing the same scope. |
99
- | \`candidates.jsonl\` | No -- **gitignored** | Operator-private triage decisions (#845 Story 2). Each operator's local accept / defer / reject stream is per-machine state; sharing it would conflate operators' timing + identity across the team. Re-derive on a fresh clone via \`task triage:bootstrap\`. |
100
- | \`summary-history.jsonl\` | No -- **gitignored** | Operator-private observability for \`task triage:summary\` output time-series. Not load-bearing for any decision. |
101
- | \`scope-lifecycle.jsonl\` | No -- **gitignored** | Operator-private scope-lifecycle audit decisions (D1 / #1121). Each demote (\`task scope:demote\`) appends one entry including a \`demote_meta\` block (\`was_promoted\`, \`original_promotion_decision_id\`, \`days_in_pending\`, \`demote_reason\`, \`demoted_from\`). Per-operator stream; sharing would conflate operators' demote timing across the team. Lightweight metrics over this log are tracked separately at #1180. |
102
- | \`decompositions/\` | No -- **gitignored** | Temporary story-decomposition proposal drafts. These JSON drafts are local scratch artifacts, not vBRIEFs; generated child story vBRIEFs are created by \`task scope:decompose\` in lifecycle folders, defaulting to \`vbrief/pending/\`. |
103
- | \`doctor-state.json\` | No -- **gitignored** | Per-machine \`task doctor\` throttle state (last exit code + timestamps) persisted to gate the 24h/4h re-probe window (#1308 / #1464). Local to each clone; never committed. |
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
- The gitignore lines live in the repo-root \`.gitignore\` (\`vbrief/.triage-cache/candidates.jsonl\`,
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\`). All paths
108
- not listed above remain committed by default.
111
+ \`vbrief/.triage-cache/decompositions/\`, and \`vbrief/.triage-cache/doctor-state.json\`).
109
112
 
110
- ## Fresh-clone regeneration
113
+ ## Fresh clone
111
114
 
112
- On a fresh clone (or any machine that has never run triage), \`candidates.jsonl\`
113
- is absent. Regenerate it with:
115
+ If \`candidates.jsonl\` is missing, run:
114
116
 
115
117
  \`\`\`
116
- task triage:bootstrap
118
+ deft triage:bootstrap
117
119
  \`\`\`
118
120
 
119
- The bootstrap path detects the missing file, runs the auto-classifier, and
120
- writes a fresh \`vbrief/.triage-cache/candidates.jsonl\`. It does NOT touch the tracked
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
- ## \`merge=union\` policy for \`*.jsonl\`
124
+ ## Merge behavior for \`*.jsonl\`
124
125
 
125
- The repo-root \`.gitattributes\` declares:
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
- auto-merge, so two branches that each appended a different record to the
133
- same JSON-lines file rebase cleanly without operator surgery. Two things
134
- operators should know:
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
- - Current Shape comment on #1144 for the canonical decisions (the source
155
- of truth this README documents).
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(gitignorePath, stepName, entries) {
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(gitattributesPath, stepName, glob, ruleLine) {
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(gitignorePath, stepName, entries);
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(gitattributesPath, stepName, glob, ruleLine);
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);
@@ -25,6 +25,8 @@ export declare const SKIP_NOTICE: string;
25
25
  /** Result of a single lane command invocation. Mirrors a subset of SpawnSyncReturns. */
26
26
  export interface RunnerResult {
27
27
  readonly status: number | null;
28
+ readonly signal?: NodeJS.Signals | null;
29
+ readonly error?: Error;
28
30
  }
29
31
  export type LaneRunner = (argv: readonly string[], cwd: string) => RunnerResult;
30
32
  export interface RunTsLaneOptions {
@@ -35,6 +37,8 @@ export interface RunTsLaneOptions {
35
37
  /** Injected sink for human-facing notices (defaults to stdout). */
36
38
  readonly out?: (message: string) => void;
37
39
  }
40
+ /** Windows command shims (.cmd/.bat) need a shell; native executables do not. */
41
+ export declare function shouldUseShellForCommand(command: string, platform?: NodeJS.Platform): boolean;
38
42
  export interface ResolvePnpmOptions {
39
43
  readonly env?: NodeJS.ProcessEnv;
40
44
  readonly platform?: NodeJS.Platform;
@@ -18,7 +18,7 @@
18
18
  */
19
19
  import { spawnSync } from "node:child_process";
20
20
  import { existsSync } from "node:fs";
21
- import { join } from "node:path";
21
+ import { posix, win32 } from "node:path";
22
22
  /**
23
23
  * Run order is deliberate: lint (cheapest, catches the biome class first),
24
24
  * then build, then the test suite.
@@ -31,11 +31,20 @@ export const LANE_COMMANDS = [
31
31
  export const SKIP_NOTICE = "[ts:check-lane] pnpm not found on PATH -- skipping the TypeScript lane " +
32
32
  "(build/lint/test). The TS engine stays validated by the dedicated CI job. " +
33
33
  "Install the Node toolchain (pnpm) to run the TS lane locally.";
34
- /** Default runner: a non-shell, inherited-stdio pnpm invocation. */
34
+ /** Windows command shims (.cmd/.bat) need a shell; native executables do not. */
35
+ export function shouldUseShellForCommand(command, platform = process.platform) {
36
+ return platform === "win32" && /\.(?:cmd|bat)$/i.test(command);
37
+ }
38
+ /** Default runner: an inherited-stdio pnpm invocation. */
35
39
  function defaultRunner(argv, cwd) {
36
40
  const [command, ...rest] = argv;
37
- const result = spawnSync(command ?? "", rest, { cwd, stdio: "inherit" });
38
- return { status: result.status };
41
+ const commandPath = command ?? "";
42
+ const result = spawnSync(commandPath, rest, {
43
+ cwd,
44
+ stdio: "inherit",
45
+ shell: shouldUseShellForCommand(commandPath),
46
+ });
47
+ return { error: result.error, signal: result.signal, status: result.status };
39
48
  }
40
49
  /** Resolve the pnpm executable path, or null when it is not installed. */
41
50
  export function resolvePnpm(options = {}) {
@@ -49,11 +58,12 @@ export function resolvePnpm(options = {}) {
49
58
  const isWindows = platform === "win32";
50
59
  const exts = isWindows ? (env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";") : [""];
51
60
  const sep = isWindows ? ";" : ":";
61
+ const joinPath = isWindows ? win32.join : posix.join;
52
62
  for (const dir of pathValue.split(sep)) {
53
63
  if (dir === "")
54
64
  continue;
55
65
  for (const ext of exts) {
56
- const candidate = join(dir, `pnpm${ext}`);
66
+ const candidate = joinPath(dir, `pnpm${ext}`);
57
67
  if (exists(candidate)) {
58
68
  return candidate;
59
69
  }
@@ -85,7 +95,11 @@ export function runTsLane(projectRoot, options) {
85
95
  // failure -- this mirrors the Python oracle, whose returncode is negative
86
96
  // (non-zero) for a signal-killed process.
87
97
  if (code === null) {
88
- out(`[ts:check-lane] \`pnpm ${command.join(" ")}\` was killed by a signal before exit -- treating as failure.`);
98
+ if (result.error) {
99
+ out(`[ts:check-lane] \`pnpm ${command.join(" ")}\` failed to start: ${result.error.message}`);
100
+ return 1;
101
+ }
102
+ out(`[ts:check-lane] \`pnpm ${command.join(" ")}\` was killed by ${result.signal ?? "a signal"} before exit -- treating as failure.`);
89
103
  return 1;
90
104
  }
91
105
  if (code !== 0) {
@@ -1,6 +1,7 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import { resolve } from "node:path";
3
3
  import { isFrameworkRepoRoot } from "../check/orchestrator.js";
4
+ import { policyColonInvocation } from "../policy/policy-invocation.js";
4
5
  import { isValueFeedbackPathAllowed, resolveValueFeedback } from "../policy/value-feedback.js";
5
6
  import { GhRestError, restCreateIssue, restIssueListPaginated, } from "../scm/gh-rest.js";
6
7
  import { resolveProjectRoot } from "../scope/project-context.js";
@@ -146,8 +147,8 @@ export function runFeedbackFile(options) {
146
147
  body,
147
148
  repo,
148
149
  message: "[deft feedback] Blocked: plan.policy.valueFeedback upstreamPrompt is OFF. " +
149
- "Enable with `task policy:enable-value-feedback -- --confirm` and set upstreamPrompt, " +
150
- "or inspect via `task policy:show --field=valueFeedback`.\n",
150
+ `Enable with \`${policyColonInvocation("enable-value-feedback", " -- --confirm")}\` and set upstreamPrompt, ` +
151
+ `or inspect via \`${policyColonInvocation("show", " --field=valueFeedback")}\`.\n`,
151
152
  };
152
153
  }
153
154
  const offline = isOffline();
@@ -3,6 +3,7 @@ import { join, resolve } from "node:path";
3
3
  import { runningInsideDeftRepo } from "../doctor/paths.js";
4
4
  import { ALL_ATTRIBUTION_EVENT_NAMES } from "../events/attribution-constants.js";
5
5
  import { DEFAULT_EVENT_LOG, readEvents } from "../lifecycle/events.js";
6
+ import { policyColonInvocation } from "../policy/policy-invocation.js";
6
7
  import { isValueFeedbackPathAllowed, resolveValueFeedback, } from "../policy/value-feedback.js";
7
8
  import { resolveProjectRoot } from "../scope/project-context.js";
8
9
  import { MAX_LINE_CHARS } from "../triage/welcome/constants.js";
@@ -364,7 +365,7 @@ export function formatValueShowReport(trend) {
364
365
  if (trend.total === 0) {
365
366
  return (`[value] No attributed signals in the last ${trend.windowLabel} ` +
366
367
  "(ledger empty for this window).\n" +
367
- "Inspect policy: `task policy:show --field=valueFeedback`.\n");
368
+ `Inspect policy: \`${policyColonInvocation("show", " --field=valueFeedback")}\`.\n`);
368
369
  }
369
370
  const classParts = ["value", "bypass", "adoption", "friction"]
370
371
  .filter((key) => trend.byClass[key] > 0)
@@ -409,7 +410,7 @@ export function runValueShow(options) {
409
410
  gated: true,
410
411
  empty: true,
411
412
  text: "[value] Blocked: plan.policy.valueFeedback is OFF. " +
412
- "Enable with `task policy:enable-value-feedback -- --confirm`.\n",
413
+ `Enable with \`${policyColonInvocation("enable-value-feedback", " -- --confirm")}\`.\n`,
413
414
  trend: null,
414
415
  };
415
416
  }
@@ -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
- const proc = call(SCM_SOURCE, "api", ["-X", "PATCH", `repos/${repo}/issues/comments/${commentId}`, "--input", "-"], { input: JSON.stringify({ body }) });
233
- if (proc.returncode !== 0) {
234
- throw new UmbrellaScmError(`edit comment ${commentId} (${repo}) failed: ${(proc.stderr || "").trim()}`);
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 data = JSON.parse(proc.stdout || "{}");
244
- return typeof data.id === "number" ? data.id : null;
243
+ const created = createIssueComment(repo, issueNumber, { body });
244
+ return typeof created.id === "number" ? created.id : null;
245
245
  }
246
- catch {
247
- return null;
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
  }
@@ -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.`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deftai/directive-core",
3
- "version": "0.73.0",
3
+ "version": "0.74.0",
4
4
  "description": "TypeScript engine core for the Directive framework.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -281,8 +281,8 @@
281
281
  "provenance": true
282
282
  },
283
283
  "dependencies": {
284
- "@deftai/directive-content": "^0.73.0",
285
- "@deftai/directive-types": "^0.73.0",
284
+ "@deftai/directive-content": "^0.74.0",
285
+ "@deftai/directive-types": "^0.74.0",
286
286
  "archiver": "^8.0.0"
287
287
  },
288
288
  "scripts": {