@deftai/directive-core 0.90.0 → 0.92.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cache/fetch.js +3 -2
- package/dist/cache/io.d.ts +13 -2
- package/dist/cache/io.js +36 -7
- package/dist/cache/operations.js +5 -3
- package/dist/doctor/main.js +37 -0
- package/dist/doctor/openclaw-skills.d.ts +42 -8
- package/dist/doctor/openclaw-skills.js +281 -38
- package/dist/eval/health.d.ts +10 -1
- package/dist/eval/health.js +16 -16
- package/dist/hooks/dispatcher.d.ts +6 -1
- package/dist/hooks/dispatcher.js +30 -0
- package/dist/init-deposit/gitignore.js +3 -0
- package/dist/init-deposit/hygiene.d.ts +43 -1
- package/dist/init-deposit/hygiene.js +112 -7
- package/dist/init-deposit/init-deposit.d.ts +5 -0
- package/dist/init-deposit/init-deposit.js +37 -2
- package/dist/init-deposit/scaffold.js +3 -1
- package/dist/intake/github-auth-modes.d.ts +1 -1
- package/dist/intake/github-auth-modes.js +25 -3
- package/dist/intake/issue-ingest-cli.js +13 -0
- package/dist/intake/reconcile-issues-cli.js +13 -0
- package/dist/orchestration/probe-session.d.ts +5 -1
- package/dist/orchestration/probe-session.js +24 -13
- package/dist/platform/agents-md.js +1 -1
- package/dist/policy/deft-directive-disable.d.ts +86 -0
- package/dist/policy/deft-directive-disable.js +167 -0
- package/dist/policy/delivery-branch.d.ts +33 -0
- package/dist/policy/delivery-branch.js +124 -0
- package/dist/policy/index.d.ts +2 -0
- package/dist/policy/index.js +17 -1
- package/dist/policy/plan-extensions.d.ts +10 -0
- package/dist/policy/plan-extensions.js +16 -0
- package/dist/pr-closing-keywords/detect.d.ts +13 -0
- package/dist/pr-closing-keywords/detect.js +73 -8
- package/dist/pr-closing-keywords/index.d.ts +2 -2
- package/dist/pr-closing-keywords/index.js +1 -1
- package/dist/pr-closing-keywords/main.js +119 -90
- package/dist/pr-closing-keywords/types.d.ts +10 -0
- package/dist/render/constants.d.ts +16 -0
- package/dist/render/constants.js +16 -1
- package/dist/render/export-spec.d.ts +7 -1
- package/dist/render/export-spec.js +51 -5
- package/dist/render/index.d.ts +3 -2
- package/dist/render/index.js +2 -2
- package/dist/render/roadmap-render.js +164 -60
- package/dist/render/scope-outlook.d.ts +5 -0
- package/dist/render/scope-outlook.js +3 -0
- package/dist/render/spec-render.d.ts +28 -2
- package/dist/render/spec-render.js +111 -14
- package/dist/render/text-utils.d.ts +6 -0
- package/dist/render/text-utils.js +23 -0
- package/dist/scm/binary.d.ts +3 -0
- package/dist/scm/binary.js +10 -2
- package/dist/scm/call.d.ts +5 -0
- package/dist/scm/call.js +5 -0
- package/dist/scm/index.d.ts +1 -0
- package/dist/scm/index.js +1 -0
- package/dist/scm/main.d.ts +5 -0
- package/dist/scm/main.js +27 -0
- package/dist/scm/readiness-cli.d.ts +24 -0
- package/dist/scm/readiness-cli.js +85 -0
- package/dist/scm/readiness.d.ts +104 -0
- package/dist/scm/readiness.js +421 -0
- package/dist/scope/batch-promote.d.ts +26 -0
- package/dist/scope/batch-promote.js +167 -0
- package/dist/scope/brief-io.d.ts +3 -1
- package/dist/scope/brief-io.js +5 -3
- package/dist/scope/delivery-evidence.d.ts +112 -0
- package/dist/scope/delivery-evidence.js +419 -0
- package/dist/scope/index.d.ts +2 -0
- package/dist/scope/index.js +2 -0
- package/dist/scope/main.d.ts +7 -0
- package/dist/scope/main.js +155 -9
- package/dist/scope/registry-artifact-sync.js +4 -1
- package/dist/scope/transition.d.ts +11 -1
- package/dist/scope/transition.js +30 -4
- package/dist/scope/wip-cap-check.d.ts +6 -0
- package/dist/scope/wip-cap-check.js +14 -2
- package/dist/session/ritual-sentinel.js +21 -12
- package/dist/session/session-start-hook.d.ts +3 -0
- package/dist/session/session-start-hook.js +21 -0
- package/dist/session/session-start.d.ts +7 -0
- package/dist/session/session-start.js +85 -0
- package/dist/swarm/complete-cohort.d.ts +20 -1
- package/dist/swarm/complete-cohort.js +56 -9
- package/dist/swarm/finalize-cohort-cli.js +9 -0
- package/dist/swarm/finalize-cohort.d.ts +8 -0
- package/dist/swarm/finalize-cohort.js +217 -21
- package/dist/triage/welcome/index.d.ts +1 -1
- package/dist/triage/welcome/index.js +1 -1
- package/dist/triage/welcome/onboard.js +7 -1
- package/dist/triage/welcome/prior-state.d.ts +7 -0
- package/dist/triage/welcome/prior-state.js +16 -2
- package/dist/triage/welcome/writers.d.ts +10 -0
- package/dist/triage/welcome/writers.js +72 -2
- package/dist/user-config/experimental-rules.d.ts +43 -0
- package/dist/user-config/experimental-rules.js +162 -0
- package/dist/user-config/index.d.ts +1 -0
- package/dist/user-config/index.js +1 -0
- package/dist/verify-source/contained-writes.js +1 -1
- package/package.json +3 -3
|
@@ -2,7 +2,13 @@
|
|
|
2
2
|
* Scoped staging + installer-managed allowlist for TS-native init/update (#1453).
|
|
3
3
|
*
|
|
4
4
|
* Mirrors cmd/deft-install/hygiene.go + deposit.go installerManagedMatchers.
|
|
5
|
-
*
|
|
5
|
+
*
|
|
6
|
+
* CRITICAL (#1430 / #3030): the allowlist MUST honor the SPEC consumer-path
|
|
7
|
+
* denylist (`CONSUMER_GUARD_MUST_FIRE`). Consumer-authored PROJECT-DEFINITION
|
|
8
|
+
* and scope briefs are never installer-managed; if they reappear in
|
|
9
|
+
* `installerManagedMatchers()`, unit tests and deposit-time assert fail closed.
|
|
10
|
+
*
|
|
11
|
+
* Refs #1576, #1453, #1430, #3029, #3030.
|
|
6
12
|
*/
|
|
7
13
|
import { execFileSync } from "node:child_process";
|
|
8
14
|
import { existsSync, readdirSync, rmSync } from "node:fs";
|
|
@@ -15,6 +21,26 @@ export const CORE_GUARD_WORKFLOW_REL = ".github/workflows/deft-core-guard.yml";
|
|
|
15
21
|
// The lifecycle dir names are identical across the legacy `vbrief/` tree and the
|
|
16
22
|
// post-#2034 / #2110 `xbrief/` tree, so both allowlist families reuse this list.
|
|
17
23
|
const VBRIEF_LIFECYCLE_DIRS = ["proposed", "pending", "active", "completed", "cancelled"];
|
|
24
|
+
/**
|
|
25
|
+
* Consumer paths that MUST trip no-mixed-core-and-app when mixed with
|
|
26
|
+
* `.deft/core/**` (#1430 SPEC). These probe paths must never match
|
|
27
|
+
* `installerManagedMatchers()` / the deposited guard ERE.
|
|
28
|
+
*
|
|
29
|
+
* Legitimate installer scaffolding (xbrief/.deft-version, lifecycle .gitkeep,
|
|
30
|
+
* schemas/, migration/, xbrief.md) is NOT in this denylist — see #2277.
|
|
31
|
+
* Init may still *create* PROJECT-DEFINITION (#3013); create ≠ allowlist.
|
|
32
|
+
*
|
|
33
|
+
* Refs #3030, #3029, #1430.
|
|
34
|
+
*/
|
|
35
|
+
export const CONSUMER_GUARD_MUST_FIRE = [
|
|
36
|
+
"xbrief/PROJECT-DEFINITION.xbrief.json",
|
|
37
|
+
"vbrief/PROJECT-DEFINITION.vbrief.json",
|
|
38
|
+
// Representative consumer scope briefs (not scaffolding markers).
|
|
39
|
+
"xbrief/active/example-scope.xbrief.json",
|
|
40
|
+
"vbrief/active/example-scope.vbrief.json",
|
|
41
|
+
"xbrief/proposed/example-scope.xbrief.json",
|
|
42
|
+
"vbrief/pending/example-scope.vbrief.json",
|
|
43
|
+
];
|
|
18
44
|
/** Single source of truth for installer-managed paths (#1440 / #1576). */
|
|
19
45
|
export function installerManagedMatchers() {
|
|
20
46
|
return [
|
|
@@ -45,6 +71,10 @@ export function installerManagedMatchers() {
|
|
|
45
71
|
// `deft update` framework-deposit PR trips no-mixed-core-and-app (#2277).
|
|
46
72
|
{ exact: "xbrief/.deft-version" },
|
|
47
73
|
{ exact: "xbrief/xbrief.md" },
|
|
74
|
+
// CRITICAL (#1430 / #3029 / #3030): do NOT allowlist consumer-authored
|
|
75
|
+
// PROJECT-DEFINITION (xbrief/ or vbrief/) or consumer scope briefs.
|
|
76
|
+
// Init may still seed PD (#3013); the seed is app-owned for guard classification
|
|
77
|
+
// so core+PD mixed PRs fail no-mixed-core-and-app. See CONSUMER_GUARD_MUST_FIRE.
|
|
48
78
|
{ prefix: "xbrief/schemas/" },
|
|
49
79
|
{ prefix: "xbrief/migration/" },
|
|
50
80
|
...VBRIEF_LIFECYCLE_DIRS.map((sub) => ({ exact: `xbrief/${sub}/.gitkeep` })),
|
|
@@ -58,12 +88,6 @@ function matcherToEre(matcher) {
|
|
|
58
88
|
return `^${escapeEre(matcher.exact)}$`;
|
|
59
89
|
return `^${escapeEre(matcher.prefix ?? "")}`;
|
|
60
90
|
}
|
|
61
|
-
/** POSIX ERE alternation embedded in the deposited deft-core-guard workflow. */
|
|
62
|
-
export function installerManagedGuardEre() {
|
|
63
|
-
return installerManagedMatchers()
|
|
64
|
-
.map((matcher) => matcherToEre(matcher))
|
|
65
|
-
.join("|");
|
|
66
|
-
}
|
|
67
91
|
function matchesInstallerManaged(path, matchers) {
|
|
68
92
|
for (const matcher of matchers) {
|
|
69
93
|
if (matcher.exact && path === matcher.exact)
|
|
@@ -73,9 +97,90 @@ function matchesInstallerManaged(path, matchers) {
|
|
|
73
97
|
}
|
|
74
98
|
return false;
|
|
75
99
|
}
|
|
100
|
+
/** Consumer scope-brief filenames under lifecycle dirs (not scaffolding). */
|
|
101
|
+
const CONSUMER_SCOPE_BRIEF_EXACT = /^(xbrief|vbrief)\/(proposed|pending|active|completed|cancelled)\/.+\.(x|v)brief\.json$/;
|
|
102
|
+
/** PROJECT-DEFINITION exact paths (xbrief or legacy vbrief). */
|
|
103
|
+
const CONSUMER_PROJECT_DEFINITION_EXACT = /^(xbrief|vbrief)\/PROJECT-DEFINITION\.(x|v)brief\.json$/;
|
|
104
|
+
/**
|
|
105
|
+
* Prefixes that would exempt entire consumer lifecycle trees or the whole
|
|
106
|
+
* xbrief/vbrief tree — never installer-managed (#1430).
|
|
107
|
+
*/
|
|
108
|
+
const FORBIDDEN_CONSUMER_PREFIXES = new Set([
|
|
109
|
+
"xbrief/",
|
|
110
|
+
"vbrief/",
|
|
111
|
+
...VBRIEF_LIFECYCLE_DIRS.flatMap((sub) => [`xbrief/${sub}/`, `vbrief/${sub}/`]),
|
|
112
|
+
]);
|
|
113
|
+
/**
|
|
114
|
+
* Fail closed when the installer-managed allowlist would exempt consumer
|
|
115
|
+
* PROJECT-DEFINITION or scope briefs (#3030 / #1430). Pure over `matchers` so
|
|
116
|
+
* tests can inject a bad matcher without mutating production state.
|
|
117
|
+
*
|
|
118
|
+
* Checks both:
|
|
119
|
+
* 1. Explicit probe paths in {@link CONSUMER_GUARD_MUST_FIRE}
|
|
120
|
+
* 2. Structural patterns (any exact consumer scope brief / PD; forbidden prefixes)
|
|
121
|
+
* so an unlisted `xbrief/active/another-scope.xbrief.json` matcher still fails.
|
|
122
|
+
*/
|
|
123
|
+
export function assertInstallerAllowlistHonors1430(matchers = installerManagedMatchers()) {
|
|
124
|
+
for (const path of CONSUMER_GUARD_MUST_FIRE) {
|
|
125
|
+
if (matchesInstallerManaged(path, matchers)) {
|
|
126
|
+
throw new Error(`#1430 violation: ${path} must not be installer-managed (SPEC consumer denylist; see CONSUMER_GUARD_MUST_FIRE / #3030)`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
for (const matcher of matchers) {
|
|
130
|
+
if (matcher.exact) {
|
|
131
|
+
if (CONSUMER_PROJECT_DEFINITION_EXACT.test(matcher.exact) ||
|
|
132
|
+
CONSUMER_SCOPE_BRIEF_EXACT.test(matcher.exact)) {
|
|
133
|
+
throw new Error(`#1430 violation: exact matcher ${matcher.exact} must not be installer-managed (consumer brief denylist / #3030)`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (matcher.prefix) {
|
|
137
|
+
const normalized = matcher.prefix.endsWith("/") ? matcher.prefix : `${matcher.prefix}/`;
|
|
138
|
+
if (FORBIDDEN_CONSUMER_PREFIXES.has(normalized)) {
|
|
139
|
+
throw new Error(`#1430 violation: prefix matcher ${matcher.prefix} must not cover consumer brief trees (#3030)`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
/** POSIX ERE alternation embedded in the deposited deft-core-guard workflow. */
|
|
145
|
+
export function installerManagedGuardEre() {
|
|
146
|
+
const matchers = installerManagedMatchers();
|
|
147
|
+
// Refuse to emit a guard workflow that would exempt consumer denylist paths.
|
|
148
|
+
assertInstallerAllowlistHonors1430(matchers);
|
|
149
|
+
return matchers.map((matcher) => matcherToEre(matcher)).join("|");
|
|
150
|
+
}
|
|
76
151
|
export function isInstallerManagedPath(path) {
|
|
77
152
|
return matchesInstallerManaged(path, installerManagedMatchers());
|
|
78
153
|
}
|
|
154
|
+
/**
|
|
155
|
+
* TS twin of Go `classifyChangedPaths` / deposited shell guard (#1430).
|
|
156
|
+
* Core = `.deft/core/**`; installer-managed = allowlist; app = everything else.
|
|
157
|
+
* Guard fails iff both core and app are non-empty.
|
|
158
|
+
*/
|
|
159
|
+
export function classifyMixedCoreAndApp(changedPaths, matchers = installerManagedMatchers()) {
|
|
160
|
+
const core = [];
|
|
161
|
+
const installerManaged = [];
|
|
162
|
+
const app = [];
|
|
163
|
+
for (const raw of changedPaths) {
|
|
164
|
+
const path = raw.replace(/\\/g, "/");
|
|
165
|
+
if (!path)
|
|
166
|
+
continue;
|
|
167
|
+
if (path === ".deft/core" || path.startsWith(".deft/core/")) {
|
|
168
|
+
core.push(path);
|
|
169
|
+
}
|
|
170
|
+
else if (matchesInstallerManaged(path, matchers)) {
|
|
171
|
+
installerManaged.push(path);
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
app.push(path);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return {
|
|
178
|
+
core,
|
|
179
|
+
installerManaged,
|
|
180
|
+
app,
|
|
181
|
+
wouldFail: core.length > 0 && app.length > 0,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
79
184
|
export function frameworkStagePaths(projectDir, deftDir, options = {}) {
|
|
80
185
|
const paths = [];
|
|
81
186
|
const seen = new Set();
|
|
@@ -34,6 +34,11 @@ export declare function userConfigDir(): string;
|
|
|
34
34
|
export declare function createUserConfigDir(io: InitDepositIo): string;
|
|
35
35
|
export declare function buildInstallSummaryJson(result: InitDepositResult, options: InitDepositArgs): Record<string, unknown>;
|
|
36
36
|
export declare function printNextSteps(result: InitDepositResult, io: InitDepositIo): void;
|
|
37
|
+
/**
|
|
38
|
+
* Ensure a minimal render-ready PROJECT-DEFINITION exists after lifecycle dirs
|
|
39
|
+
* are deposited (#3013 / epic #3009). Idempotent: never overwrites existing identity.
|
|
40
|
+
*/
|
|
41
|
+
export declare function seedMinimalProjectDefinition(projectDir: string, io: InitDepositIo): boolean;
|
|
37
42
|
export declare function runInitDeposit(args: InitDepositArgs, io: InitDepositIo, seams?: InitDepositSeams): Promise<InitDepositResult>;
|
|
38
43
|
export interface RunInitDepositCliOptions extends InitDepositArgs {
|
|
39
44
|
readonly writeOut: (text: string) => void;
|
|
@@ -7,12 +7,14 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
9
9
|
import { homedir, platform } from "node:os";
|
|
10
|
-
import { join, resolve } from "node:path";
|
|
10
|
+
import { basename, join, resolve } from "node:path";
|
|
11
11
|
import { assertDepositContained } from "../deposit/contain.js";
|
|
12
12
|
import { copyTree } from "../deposit/copy-tree.js";
|
|
13
13
|
import { prunePythonArtifactsFromDeposit } from "../deposit/python-free.js";
|
|
14
14
|
import { resolveInstalledContentRoot } from "../deposit/resolve-content.js";
|
|
15
15
|
import { readCorePackageVersion } from "../engine-version.js";
|
|
16
|
+
import { renderProjectDefinition } from "../render/project-render.js";
|
|
17
|
+
import { removeStaleMigratedFrameworkNarrative } from "../xbrief-migrate/migrate-project.js";
|
|
16
18
|
import { writeAgentHookDeposit } from "./agent-hooks.js";
|
|
17
19
|
import { ensureInitGitignoreLines, reconstituteDepositFromContent } from "./gitignore.js";
|
|
18
20
|
import { depositStagePaths, printCommitGuidance } from "./hygiene.js";
|
|
@@ -20,6 +22,7 @@ import { buildLegacyRefusalJson, buildLegacyRefusalMessage, detectLegacyLayout,
|
|
|
20
22
|
import { printMigrateNudgeIfNeeded } from "./migrate.js";
|
|
21
23
|
import { ensurePrettierIgnoreLines } from "./prettierignore.js";
|
|
22
24
|
import { CANONICAL_INSTALL_ROOT, depositNeutralization, ensureTaskfile, writeAgentsMd, writeAgentsSkills, writeConsumerGitHooks, writeConsumerVbrief, writeInstallManifest, } from "./scaffold.js";
|
|
25
|
+
import { syncBareVersionMarker } from "./xbrief-projections.js";
|
|
23
26
|
export function parseInitArgv(canonicalArgv, userArgv = []) {
|
|
24
27
|
const args = [...canonicalArgv, ...userArgv];
|
|
25
28
|
let projectDir = process.cwd();
|
|
@@ -113,10 +116,36 @@ export function printNextSteps(result, io) {
|
|
|
113
116
|
io.printf(` 1. Open your AI coding assistant in ${result.projectDir}\n`);
|
|
114
117
|
io.printf(" 2. Deft skill auto-discovery is partially implemented — if your agent doesn't\n");
|
|
115
118
|
io.printf(' start setup automatically, tell it: "Use AGENTS.md"\n');
|
|
116
|
-
io.printf(" 3. On first session, the agent will guide you through
|
|
119
|
+
io.printf(" 3. On first session, the agent will guide you through USER.md (if missing).\n");
|
|
120
|
+
io.printf(" 4. PROJECT-DEFINITION is seeded at init (#3013) — run `task project:render` once to refresh items from lifecycle folders; do not multi-turn invent project identity.\n");
|
|
117
121
|
printMigrateNudgeIfNeeded(result.projectDir, io);
|
|
118
122
|
io.printf("\n");
|
|
119
123
|
}
|
|
124
|
+
/**
|
|
125
|
+
* Ensure a minimal render-ready PROJECT-DEFINITION exists after lifecycle dirs
|
|
126
|
+
* are deposited (#3013 / epic #3009). Idempotent: never overwrites existing identity.
|
|
127
|
+
*/
|
|
128
|
+
export function seedMinimalProjectDefinition(projectDir, io) {
|
|
129
|
+
const xbriefDir = join(projectDir, "xbrief");
|
|
130
|
+
const legacyDir = join(projectDir, "vbrief");
|
|
131
|
+
const root = existsSync(xbriefDir) ? xbriefDir : existsSync(legacyDir) ? legacyDir : null;
|
|
132
|
+
if (root === null) {
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
const isXbrief = basename(root) === "xbrief";
|
|
136
|
+
const projectDefPath = join(root, isXbrief ? "PROJECT-DEFINITION.xbrief.json" : "PROJECT-DEFINITION.vbrief.json");
|
|
137
|
+
if (existsSync(projectDefPath)) {
|
|
138
|
+
io.printf("PROJECT-DEFINITION already present — leaving identity intact (#3013).\n");
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
const [ok, message] = renderProjectDefinition(root);
|
|
142
|
+
if (ok) {
|
|
143
|
+
io.printf(`${message} (minimal seed for one-shot project:render; #3013)\n`);
|
|
144
|
+
return true;
|
|
145
|
+
}
|
|
146
|
+
io.printf(`PROJECT-DEFINITION seed skipped: ${message}\n`);
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
120
149
|
export async function runInitDeposit(args, io, seams = {}) {
|
|
121
150
|
const projectDir = args.projectDir;
|
|
122
151
|
const deftDir = join(projectDir, CANONICAL_INSTALL_ROOT);
|
|
@@ -154,6 +183,12 @@ export async function runInitDeposit(args, io, seams = {}) {
|
|
|
154
183
|
const skillsCreated = writeAgentsSkills(projectDir, io);
|
|
155
184
|
await depositNeutralization(projectDir, io);
|
|
156
185
|
await writeConsumerVbrief(projectDir, deftDir, io);
|
|
186
|
+
seedMinimalProjectDefinition(projectDir, io);
|
|
187
|
+
// Seeding PROJECT-DEFINITION.xbrief.json makes resolveLifecycleRoot succeed.
|
|
188
|
+
// Align the same consumer derivatives update repairs (#2595 / #2806) so
|
|
189
|
+
// init + git add -A is already clean under core.autocrlf=true (#2118 / #3013).
|
|
190
|
+
syncBareVersionMarker(projectDir, version);
|
|
191
|
+
removeStaleMigratedFrameworkNarrative(projectDir);
|
|
157
192
|
writeConsumerGitHooks(projectDir, deftDir, io, seams.gitHooks);
|
|
158
193
|
writeAgentHookDeposit(projectDir, io);
|
|
159
194
|
let taskfileWired = false;
|
|
@@ -14,7 +14,7 @@ import { assertDestinationNotSymlink, ProjectionContainmentError, } from "../fs/
|
|
|
14
14
|
import { agentsRefreshPlan } from "../platform/agents-md.js";
|
|
15
15
|
import { MIGRATED_ARTIFACT_DIR } from "../xbrief-migrate/constants.js";
|
|
16
16
|
import { CANONICAL_INSTALL_ROOT } from "./constants.js";
|
|
17
|
-
import { installerManagedGuardEre } from "./hygiene.js";
|
|
17
|
+
import { assertInstallerAllowlistHonors1430, installerManagedGuardEre } from "./hygiene.js";
|
|
18
18
|
import { syncConsumerXbriefSchemas } from "./xbrief-projections.js";
|
|
19
19
|
export { CANONICAL_INSTALL_ROOT };
|
|
20
20
|
export const CORE_GLOB = ".deft/core/**";
|
|
@@ -632,6 +632,8 @@ export function mergeCoreGuardWorkflowRefresh(existing, desired) {
|
|
|
632
632
|
return desired.slice(0, idx) + existingCheckout + desired.slice(idx + desiredCheckout.length);
|
|
633
633
|
}
|
|
634
634
|
function coreGuardWorkflowContent() {
|
|
635
|
+
// Fail closed before emitting guard ERE if allowlist violates #1430 denylist (#3030).
|
|
636
|
+
assertInstallerAllowlistHonors1430();
|
|
635
637
|
const baseSha = githubActionsExpr("github.event.pull_request.base.sha");
|
|
636
638
|
const headSha = githubActionsExpr("github.event.pull_request.head.sha");
|
|
637
639
|
return ("name: deft-core-guard\n\n" +
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { CompletedProcess } from "../scm/call.js";
|
|
2
2
|
import { probeRuntimeCapabilities, type RuntimeCapabilityReport } from "./platform-capabilities.js";
|
|
3
3
|
export declare const GITHUB_AUTH_MODE_INJECTED_TOKEN = "injected-token";
|
|
4
4
|
export declare const GITHUB_AUTH_MODE_HOST_GH = "host-gh";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
2
|
import { pyRepr } from "../scm/py-format.js";
|
|
3
3
|
import { getPlatformCapabilities, probeRuntimeCapabilities, RUNTIME_MODE_CLOUD_HEADLESS, RUNTIME_MODE_CURSOR_NATIVE_SANDBOX, } from "./platform-capabilities.js";
|
|
4
4
|
export const GITHUB_AUTH_MODE_INJECTED_TOKEN = "injected-token";
|
|
@@ -37,9 +37,31 @@ export function inferGithubAuthMode(runtimeReport) {
|
|
|
37
37
|
}
|
|
38
38
|
return GITHUB_AUTH_MODE_HOST_GH;
|
|
39
39
|
}
|
|
40
|
+
/**
|
|
41
|
+
* Prefer live `gh` for auth/API validation. Do not route through scm.call /
|
|
42
|
+
* BINARY_PREFERENCE (ghx-first): ghx is a cached GET proxy and rejects multi-arg
|
|
43
|
+
* `api user --jq .login` forms used here (#2275 Greptile P1 / #954).
|
|
44
|
+
*/
|
|
40
45
|
function defaultRunGh(args, environ) {
|
|
41
|
-
const
|
|
42
|
-
|
|
46
|
+
const binary = "gh";
|
|
47
|
+
try {
|
|
48
|
+
const result = spawnSync(binary, [...args], {
|
|
49
|
+
env: environ,
|
|
50
|
+
encoding: "utf8",
|
|
51
|
+
timeout: 30_000,
|
|
52
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
53
|
+
});
|
|
54
|
+
return {
|
|
55
|
+
args: [binary, ...args],
|
|
56
|
+
returncode: result.status ?? 1,
|
|
57
|
+
stdout: typeof result.stdout === "string" ? result.stdout : "",
|
|
58
|
+
stderr: typeof result.stderr === "string" ? result.stderr : "",
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
63
|
+
return { args: [binary, ...args], returncode: 1, stdout: "", stderr: message };
|
|
64
|
+
}
|
|
43
65
|
}
|
|
44
66
|
function splitRepo(repo) {
|
|
45
67
|
const idx = repo.indexOf("/");
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { ScmStubError } from "../scm/errors.js";
|
|
4
|
+
import { requireScmReady } from "../scm/readiness.js";
|
|
3
5
|
import { issueIngestMain } from "./issue-ingest.js";
|
|
4
6
|
function parseArgs(argv) {
|
|
5
7
|
const out = {};
|
|
@@ -25,6 +27,17 @@ function parseArgs(argv) {
|
|
|
25
27
|
return out;
|
|
26
28
|
}
|
|
27
29
|
export function mainEntry(argv = process.argv.slice(2)) {
|
|
30
|
+
// #2275: fail loud when gh/auth is missing in this execution env.
|
|
31
|
+
try {
|
|
32
|
+
requireScmReady();
|
|
33
|
+
}
|
|
34
|
+
catch (err) {
|
|
35
|
+
if (err instanceof ScmStubError) {
|
|
36
|
+
process.stderr.write(`error: ${err.message}\n`);
|
|
37
|
+
return 2;
|
|
38
|
+
}
|
|
39
|
+
throw err;
|
|
40
|
+
}
|
|
28
41
|
return issueIngestMain(parseArgs(argv));
|
|
29
42
|
}
|
|
30
43
|
if (process.argv[1] !== undefined && fileURLToPath(import.meta.url) === process.argv[1]) {
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { ScmStubError } from "../scm/errors.js";
|
|
4
|
+
import { requireScmReady } from "../scm/readiness.js";
|
|
3
5
|
import { reconcileMain } from "./reconcile-issues.js";
|
|
4
6
|
function parseArgs(argv) {
|
|
5
7
|
const out = {};
|
|
@@ -23,6 +25,17 @@ function parseArgs(argv) {
|
|
|
23
25
|
return out;
|
|
24
26
|
}
|
|
25
27
|
export function mainEntry(argv = process.argv.slice(2)) {
|
|
28
|
+
// #2275: fail loud when gh/auth is missing in this execution env.
|
|
29
|
+
try {
|
|
30
|
+
requireScmReady();
|
|
31
|
+
}
|
|
32
|
+
catch (err) {
|
|
33
|
+
if (err instanceof ScmStubError) {
|
|
34
|
+
process.stderr.write(`error: ${err.message}\n`);
|
|
35
|
+
return 2;
|
|
36
|
+
}
|
|
37
|
+
throw err;
|
|
38
|
+
}
|
|
26
39
|
return reconcileMain(parseArgs(argv));
|
|
27
40
|
}
|
|
28
41
|
if (process.argv[1] !== undefined && fileURLToPath(import.meta.url) === process.argv[1]) {
|
|
@@ -36,7 +36,11 @@ export declare function detectGitBranch(projectRoot: string, execGit?: (args: st
|
|
|
36
36
|
}): string;
|
|
37
37
|
/** Read probe session from project root. */
|
|
38
38
|
export declare function readSession(projectRoot: string): ProbeSession | null;
|
|
39
|
-
/**
|
|
39
|
+
/**
|
|
40
|
+
* Atomically persist session to .deft/probe-session.json.
|
|
41
|
+
* #3042: contain against projectRoot; refuse escaping `.deft` symlink parents
|
|
42
|
+
* (no bare open/write/rename).
|
|
43
|
+
*/
|
|
40
44
|
export declare function writeSession(projectRoot: string, session: ProbeSession): string;
|
|
41
45
|
export declare function startSession(projectRoot: string, options: {
|
|
42
46
|
target: string;
|
|
@@ -3,8 +3,10 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { execFileSync } from "node:child_process";
|
|
5
5
|
import { randomBytes } from "node:crypto";
|
|
6
|
-
import {
|
|
6
|
+
import { existsSync, readFileSync, renameSync, rmSync } from "node:fs";
|
|
7
7
|
import { join, resolve } from "node:path";
|
|
8
|
+
import { containedWrite } from "../fs/contained-write.js";
|
|
9
|
+
import { assertWriteTargetSafe } from "../fs/projection-containment.js";
|
|
8
10
|
export const SCHEMA_VERSION = 1;
|
|
9
11
|
export const SESSION_RELPATH = [".deft", "probe-session.json"];
|
|
10
12
|
export const STATE_INTERROGATE = "interrogate";
|
|
@@ -188,11 +190,16 @@ export function readSession(projectRoot) {
|
|
|
188
190
|
completed_at: completedAt,
|
|
189
191
|
};
|
|
190
192
|
}
|
|
191
|
-
/**
|
|
193
|
+
/**
|
|
194
|
+
* Atomically persist session to .deft/probe-session.json.
|
|
195
|
+
* #3042: contain against projectRoot; refuse escaping `.deft` symlink parents
|
|
196
|
+
* (no bare open/write/rename).
|
|
197
|
+
*/
|
|
192
198
|
export function writeSession(projectRoot, session) {
|
|
193
|
-
const
|
|
194
|
-
|
|
195
|
-
|
|
199
|
+
const root = resolve(projectRoot);
|
|
200
|
+
const sessionFile = sessionPath(root);
|
|
201
|
+
assertWriteTargetSafe(root, sessionFile);
|
|
202
|
+
const tmpName = join(root, ".deft", `.probe-session.${randomBytes(8).toString("hex")}.json.tmp`);
|
|
196
203
|
const sortedPayload = sessionToDict(session);
|
|
197
204
|
const sortedKeys = Object.keys(sortedPayload).sort();
|
|
198
205
|
const sortedObj = {};
|
|
@@ -200,20 +207,24 @@ export function writeSession(projectRoot, session) {
|
|
|
200
207
|
sortedObj[k] = sortedPayload[k];
|
|
201
208
|
}
|
|
202
209
|
const finalContent = `${JSON.stringify(sortedObj, null, 2)}\n`;
|
|
203
|
-
const fd = openSync(tmpName, "w");
|
|
204
210
|
try {
|
|
205
|
-
|
|
211
|
+
containedWrite({
|
|
212
|
+
root,
|
|
213
|
+
target: tmpName,
|
|
214
|
+
data: finalContent,
|
|
215
|
+
mode: "create",
|
|
216
|
+
});
|
|
217
|
+
renameSync(tmpName, sessionFile);
|
|
218
|
+
}
|
|
219
|
+
catch (err) {
|
|
206
220
|
try {
|
|
207
|
-
|
|
221
|
+
rmSync(tmpName, { force: true });
|
|
208
222
|
}
|
|
209
223
|
catch {
|
|
210
|
-
|
|
224
|
+
/* best-effort cleanup */
|
|
211
225
|
}
|
|
226
|
+
throw err;
|
|
212
227
|
}
|
|
213
|
-
finally {
|
|
214
|
-
closeSync(fd);
|
|
215
|
-
}
|
|
216
|
-
renameSync(tmpName, sessionFile);
|
|
217
228
|
return sessionFile;
|
|
218
229
|
}
|
|
219
230
|
export function startSession(projectRoot, options) {
|
|
@@ -324,7 +324,7 @@ export function applyAgentsRefresh(projectRoot, options = {}, seams = {}, lockDe
|
|
|
324
324
|
const writable = AGENTS_REFRESH_WRITABLE_STATES.has(state) && typeof newContent === "string";
|
|
325
325
|
return { state, path, wrote: false, writable };
|
|
326
326
|
}
|
|
327
|
-
atomicWriteText(path, newContent);
|
|
327
|
+
atomicWriteText(path, newContent, { projectRoot });
|
|
328
328
|
return { state, path, wrote: true, writable: true };
|
|
329
329
|
}, lockDeps);
|
|
330
330
|
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Temporary test/local kill-switch for Directive enforcement (#3039).
|
|
3
|
+
*
|
|
4
|
+
* Presence of root `.deft-directive-disable` means Directive **enforcement** is
|
|
5
|
+
* OFF (hooks, session ritual, automation) while the deposit may remain — **only
|
|
6
|
+
* when the file is not tracked by git**. A committed flag is misconfig: doctor
|
|
7
|
+
* warns and enforcement stays ON (repository-controlled content must not disable
|
|
8
|
+
* hooks for downstream clones).
|
|
9
|
+
*
|
|
10
|
+
* Distinct from `.no-deft-directive` (#2926 permanent opt-out): flag+deposit is
|
|
11
|
+
* **not** inconsistent here.
|
|
12
|
+
*
|
|
13
|
+
* Product choices (v1):
|
|
14
|
+
* - Flag is **root-only** (workspace root the tool opened).
|
|
15
|
+
* - Flag **must be gitignored** (canonical baseline); committed flag → doctor
|
|
16
|
+
* warns and does **not** short-circuit enforcement.
|
|
17
|
+
* - Deposit presence is **OK**.
|
|
18
|
+
* - Full re-enable requires: delete the file **and** start a **new** agent session.
|
|
19
|
+
* - Precedence: active kill-switch first, then `.no-deft-directive`, else normal.
|
|
20
|
+
*/
|
|
21
|
+
/** Canonical root-only filename (lowercase). Presence = candidate flag. */
|
|
22
|
+
export declare const DEFT_DIRECTIVE_DISABLE_FLAG_NAME = ".deft-directive-disable";
|
|
23
|
+
/** Gitignore entry the deposit must ensure (same as flag name). */
|
|
24
|
+
export declare const DEFT_DIRECTIVE_DISABLE_GITIGNORE_LINE = ".deft-directive-disable";
|
|
25
|
+
/**
|
|
26
|
+
* Canonical recovery message when Directive is disabled by the test kill-switch.
|
|
27
|
+
* Surfaces (doctor, agent, CLI, hooks) share this wording.
|
|
28
|
+
*/
|
|
29
|
+
export declare const DEFT_DIRECTIVE_DISABLE_RECOVERY_MESSAGE: string;
|
|
30
|
+
/** One-line summary for log lines and short CLI output. */
|
|
31
|
+
export declare const DEFT_DIRECTIVE_DISABLE_ONE_LINE = "Directive disabled via `.deft-directive-disable` (test/local kill-switch; deposit OK)";
|
|
32
|
+
/** Doctor/status label when the kill-switch is active (local / untracked). */
|
|
33
|
+
export declare const DEFT_DIRECTIVE_DISABLE_STATUS: "disabled-test-kill-switch";
|
|
34
|
+
/**
|
|
35
|
+
* Warning when the kill-switch file is tracked by git (should be gitignored).
|
|
36
|
+
* Tracked flags do **not** disable enforcement.
|
|
37
|
+
*/
|
|
38
|
+
export declare const DEFT_DIRECTIVE_DISABLE_TRACKED_WARNING = "Misconfig: `.deft-directive-disable` is tracked by git. The flag must be gitignored (local kill-switch only). Untrack it and ensure `.gitignore` covers the path. Enforcement is NOT disabled while the flag is tracked.";
|
|
39
|
+
export interface DeftDirectiveDisableSeams {
|
|
40
|
+
readonly isFile?: (path: string) => boolean;
|
|
41
|
+
readonly isDir?: (path: string) => boolean;
|
|
42
|
+
/** Return true when `git ls-files` lists the flag (tracked). */
|
|
43
|
+
readonly isGitTracked?: (projectRoot: string, relPath: string) => boolean;
|
|
44
|
+
/** Skip tracked probe + cache (tests / doctor full re-check). */
|
|
45
|
+
readonly skipTrackedCache?: boolean;
|
|
46
|
+
}
|
|
47
|
+
export interface DeftDirectiveDisableState {
|
|
48
|
+
readonly present: boolean;
|
|
49
|
+
readonly flagPath: string;
|
|
50
|
+
readonly depositPresent: boolean;
|
|
51
|
+
/**
|
|
52
|
+
* True when the flag file is tracked by git (misconfig — should be gitignored).
|
|
53
|
+
* Always false when the flag is absent or when the probe cannot run.
|
|
54
|
+
*/
|
|
55
|
+
readonly trackedByGit: boolean;
|
|
56
|
+
/**
|
|
57
|
+
* True when the kill-switch is **active** for enforcement short-circuit:
|
|
58
|
+
* present and **not** tracked by git.
|
|
59
|
+
*/
|
|
60
|
+
readonly active: boolean;
|
|
61
|
+
}
|
|
62
|
+
/** Absolute path to the root kill-switch flag file. */
|
|
63
|
+
export declare function deftDirectiveDisableFlagPath(projectRoot: string): string;
|
|
64
|
+
/**
|
|
65
|
+
* Detect root `.deft-directive-disable`. Presence is file-existence only
|
|
66
|
+
* (empty or short comment OK). Deposit presence is reported but never treated
|
|
67
|
+
* as inconsistent (#3039 vs #2926).
|
|
68
|
+
*
|
|
69
|
+
* Enforcement short-circuit uses `state.active` (present && !trackedByGit).
|
|
70
|
+
*/
|
|
71
|
+
export declare function detectDeftDirectiveDisable(projectRoot: string, seams?: DeftDirectiveDisableSeams): DeftDirectiveDisableState;
|
|
72
|
+
/**
|
|
73
|
+
* True when the local (untracked) kill-switch is active for enforcement
|
|
74
|
+
* short-circuit. Tracked flags return false so hooks stay enforced.
|
|
75
|
+
*/
|
|
76
|
+
export declare function isDeftDirectiveDisableActive(projectRoot: string, seams?: DeftDirectiveDisableSeams): boolean;
|
|
77
|
+
/**
|
|
78
|
+
* Full operator-facing message for the kill-switch, optionally combined with
|
|
79
|
+
* permanent opt-out when both flags are present.
|
|
80
|
+
*/
|
|
81
|
+
export declare function formatDeftDirectiveDisableMessage(options?: {
|
|
82
|
+
readonly permanentOptOutAlsoPresent?: boolean;
|
|
83
|
+
readonly trackedByGit?: boolean;
|
|
84
|
+
readonly oneLine?: boolean;
|
|
85
|
+
}): string;
|
|
86
|
+
//# sourceMappingURL=deft-directive-disable.d.ts.map
|