@deftai/directive-core 0.68.1 → 0.70.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/deposit/contain.d.ts +41 -0
- package/dist/deposit/contain.js +113 -0
- package/dist/deposit/copy-tree.js +8 -2
- package/dist/doctor/checks.d.ts +11 -0
- package/dist/doctor/checks.js +75 -2
- package/dist/doctor/constants.d.ts +9 -0
- package/dist/doctor/constants.js +11 -0
- package/dist/doctor/main.d.ts +47 -1
- package/dist/doctor/main.js +406 -3
- package/dist/doctor/manifest.d.ts +20 -0
- package/dist/doctor/manifest.js +22 -0
- package/dist/doctor/payload-staleness.d.ts +8 -0
- package/dist/doctor/payload-staleness.js +11 -8
- package/dist/doctor/types.d.ts +56 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/init-deposit/gitignore.d.ts +18 -0
- package/dist/init-deposit/gitignore.js +94 -15
- package/dist/init-deposit/headless-manifest.d.ts +87 -0
- package/dist/init-deposit/headless-manifest.js +261 -0
- package/dist/init-deposit/index.d.ts +3 -0
- package/dist/init-deposit/index.js +3 -0
- package/dist/init-deposit/init-deposit.js +6 -0
- package/dist/init-deposit/init-dispatch.d.ts +90 -0
- package/dist/init-deposit/init-dispatch.js +170 -0
- package/dist/init-deposit/refresh.d.ts +79 -2
- package/dist/init-deposit/refresh.js +182 -5
- package/dist/init-deposit/scaffold.d.ts +25 -0
- package/dist/init-deposit/scaffold.js +57 -0
- package/dist/init-deposit/untrack-core.d.ts +84 -0
- package/dist/init-deposit/untrack-core.js +150 -0
- package/dist/intake/issue-ingest.d.ts +11 -0
- package/dist/intake/issue-ingest.js +86 -11
- package/dist/policy/plan-extensions.d.ts +31 -0
- package/dist/policy/plan-extensions.js +45 -0
- package/dist/policy/wip.d.ts +2 -2
- package/dist/policy/wip.js +2 -2
- package/dist/resolution/classify.d.ts +41 -0
- package/dist/resolution/classify.js +138 -0
- package/dist/resolution/engine-ladder.d.ts +98 -0
- package/dist/resolution/engine-ladder.js +185 -0
- package/dist/resolution/index.d.ts +19 -0
- package/dist/resolution/index.js +19 -0
- package/dist/resolution/integrity.d.ts +48 -0
- package/dist/resolution/integrity.js +80 -0
- package/dist/resolution/package-manager.d.ts +77 -0
- package/dist/resolution/package-manager.js +103 -0
- package/dist/resolution/pin.d.ts +73 -0
- package/dist/resolution/pin.js +169 -0
- package/dist/resolution/plan.d.ts +66 -0
- package/dist/resolution/plan.js +219 -0
- package/dist/resolution/skew-policy.d.ts +46 -0
- package/dist/resolution/skew-policy.js +120 -0
- package/dist/session/session-start.d.ts +2 -0
- package/dist/session/session-start.js +28 -1
- package/dist/swarm/routing.js +10 -3
- package/dist/triage/bulk/index.d.ts +11 -2
- package/dist/triage/bulk/index.js +41 -4
- package/dist/triage/help/registry-data.d.ts +5 -5
- package/dist/triage/help/registry-data.js +18 -5
- package/dist/triage/scope/cli.d.ts +2 -1
- package/dist/triage/scope/cli.js +30 -6
- package/dist/triage/scope/mutations.d.ts +10 -0
- package/dist/triage/scope/mutations.js +22 -0
- package/dist/triage/welcome/constants.d.ts +1 -2
- package/dist/triage/welcome/constants.js +1 -2
- package/dist/triage/welcome/index.d.ts +1 -0
- package/dist/triage/welcome/index.js +1 -0
- package/dist/triage/welcome/onboard.d.ts +35 -0
- package/dist/triage/welcome/onboard.js +94 -0
- package/dist/umbrella-current-shape/index.d.ts +25 -1
- package/dist/umbrella-current-shape/index.js +84 -4
- package/dist/user-config/index.d.ts +2 -0
- package/dist/user-config/index.js +2 -0
- package/dist/user-config/resolve-user-md.d.ts +76 -0
- package/dist/user-config/resolve-user-md.js +120 -0
- package/dist/vbrief-build/project-definition-io.d.ts +6 -0
- package/dist/vbrief-build/project-definition-io.js +7 -2
- package/dist/vbrief-validate/precutover.js +7 -3
- package/dist/xbrief-migrate/constants.d.ts +10 -0
- package/dist/xbrief-migrate/constants.js +19 -0
- package/dist/xbrief-migrate/detect.d.ts +34 -0
- package/dist/xbrief-migrate/detect.js +33 -0
- package/dist/xbrief-migrate/fs-helpers.d.ts +13 -0
- package/dist/xbrief-migrate/fs-helpers.js +46 -1
- package/dist/xbrief-migrate/index.d.ts +4 -2
- package/dist/xbrief-migrate/index.js +4 -2
- package/dist/xbrief-migrate/migrate-project.d.ts +23 -1
- package/dist/xbrief-migrate/migrate-project.js +97 -8
- package/dist/xbrief-migrate/signpost.d.ts +7 -1
- package/dist/xbrief-migrate/signpost.js +18 -3
- package/package.json +7 -3
|
@@ -212,6 +212,63 @@ export function writeInstallManifest(projectDir, deftDir, fields) {
|
|
|
212
212
|
writeFileSync(path, body, "utf8");
|
|
213
213
|
return path;
|
|
214
214
|
}
|
|
215
|
+
/** Canonical committed pin dependency name (mirrors resolution/pin.ts). */
|
|
216
|
+
export const PIN_DEPENDENCY_NAME = "@deftai/directive";
|
|
217
|
+
/**
|
|
218
|
+
* Write the committed `package.json` version pin on `@deftai/directive`
|
|
219
|
+
* (exact devDependency; `"private": true` preserved) so the resolution spine
|
|
220
|
+
* has a canonical, npm-native pin to read before directive runs. This unblocks
|
|
221
|
+
* #2269 (which is gated on the pin existing).
|
|
222
|
+
*
|
|
223
|
+
* Behavior:
|
|
224
|
+
* - existing `package.json` -> the exact devDependency pin is set/updated;
|
|
225
|
+
* an existing `"private"` value is preserved verbatim (never clobbered).
|
|
226
|
+
* - absent `package.json` -> a minimal `{ "private": true, ... }` is created.
|
|
227
|
+
* - idempotent -> re-running with the same pin makes no change.
|
|
228
|
+
*
|
|
229
|
+
* This is a deposit primitive; wiring it into the `init` verb flow is owned by
|
|
230
|
+
* #2265 (the init-consumes-plan child), per the #2264 scope guard.
|
|
231
|
+
*/
|
|
232
|
+
export function ensurePackageJsonPin(projectDir, version, io) {
|
|
233
|
+
const pinVersion = version.trim().replace(/^v/i, "");
|
|
234
|
+
const path = join(projectDir, "package.json");
|
|
235
|
+
const existed = existsSync(path);
|
|
236
|
+
let pkg = {};
|
|
237
|
+
if (existed) {
|
|
238
|
+
try {
|
|
239
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
240
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
241
|
+
pkg = parsed;
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
throw new Error("package.json root must be a JSON object");
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
catch (cause) {
|
|
248
|
+
throw new Error(`could not parse package.json (leaving it unchanged): ${String(cause)}`);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
else {
|
|
252
|
+
// Fresh scaffold: a consumer deposit is a non-published workspace.
|
|
253
|
+
pkg.private = true;
|
|
254
|
+
}
|
|
255
|
+
const devDeps = typeof pkg.devDependencies === "object" &&
|
|
256
|
+
pkg.devDependencies !== null &&
|
|
257
|
+
!Array.isArray(pkg.devDependencies)
|
|
258
|
+
? { ...pkg.devDependencies }
|
|
259
|
+
: {};
|
|
260
|
+
if (devDeps[PIN_DEPENDENCY_NAME] === pinVersion) {
|
|
261
|
+
io.printf(`package.json already pins ${PIN_DEPENDENCY_NAME}@${pinVersion} — skipping.\n`);
|
|
262
|
+
return { changed: false, pinVersion, created: false };
|
|
263
|
+
}
|
|
264
|
+
devDeps[PIN_DEPENDENCY_NAME] = pinVersion;
|
|
265
|
+
pkg.devDependencies = devDeps;
|
|
266
|
+
writeFileSync(path, `${JSON.stringify(pkg, null, 2)}\n`, "utf8");
|
|
267
|
+
io.printf(existed
|
|
268
|
+
? `package.json updated: pinned ${PIN_DEPENDENCY_NAME}@${pinVersion} (exact).\n`
|
|
269
|
+
: `package.json created: pinned ${PIN_DEPENDENCY_NAME}@${pinVersion} (exact, private).\n`);
|
|
270
|
+
return { changed: true, pinVersion, created: !existed };
|
|
271
|
+
}
|
|
215
272
|
export function writeAgentsMd(projectDir, deftDir, io) {
|
|
216
273
|
const plan = agentsRefreshPlan(projectDir, { frameworkRoot: deftDir });
|
|
217
274
|
const state = plan.state;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `migrate --untrack-core`: the one-time vendored→hybrid `.deft/core` un-commit
|
|
3
|
+
* (#2269). Owns the single destructive `git rm --cached -r .deft/core` index
|
|
4
|
+
* mutation in the framework — `init`/`update` only ever write the
|
|
5
|
+
* non-destructive `.gitignore` entry (boundary asserted by the colocated test).
|
|
6
|
+
*
|
|
7
|
+
* Safety contract:
|
|
8
|
+
* - NEVER touches working-tree content. `git rm --cached` removes files from
|
|
9
|
+
* the index only; the on-disk deposit is left intact.
|
|
10
|
+
* - Gated on a committed `package.json` pin existing (#2264 / Decision 2):
|
|
11
|
+
* once `.deft/core` is un-committed, content can be reconstituted from the
|
|
12
|
+
* pinned engine, so nothing is lost. With no pin, un-committing could leave
|
|
13
|
+
* the deposit unrecoverable, so we refuse.
|
|
14
|
+
* - Idempotent: once the deposit is un-tracked and ignored, re-running is a
|
|
15
|
+
* no-op that mutates nothing.
|
|
16
|
+
*
|
|
17
|
+
* The doctor detection that SURFACES this verb (the "payload tracked but should
|
|
18
|
+
* be ignored" signpost emitting `Next command: directive migrate --untrack-core`)
|
|
19
|
+
* is owned by child D (#2267); this module provides only the verb it points at.
|
|
20
|
+
*
|
|
21
|
+
* Refs #2269, #2264, #2123, #2124, #2203.
|
|
22
|
+
*/
|
|
23
|
+
import { type PinReadResult } from "../resolution/pin.js";
|
|
24
|
+
import type { InitDepositIo } from "./constants.js";
|
|
25
|
+
import { type GitLsFiles } from "./gitignore.js";
|
|
26
|
+
/** The deposit path un-committed from the git index (never from the working tree). */
|
|
27
|
+
export declare const UNTRACK_CORE_PATH = ".deft/core";
|
|
28
|
+
export type UntrackCoreOutcome = "untracked" | "already-clean" | "refused-missing-pin" | "git-error";
|
|
29
|
+
export interface UntrackCoreResult {
|
|
30
|
+
readonly outcome: UntrackCoreOutcome;
|
|
31
|
+
/** 0: untracked / already-clean · 1: refused (missing pin) · 2: git error. */
|
|
32
|
+
readonly exitCode: 0 | 1 | 2;
|
|
33
|
+
/** Whether `.deft/core` was tracked in git when the verb ran. */
|
|
34
|
+
readonly deftCoreTracked: boolean;
|
|
35
|
+
/** The committed exact pin version, or null when absent / non-exact. */
|
|
36
|
+
readonly pinVersion: string | null;
|
|
37
|
+
/** Whether the `.gitignore` reconciliation changed the file. */
|
|
38
|
+
readonly gitignoreChanged: boolean;
|
|
39
|
+
readonly message: string;
|
|
40
|
+
}
|
|
41
|
+
/** Outcome of the injected `git rm --cached -r <paths>` runner. */
|
|
42
|
+
export interface GitRmCachedResult {
|
|
43
|
+
readonly ok: boolean;
|
|
44
|
+
/** stdout on success, or the error detail on failure. */
|
|
45
|
+
readonly detail: string;
|
|
46
|
+
}
|
|
47
|
+
/** Injected git runner for the destructive index mutation (default shells out). */
|
|
48
|
+
export type GitRmCached = (projectDir: string, paths: readonly string[]) => GitRmCachedResult;
|
|
49
|
+
export interface UntrackCoreSeams {
|
|
50
|
+
/** Probe whether `.deft/core` is git-tracked (default: `git ls-files`). */
|
|
51
|
+
gitLsFiles?: GitLsFiles;
|
|
52
|
+
/** Run `git rm --cached -r <paths>` (default: shells out to git). */
|
|
53
|
+
gitRmCached?: GitRmCached;
|
|
54
|
+
/** Read the committed `package.json` pin (default: resolution/pin.ts readPin). */
|
|
55
|
+
readPin?: (projectRoot: string) => PinReadResult;
|
|
56
|
+
/** Reconcile `.gitignore` (default: ensureUntrackCoreGitignoreLines). */
|
|
57
|
+
ensureGitignore?: (projectDir: string, io: InitDepositIo) => {
|
|
58
|
+
changed: boolean;
|
|
59
|
+
deftCoreIgnored: boolean;
|
|
60
|
+
};
|
|
61
|
+
/** Output sink for reconciliation messages (default: no-op; the CLI prints the summary). */
|
|
62
|
+
io?: InitDepositIo;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Orchestrate the vendored→hybrid `.deft/core` un-commit for `projectRoot`.
|
|
66
|
+
* Pure of process exit; the CLI wrapper maps {@link UntrackCoreResult.exitCode}
|
|
67
|
+
* to a process code. Three primary outcomes (untracked / already-clean /
|
|
68
|
+
* refused-missing-pin) plus a git-error fallthrough.
|
|
69
|
+
*/
|
|
70
|
+
export declare function untrackCore(projectRoot: string, seams?: UntrackCoreSeams): UntrackCoreResult;
|
|
71
|
+
export interface RunUntrackCoreCliOptions {
|
|
72
|
+
readonly projectDir: string;
|
|
73
|
+
readonly jsonOut: boolean;
|
|
74
|
+
readonly writeOut: (text: string) => void;
|
|
75
|
+
readonly writeErr: (text: string) => void;
|
|
76
|
+
readonly seams?: UntrackCoreSeams;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* CLI-facing wrapper: runs the un-commit, emits JSON or human output, maps the
|
|
80
|
+
* outcome to a 0/1/2 exit code. Refusals and git errors print to stderr;
|
|
81
|
+
* success prints to stdout.
|
|
82
|
+
*/
|
|
83
|
+
export declare function runUntrackCoreCli(options: RunUntrackCoreCliOptions): number;
|
|
84
|
+
//# sourceMappingURL=untrack-core.d.ts.map
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `migrate --untrack-core`: the one-time vendored→hybrid `.deft/core` un-commit
|
|
3
|
+
* (#2269). Owns the single destructive `git rm --cached -r .deft/core` index
|
|
4
|
+
* mutation in the framework — `init`/`update` only ever write the
|
|
5
|
+
* non-destructive `.gitignore` entry (boundary asserted by the colocated test).
|
|
6
|
+
*
|
|
7
|
+
* Safety contract:
|
|
8
|
+
* - NEVER touches working-tree content. `git rm --cached` removes files from
|
|
9
|
+
* the index only; the on-disk deposit is left intact.
|
|
10
|
+
* - Gated on a committed `package.json` pin existing (#2264 / Decision 2):
|
|
11
|
+
* once `.deft/core` is un-committed, content can be reconstituted from the
|
|
12
|
+
* pinned engine, so nothing is lost. With no pin, un-committing could leave
|
|
13
|
+
* the deposit unrecoverable, so we refuse.
|
|
14
|
+
* - Idempotent: once the deposit is un-tracked and ignored, re-running is a
|
|
15
|
+
* no-op that mutates nothing.
|
|
16
|
+
*
|
|
17
|
+
* The doctor detection that SURFACES this verb (the "payload tracked but should
|
|
18
|
+
* be ignored" signpost emitting `Next command: directive migrate --untrack-core`)
|
|
19
|
+
* is owned by child D (#2267); this module provides only the verb it points at.
|
|
20
|
+
*
|
|
21
|
+
* Refs #2269, #2264, #2123, #2124, #2203.
|
|
22
|
+
*/
|
|
23
|
+
import { execFileSync } from "node:child_process";
|
|
24
|
+
import { PIN_DEPENDENCY_NAME, readPin } from "../resolution/pin.js";
|
|
25
|
+
import { ensureUntrackCoreGitignoreLines, isDepositTrackedInGit, } from "./gitignore.js";
|
|
26
|
+
/** The deposit path un-committed from the git index (never from the working tree). */
|
|
27
|
+
export const UNTRACK_CORE_PATH = ".deft/core";
|
|
28
|
+
/** Default `git rm --cached -r <paths>`: index-only removal, working tree preserved. */
|
|
29
|
+
function defaultGitRmCached(projectDir, paths) {
|
|
30
|
+
try {
|
|
31
|
+
const out = execFileSync("git", ["rm", "--cached", "-r", "--", ...paths], {
|
|
32
|
+
cwd: projectDir,
|
|
33
|
+
encoding: "utf8",
|
|
34
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
35
|
+
});
|
|
36
|
+
return { ok: true, detail: out };
|
|
37
|
+
}
|
|
38
|
+
catch (err) {
|
|
39
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
40
|
+
return { ok: false, detail };
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function refusedMissingPinMessage(pin) {
|
|
44
|
+
const reason = pin.nonExact
|
|
45
|
+
? `package.json pins ${PIN_DEPENDENCY_NAME} with a non-exact range spec (${pin.rawSpec ?? "?"})`
|
|
46
|
+
: `no committed exact package.json pin on ${PIN_DEPENDENCY_NAME} was found`;
|
|
47
|
+
return ("directive migrate --untrack-core: refusing to run git rm --cached on .deft/core because " +
|
|
48
|
+
`${reason}. Un-committing without a committed exact pin could leave the deposit content ` +
|
|
49
|
+
"unrecoverable (nothing to reconstitute from). Write and commit an exact pin " +
|
|
50
|
+
"(e.g. via `directive init` / `directive update`), then re-run.");
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Orchestrate the vendored→hybrid `.deft/core` un-commit for `projectRoot`.
|
|
54
|
+
* Pure of process exit; the CLI wrapper maps {@link UntrackCoreResult.exitCode}
|
|
55
|
+
* to a process code. Three primary outcomes (untracked / already-clean /
|
|
56
|
+
* refused-missing-pin) plus a git-error fallthrough.
|
|
57
|
+
*/
|
|
58
|
+
export function untrackCore(projectRoot, seams = {}) {
|
|
59
|
+
const gitRmCached = seams.gitRmCached ?? defaultGitRmCached;
|
|
60
|
+
const readPinFn = seams.readPin ?? readPin;
|
|
61
|
+
const ensureGitignore = seams.ensureGitignore ?? ensureUntrackCoreGitignoreLines;
|
|
62
|
+
const io = seams.io ?? { printf: () => { } };
|
|
63
|
+
const tracked = isDepositTrackedInGit(projectRoot, seams.gitLsFiles);
|
|
64
|
+
// Not tracked (or no git repo / git unavailable): nothing destructive to do.
|
|
65
|
+
// Reconcile the ignore entry so the layout is unambiguous, then report clean.
|
|
66
|
+
if (tracked !== true) {
|
|
67
|
+
const gi = ensureGitignore(projectRoot, io);
|
|
68
|
+
return {
|
|
69
|
+
outcome: "already-clean",
|
|
70
|
+
exitCode: 0,
|
|
71
|
+
deftCoreTracked: false,
|
|
72
|
+
pinVersion: readPinFn(projectRoot).pinVersion,
|
|
73
|
+
gitignoreChanged: gi.changed,
|
|
74
|
+
message: `directive migrate --untrack-core: .deft/core is not tracked in git — nothing to ` +
|
|
75
|
+
`un-commit. .gitignore ${gi.changed ? "reconciled (now ignores .deft/core/)" : "already ignores it"}.`,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
// Tracked: gate on the committed pin so content stays reconstitutable.
|
|
79
|
+
// Refuse on either an absent pin OR a non-exact range spec — a range cannot
|
|
80
|
+
// deterministically reconstitute the exact deposit, so it is not a safe gate.
|
|
81
|
+
// The `|| pin.nonExact` makes the invariant self-documenting and robust to any
|
|
82
|
+
// future `readPin` that resolves a version alongside `nonExact: true` (#2269).
|
|
83
|
+
const pin = readPinFn(projectRoot);
|
|
84
|
+
if (pin.pinVersion === null || pin.nonExact) {
|
|
85
|
+
return {
|
|
86
|
+
outcome: "refused-missing-pin",
|
|
87
|
+
exitCode: 1,
|
|
88
|
+
deftCoreTracked: true,
|
|
89
|
+
pinVersion: null,
|
|
90
|
+
gitignoreChanged: false,
|
|
91
|
+
message: refusedMissingPinMessage(pin),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
const rm = gitRmCached(projectRoot, [UNTRACK_CORE_PATH]);
|
|
95
|
+
if (!rm.ok) {
|
|
96
|
+
return {
|
|
97
|
+
outcome: "git-error",
|
|
98
|
+
exitCode: 2,
|
|
99
|
+
deftCoreTracked: true,
|
|
100
|
+
pinVersion: pin.pinVersion,
|
|
101
|
+
gitignoreChanged: false,
|
|
102
|
+
message: `directive migrate --untrack-core: git rm --cached failed: ${rm.detail.trim()}`,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
const gi = ensureGitignore(projectRoot, io);
|
|
106
|
+
return {
|
|
107
|
+
outcome: "untracked",
|
|
108
|
+
exitCode: 0,
|
|
109
|
+
deftCoreTracked: true,
|
|
110
|
+
pinVersion: pin.pinVersion,
|
|
111
|
+
gitignoreChanged: gi.changed,
|
|
112
|
+
message: `directive migrate --untrack-core: removed ${UNTRACK_CORE_PATH} from the git index ` +
|
|
113
|
+
`(working tree untouched); pin ${pin.pinVersion} lets \`directive update\` reconstitute ` +
|
|
114
|
+
`content. .gitignore ${gi.changed ? "reconciled (now ignores .deft/core/)" : "already ignores it"}. ` +
|
|
115
|
+
"Commit the removal to complete the un-commit.",
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
function buildUntrackCoreSummaryJson(result, projectDir) {
|
|
119
|
+
return {
|
|
120
|
+
success: result.exitCode === 0,
|
|
121
|
+
action: "migrate-untrack-core",
|
|
122
|
+
outcome: result.outcome,
|
|
123
|
+
exit_code: result.exitCode,
|
|
124
|
+
project_dir: projectDir,
|
|
125
|
+
deft_core_tracked: result.deftCoreTracked,
|
|
126
|
+
pin_version: result.pinVersion,
|
|
127
|
+
gitignore_changed: result.gitignoreChanged,
|
|
128
|
+
message: result.message,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* CLI-facing wrapper: runs the un-commit, emits JSON or human output, maps the
|
|
133
|
+
* outcome to a 0/1/2 exit code. Refusals and git errors print to stderr;
|
|
134
|
+
* success prints to stdout.
|
|
135
|
+
*/
|
|
136
|
+
export function runUntrackCoreCli(options) {
|
|
137
|
+
const result = untrackCore(options.projectDir, { ...options.seams, io: { printf: () => { } } });
|
|
138
|
+
if (options.jsonOut) {
|
|
139
|
+
options.writeOut(`${JSON.stringify(buildUntrackCoreSummaryJson(result, options.projectDir), null, 2)}\n`);
|
|
140
|
+
return result.exitCode;
|
|
141
|
+
}
|
|
142
|
+
if (result.exitCode === 0) {
|
|
143
|
+
options.writeOut(`${result.message}\n`);
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
options.writeErr(`${result.message}\n`);
|
|
147
|
+
}
|
|
148
|
+
return result.exitCode;
|
|
149
|
+
}
|
|
150
|
+
//# sourceMappingURL=untrack-core.js.map
|
|
@@ -1,7 +1,18 @@
|
|
|
1
|
+
import { type ScanFlag } from "../cache/scanner.js";
|
|
1
2
|
import { LEGACY_ARTIFACT_SUFFIX, LEGACY_INFO_ROOT_KEY, MIGRATED_ARTIFACT_SUFFIX, MIGRATED_INFO_ROOT_KEY } from "../xbrief-migrate/constants.js";
|
|
2
3
|
import { type ScmCallFn } from "./reconcile-issues.js";
|
|
3
4
|
export declare const INGEST_STATUSES: readonly ["proposed", "pending", "active"];
|
|
4
5
|
export type IngestStatus = (typeof INGEST_STATUSES)[number];
|
|
6
|
+
/**
|
|
7
|
+
* Thrown when the quarantine scanner hard-fails (credential-shaped content) on
|
|
8
|
+
* an ingested issue body/comment thread (#2306). Ingest MUST fail closed: emit
|
|
9
|
+
* nothing and propagate a non-zero exit rather than persisting the xBRIEF.
|
|
10
|
+
*/
|
|
11
|
+
export declare class ScannerHardFailError extends Error {
|
|
12
|
+
readonly issueNumber: number;
|
|
13
|
+
readonly flags: readonly ScanFlag[];
|
|
14
|
+
constructor(issueNumber: number, flags: readonly ScanFlag[]);
|
|
15
|
+
}
|
|
5
16
|
/** GitHub issue comment thread entry (REST `repos/.../issues/N/comments`). */
|
|
6
17
|
export interface IssueComment {
|
|
7
18
|
readonly id?: number;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { basename, join, resolve } from "node:path";
|
|
3
3
|
import { cacheGet } from "../cache/operations.js";
|
|
4
|
+
import { scan } from "../cache/scanner.js";
|
|
4
5
|
import { hasArtifactSuffix, resolveLifecycleRoot } from "../layout/resolve.js";
|
|
5
6
|
import { call } from "../scm/call.js";
|
|
6
7
|
import { resolveProjectRoot } from "../scope/project-context.js";
|
|
@@ -11,6 +12,27 @@ import { LEGACY_ARTIFACT_SUFFIX, LEGACY_INFO_ROOT_KEY, LEGACY_VBRIEF_VERSION, MI
|
|
|
11
12
|
import { findAcHeading, parseCheckboxItems, parseListItems, sliceAcSection, stripCodeBlocks, stripFencedCodeBlocks, } from "./markdown-scanners.js";
|
|
12
13
|
import { detectRepo, extractReferencesFromVbrief, fetchOpenIssues, GITHUB_ISSUE_REF_TYPES, LIFECYCLE_FOLDERS, parseIssueNumber, } from "./reconcile-issues.js";
|
|
13
14
|
export const INGEST_STATUSES = ["proposed", "pending", "active"];
|
|
15
|
+
/**
|
|
16
|
+
* Thrown when the quarantine scanner hard-fails (credential-shaped content) on
|
|
17
|
+
* an ingested issue body/comment thread (#2306). Ingest MUST fail closed: emit
|
|
18
|
+
* nothing and propagate a non-zero exit rather than persisting the xBRIEF.
|
|
19
|
+
*/
|
|
20
|
+
export class ScannerHardFailError extends Error {
|
|
21
|
+
issueNumber;
|
|
22
|
+
flags;
|
|
23
|
+
constructor(issueNumber, flags) {
|
|
24
|
+
const details = flags
|
|
25
|
+
.filter((f) => f.severity === "hard-fail")
|
|
26
|
+
.map((f) => f.detail)
|
|
27
|
+
.join("; ");
|
|
28
|
+
super(`issue:ingest refused #${issueNumber}: quarantine scanner hard-fail` +
|
|
29
|
+
(details.length > 0 ? ` (${details})` : "") +
|
|
30
|
+
" -- nothing written.");
|
|
31
|
+
this.name = "ScannerHardFailError";
|
|
32
|
+
this.issueNumber = issueNumber;
|
|
33
|
+
this.flags = flags;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
14
36
|
/** Enriched on issues after `fetchIssue` when the comment thread is non-empty (#2143). */
|
|
15
37
|
export const ISSUE_COMMENT_THREAD_KEY = "issueCommentThread";
|
|
16
38
|
const STATUS_MAP = {
|
|
@@ -311,7 +333,14 @@ export function buildIssueVbrief(issue, status, repoUrl, options = {}) {
|
|
|
311
333
|
};
|
|
312
334
|
if (overviewSource.length > 0) {
|
|
313
335
|
warnBodyControlCharacters(number, overviewSource);
|
|
314
|
-
|
|
336
|
+
// #2306: quarantine-scan untrusted body + comment-thread content before it
|
|
337
|
+
// is persisted as agent-facing scope authority. Fail closed on a credential
|
|
338
|
+
// hard-fail; otherwise persist the fenced/quarantined transform.
|
|
339
|
+
const scanResult = scan(overviewSource);
|
|
340
|
+
if (!scanResult.passed) {
|
|
341
|
+
throw new ScannerHardFailError(number, scanResult.flags);
|
|
342
|
+
}
|
|
343
|
+
narratives.Overview = scanResult.transformed_content;
|
|
315
344
|
}
|
|
316
345
|
if (labelNames.length > 0) {
|
|
317
346
|
narratives.Labels = labelNames.join(", ");
|
|
@@ -369,6 +398,19 @@ export function fetchFromCache(repo, number, options = {}) {
|
|
|
369
398
|
if (typeof issue.html_url === "string" && issue.html_url.length > 0) {
|
|
370
399
|
issue.url = issue.html_url;
|
|
371
400
|
}
|
|
401
|
+
// #2306: consume the cache entry's SCANNED content.md (fenced/quarantined at
|
|
402
|
+
// cache-put) rather than the raw body, so the cache read path cannot bypass
|
|
403
|
+
// the quarantine transform. When content.md is absent (e.g. a credential
|
|
404
|
+
// hard-fail deleted it), the raw body falls through and is re-scanned in
|
|
405
|
+
// buildIssueVbrief.
|
|
406
|
+
if (result.contentPath !== null) {
|
|
407
|
+
try {
|
|
408
|
+
issue.body = readFileSync(result.contentPath, "utf8");
|
|
409
|
+
}
|
|
410
|
+
catch {
|
|
411
|
+
// fall back to the raw body (re-scanned downstream)
|
|
412
|
+
}
|
|
413
|
+
}
|
|
372
414
|
return issue;
|
|
373
415
|
}
|
|
374
416
|
catch {
|
|
@@ -514,9 +556,25 @@ export function ingestBulk(issues, options) {
|
|
|
514
556
|
created: [],
|
|
515
557
|
duplicate: [],
|
|
516
558
|
dryrun: [],
|
|
559
|
+
failed: [],
|
|
517
560
|
};
|
|
518
561
|
for (const issue of filtered) {
|
|
519
|
-
|
|
562
|
+
let ingested;
|
|
563
|
+
try {
|
|
564
|
+
ingested = ingestOne(issue, { ...options, existingRefs: refs });
|
|
565
|
+
}
|
|
566
|
+
catch (exc) {
|
|
567
|
+
// #2306: a per-issue quarantine hard-fail must not sink the whole batch;
|
|
568
|
+
// record it, emit nothing for that issue, and surface a non-zero exit
|
|
569
|
+
// upstream via the `failed` bucket.
|
|
570
|
+
if (exc instanceof ScannerHardFailError) {
|
|
571
|
+
summary.failed.push(`#${exc.issueNumber}`);
|
|
572
|
+
process.stderr.write(`${exc.message}\n`);
|
|
573
|
+
continue;
|
|
574
|
+
}
|
|
575
|
+
throw exc;
|
|
576
|
+
}
|
|
577
|
+
const [result, path, _msg] = ingested;
|
|
520
578
|
const rel = path !== null ? path.replace(`${options.vbriefDir}/`, "").replace(/\\/g, "/") : "";
|
|
521
579
|
summary[result].push(rel);
|
|
522
580
|
if (result === "created" && path !== null) {
|
|
@@ -581,7 +639,8 @@ export function issueIngestMain(args) {
|
|
|
581
639
|
const created = summary.created;
|
|
582
640
|
const duplicate = summary.duplicate;
|
|
583
641
|
const dryrun = summary.dryrun;
|
|
584
|
-
|
|
642
|
+
const failed = summary.failed ?? [];
|
|
643
|
+
process.stdout.write(`issue:ingest bulk summary: ${created.length} created, ${duplicate.length} duplicate, ${dryrun.length} dry-run, ${failed.length} refused (total considered: ${summary.total})\n`);
|
|
585
644
|
for (const entry of created) {
|
|
586
645
|
process.stdout.write(` CREATED ${entry}\n`);
|
|
587
646
|
}
|
|
@@ -591,19 +650,35 @@ export function issueIngestMain(args) {
|
|
|
591
650
|
for (const entry of duplicate) {
|
|
592
651
|
process.stdout.write(` SKIP ${entry} (already has scope vBRIEF)\n`);
|
|
593
652
|
}
|
|
594
|
-
|
|
653
|
+
for (const entry of failed) {
|
|
654
|
+
process.stdout.write(` REFUSED ${entry} (quarantine scanner hard-fail; nothing written)\n`);
|
|
655
|
+
}
|
|
656
|
+
// #2306: fail closed on any quarantine hard-fail in the batch.
|
|
657
|
+
return failed.length > 0 ? 2 : 0;
|
|
595
658
|
}
|
|
596
659
|
const issue = fetchIssue(repo, args.number, { cwd: projectRoot });
|
|
597
660
|
if (issue === null) {
|
|
598
661
|
return 2;
|
|
599
662
|
}
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
663
|
+
let result;
|
|
664
|
+
let msg;
|
|
665
|
+
try {
|
|
666
|
+
[result, , msg] = ingestOne(issue, {
|
|
667
|
+
vbriefDir,
|
|
668
|
+
status,
|
|
669
|
+
repoUrl,
|
|
670
|
+
dryRun: args.dryRun,
|
|
671
|
+
cwd: projectRoot,
|
|
672
|
+
});
|
|
673
|
+
}
|
|
674
|
+
catch (exc) {
|
|
675
|
+
// #2306: fail closed -- emit nothing, non-zero exit on a quarantine hard-fail.
|
|
676
|
+
if (exc instanceof ScannerHardFailError) {
|
|
677
|
+
process.stderr.write(`${exc.message}\n`);
|
|
678
|
+
return 2;
|
|
679
|
+
}
|
|
680
|
+
throw exc;
|
|
681
|
+
}
|
|
607
682
|
process.stdout.write(`${msg}\n`);
|
|
608
683
|
return result === "duplicate" ? 1 : 0;
|
|
609
684
|
}
|
|
@@ -32,4 +32,35 @@ export declare function readPlanCompletedNote(plan: unknown): unknown;
|
|
|
32
32
|
* bare block alongside the namespaced one.
|
|
33
33
|
*/
|
|
34
34
|
export declare function migrateLegacyPolicyKey(plan: Record<string, unknown>): void;
|
|
35
|
+
/** The namespaced/legacy key pairs a bare block can silently shadow (#2301). */
|
|
36
|
+
export declare const SHADOWABLE_PLAN_EXTENSIONS: ReadonlyArray<{
|
|
37
|
+
readonly namespacedKey: string;
|
|
38
|
+
readonly legacyKey: string;
|
|
39
|
+
}>;
|
|
40
|
+
/** A plan-extension key whose bare form is silently shadowed by the namespaced form. */
|
|
41
|
+
export interface ShadowedPlanExtension {
|
|
42
|
+
/** The namespaced key that wins the read (e.g. `x-directive/policy`). */
|
|
43
|
+
readonly namespacedKey: string;
|
|
44
|
+
/** The bare legacy key that is silently ignored (e.g. `policy`). */
|
|
45
|
+
readonly legacyKey: string;
|
|
46
|
+
/**
|
|
47
|
+
* Best-effort list of sub-keys present in the shadowed bare object (e.g.
|
|
48
|
+
* `["triageScope", "wipCap"]`). Empty when the bare value is not an object.
|
|
49
|
+
*/
|
|
50
|
+
readonly shadowedSubKeys: readonly string[];
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Detect every plan-extension key where a bare (legacy) block coexists with the
|
|
54
|
+
* namespaced form (#2301). Because `readPlanExtension` is namespace-first, the
|
|
55
|
+
* bare block is never read once the namespaced key exists -- edits to it take no
|
|
56
|
+
* effect. Detecting the coexistence lets callers emit a loud diagnostic instead
|
|
57
|
+
* of the silent no-op that the #2295 onboarding trap exhibited.
|
|
58
|
+
*/
|
|
59
|
+
export declare function detectShadowedPlanExtensions(plan: unknown): ShadowedPlanExtension[];
|
|
60
|
+
/**
|
|
61
|
+
* Render a human-readable, loud diagnostic for a single shadowed plan-extension
|
|
62
|
+
* key. The message is surface-agnostic (no leading tag) so each caller can
|
|
63
|
+
* prefix it (`[policy:show]`, a doctor finding, ...).
|
|
64
|
+
*/
|
|
65
|
+
export declare function describeShadowedPlanExtension(shadow: ShadowedPlanExtension): string;
|
|
35
66
|
//# sourceMappingURL=plan-extensions.d.ts.map
|
|
@@ -54,4 +54,49 @@ export function migrateLegacyPolicyKey(plan) {
|
|
|
54
54
|
delete plan[LEGACY_PLAN_POLICY_KEY];
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
|
+
/** The namespaced/legacy key pairs a bare block can silently shadow (#2301). */
|
|
58
|
+
export const SHADOWABLE_PLAN_EXTENSIONS = [
|
|
59
|
+
{ namespacedKey: PLAN_POLICY_KEY, legacyKey: LEGACY_PLAN_POLICY_KEY },
|
|
60
|
+
{ namespacedKey: PLAN_COMPLETED_NOTE_KEY, legacyKey: LEGACY_PLAN_COMPLETED_NOTE_KEY },
|
|
61
|
+
];
|
|
62
|
+
/**
|
|
63
|
+
* Detect every plan-extension key where a bare (legacy) block coexists with the
|
|
64
|
+
* namespaced form (#2301). Because `readPlanExtension` is namespace-first, the
|
|
65
|
+
* bare block is never read once the namespaced key exists -- edits to it take no
|
|
66
|
+
* effect. Detecting the coexistence lets callers emit a loud diagnostic instead
|
|
67
|
+
* of the silent no-op that the #2295 onboarding trap exhibited.
|
|
68
|
+
*/
|
|
69
|
+
export function detectShadowedPlanExtensions(plan) {
|
|
70
|
+
const planObj = asPlanObject(plan);
|
|
71
|
+
if (planObj === null) {
|
|
72
|
+
return [];
|
|
73
|
+
}
|
|
74
|
+
const shadows = [];
|
|
75
|
+
for (const { namespacedKey, legacyKey } of SHADOWABLE_PLAN_EXTENSIONS) {
|
|
76
|
+
if (planObj[namespacedKey] === undefined || planObj[legacyKey] === undefined) {
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
const legacyValue = planObj[legacyKey];
|
|
80
|
+
const shadowedSubKeys = typeof legacyValue === "object" && legacyValue !== null && !Array.isArray(legacyValue)
|
|
81
|
+
? Object.keys(legacyValue)
|
|
82
|
+
: [];
|
|
83
|
+
shadows.push({ namespacedKey, legacyKey, shadowedSubKeys });
|
|
84
|
+
}
|
|
85
|
+
return shadows;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Render a human-readable, loud diagnostic for a single shadowed plan-extension
|
|
89
|
+
* key. The message is surface-agnostic (no leading tag) so each caller can
|
|
90
|
+
* prefix it (`[policy:show]`, a doctor finding, ...).
|
|
91
|
+
*/
|
|
92
|
+
export function describeShadowedPlanExtension(shadow) {
|
|
93
|
+
const subKeys = shadow.shadowedSubKeys.length > 0
|
|
94
|
+
? ` Shadowed field(s): ${shadow.shadowedSubKeys
|
|
95
|
+
.map((k) => `plan.${shadow.legacyKey}.${k}`)
|
|
96
|
+
.join(", ")}.`
|
|
97
|
+
: "";
|
|
98
|
+
return (`bare \`plan.${shadow.legacyKey}\` coexists with namespaced \`plan.${shadow.namespacedKey}\`; ` +
|
|
99
|
+
`the bare block is IGNORED (namespaced-first read) so edits to it silently take no effect.${subKeys} ` +
|
|
100
|
+
`Fold its values into \`plan.${shadow.namespacedKey}\` and delete \`plan.${shadow.legacyKey}\`.`);
|
|
101
|
+
}
|
|
57
102
|
//# sourceMappingURL=plan-extensions.js.map
|
package/dist/policy/wip.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
/** Framework default WIP cap (#1124 / umbrella #1119). */
|
|
2
|
-
export declare const DEFAULT_WIP_CAP =
|
|
1
|
+
/** Framework default WIP cap (#2319; raised from 10 per #1124 / umbrella #1119). */
|
|
2
|
+
export declare const DEFAULT_WIP_CAP = 20;
|
|
3
3
|
/** vBRIEF lifecycle folders that count toward the WIP set. */
|
|
4
4
|
export declare const WIP_LIFECYCLE_DIRS: readonly ["pending", "active"];
|
|
5
5
|
export type WipCapSource = "typed" | "default" | "default-on-error";
|
package/dist/policy/wip.js
CHANGED
|
@@ -3,8 +3,8 @@ import { join } from "node:path";
|
|
|
3
3
|
import { hasArtifactSuffix, resolveLifecycleRoot } from "../layout/resolve.js";
|
|
4
4
|
import { readPlanPolicy } from "./plan-extensions.js";
|
|
5
5
|
import { loadProjectDefinition } from "./resolve.js";
|
|
6
|
-
/** Framework default WIP cap (#1124 / umbrella #1119). */
|
|
7
|
-
export const DEFAULT_WIP_CAP =
|
|
6
|
+
/** Framework default WIP cap (#2319; raised from 10 per #1124 / umbrella #1119). */
|
|
7
|
+
export const DEFAULT_WIP_CAP = 20;
|
|
8
8
|
/** vBRIEF lifecycle folders that count toward the WIP set. */
|
|
9
9
|
export const WIP_LIFECYCLE_DIRS = ["pending", "active"];
|
|
10
10
|
function pythonTypeName(value) {
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Orthogonal fact-set classifier for the resolution spine (#2264 / epic #2203).
|
|
3
|
+
*
|
|
4
|
+
* `classify()` returns a flat, independent fact-set — never a single collapsed
|
|
5
|
+
* enum. `plan()` owns the collapse into one recommended action, so there is
|
|
6
|
+
* exactly one precedence table in the system.
|
|
7
|
+
*
|
|
8
|
+
* It REUSES the existing detectors rather than re-implementing them:
|
|
9
|
+
* - pre-cutover artifacts -> `../vbrief-validate/precutover.ts`
|
|
10
|
+
* - managed-section sha -> `../platform/agents-md.ts::parseManagedSectionAttrs`
|
|
11
|
+
* - deposited payload version-> `../doctor/manifest.ts` + `../init-deposit/constants.ts`
|
|
12
|
+
* - committed pin -> `./pin.ts`
|
|
13
|
+
*/
|
|
14
|
+
import type { ResolutionFacts } from "@deftai/directive-types";
|
|
15
|
+
/** Result of probing whether an engine is reachable in the execution environment. */
|
|
16
|
+
export interface EngineProbeResult {
|
|
17
|
+
readonly reachable: boolean;
|
|
18
|
+
readonly version: string | null;
|
|
19
|
+
}
|
|
20
|
+
export interface ClassifySeams {
|
|
21
|
+
readonly isFile?: (p: string) => boolean;
|
|
22
|
+
readonly isDir?: (p: string) => boolean;
|
|
23
|
+
readonly readText?: (p: string) => string | null;
|
|
24
|
+
/**
|
|
25
|
+
* Probe for a reachable engine IN THE ENVIRONMENT THE CALLER IS RUNNING IN.
|
|
26
|
+
* Injected so classification is deterministic + offline in tests; the default
|
|
27
|
+
* shells out to the `directive` / `deft` CLI (the #2124 execution-env probe).
|
|
28
|
+
*/
|
|
29
|
+
readonly engineProbe?: () => EngineProbeResult;
|
|
30
|
+
/** Pre-cutover probe; defaults to the shared `detectPreCutover` detector. */
|
|
31
|
+
readonly preCutoverProbe?: (cwd: string) => boolean;
|
|
32
|
+
}
|
|
33
|
+
/** Default engine probe: try `directive --version`, then `deft --version`. */
|
|
34
|
+
export declare function defaultEngineProbe(): EngineProbeResult;
|
|
35
|
+
/**
|
|
36
|
+
* Classify a project directory into the orthogonal resolution fact-set. All I/O
|
|
37
|
+
* is behind injectable seams so callers (tests, sandboxed runtimes) can supply a
|
|
38
|
+
* deterministic view of the filesystem and the engine-reachability probe.
|
|
39
|
+
*/
|
|
40
|
+
export declare function classify(cwd: string, seams?: ClassifySeams): ResolutionFacts;
|
|
41
|
+
//# sourceMappingURL=classify.d.ts.map
|