@deftai/directive 0.104.0 → 0.105.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/authz.d.ts +4 -29
- package/dist/authz.js +16 -181
- package/dist/dispatch.d.ts +1 -1
- package/dist/dispatch.js +13 -0
- package/dist/human-presence-mint.d.ts +61 -0
- package/dist/human-presence-mint.js +192 -0
- package/dist/occupancy-steal.d.ts +9 -0
- package/dist/occupancy-steal.js +54 -0
- package/dist/scm-sync-default.d.ts +34 -0
- package/dist/scm-sync-default.js +210 -0
- package/dist/scope-record-approved-scope.d.ts +6 -1
- package/dist/scope-record-approved-scope.js +72 -24
- package/dist/session-start.d.ts +4 -0
- package/dist/session-start.js +23 -0
- package/dist/verify-ac.d.ts +2 -0
- package/dist/verify-ac.js +30 -6
- package/dist/verify-completed-tracked.d.ts +2 -1
- package/dist/verify-completed-tracked.js +31 -1
- package/dist/verify-literal-ac.js +7 -0
- package/dist/verify-orphan-active.d.ts +1 -0
- package/dist/verify-orphan-active.js +30 -0
- package/dist/verify-session-ritual.d.ts +10 -2
- package/dist/verify-session-ritual.js +16 -3
- package/package.json +3 -3
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI for `scm:sync-default` (#3391).
|
|
3
|
+
*
|
|
4
|
+
* Opens dest-targeted default-branch sync PRs using the shared detector and
|
|
5
|
+
* syncMaxFiles. Over-limit legs are new branches and new PRs.
|
|
6
|
+
*/
|
|
7
|
+
import { planSyncDefault, type SyncDefaultForge, type SyncDefaultOpenPull } from "@deftai/directive-core/policy";
|
|
8
|
+
export declare const USAGE: string;
|
|
9
|
+
export interface SyncDefaultCliArgs {
|
|
10
|
+
readonly dryRun?: boolean;
|
|
11
|
+
readonly json?: boolean;
|
|
12
|
+
readonly help?: boolean;
|
|
13
|
+
readonly maxFiles?: number;
|
|
14
|
+
readonly projectRoot?: string;
|
|
15
|
+
readonly repo?: string;
|
|
16
|
+
}
|
|
17
|
+
export declare function parseSyncDefaultArgs(argv: readonly string[]): {
|
|
18
|
+
args: SyncDefaultCliArgs;
|
|
19
|
+
error: string | null;
|
|
20
|
+
};
|
|
21
|
+
export declare function pullsFromRestJson(payload: unknown): readonly SyncDefaultOpenPull[];
|
|
22
|
+
export declare function createGhSyncDefaultForge(): SyncDefaultForge;
|
|
23
|
+
export declare function resolveRepoFromGit(projectRoot: string): string | null;
|
|
24
|
+
export declare function runSyncDefaultCli(args: SyncDefaultCliArgs, options?: {
|
|
25
|
+
writeOut?: (s: string) => void;
|
|
26
|
+
writeErr?: (s: string) => void;
|
|
27
|
+
forge?: SyncDefaultForge;
|
|
28
|
+
cwd?: string;
|
|
29
|
+
runGit?: Parameters<typeof planSyncDefault>[0]["runGit"];
|
|
30
|
+
resolveRepo?: (projectRoot: string) => string | null;
|
|
31
|
+
}): number;
|
|
32
|
+
export declare function mainEntry(argv?: string[]): number;
|
|
33
|
+
export declare function main(argv?: string[]): number;
|
|
34
|
+
//# sourceMappingURL=scm-sync-default.d.ts.map
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI for `scm:sync-default` (#3391).
|
|
3
|
+
*
|
|
4
|
+
* Opens dest-targeted default-branch sync PRs using the shared detector and
|
|
5
|
+
* syncMaxFiles. Over-limit legs are new branches and new PRs.
|
|
6
|
+
*/
|
|
7
|
+
import { writeFileSync } from "node:fs";
|
|
8
|
+
import { tmpdir } from "node:os";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import { fileURLToPath } from "node:url";
|
|
11
|
+
import { applySyncDefault, formatSyncDefaultHuman, parseGithubOwnerRepo, planSyncDefault, SYNC_DEFAULT_VERB, } from "@deftai/directive-core/policy";
|
|
12
|
+
import { runGhApi, splitRepo } from "@deftai/directive-core/scm";
|
|
13
|
+
import { defaultGitRunner } from "@deftai/directive-core/session";
|
|
14
|
+
export const USAGE = `usage: deft ${SYNC_DEFAULT_VERB} [--dry-run] [--json] [--max-files N] [--repo OWNER/REPO]\n` +
|
|
15
|
+
" Open dest-targeted sync PRs under syncMaxFiles. Under the limit: one new PR\n" +
|
|
16
|
+
" from source tip to dest. Over: merge-commit cuts; each dest-based leg is a\n" +
|
|
17
|
+
" new branch and a new PR. After a leg merges, run again. Never retarget or\n" +
|
|
18
|
+
" reuse an oversized PR. Each leg must be new when the reviewer first sees it.\n" +
|
|
19
|
+
" Required checks stay on except the Wave 1 core-guard sync exemption.\n";
|
|
20
|
+
export function parseSyncDefaultArgs(argv) {
|
|
21
|
+
const out = {};
|
|
22
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
23
|
+
const arg = argv[i];
|
|
24
|
+
if (arg === "--dry-run") {
|
|
25
|
+
out.dryRun = true;
|
|
26
|
+
}
|
|
27
|
+
else if (arg === "--json") {
|
|
28
|
+
out.json = true;
|
|
29
|
+
}
|
|
30
|
+
else if (arg === "--help" || arg === "-h") {
|
|
31
|
+
out.help = true;
|
|
32
|
+
}
|
|
33
|
+
else if (arg === "--max-files") {
|
|
34
|
+
const raw = argv[++i];
|
|
35
|
+
const parsed = raw === undefined ? Number.NaN : Number(raw);
|
|
36
|
+
if (!Number.isInteger(parsed) || parsed < 0) {
|
|
37
|
+
return { args: out, error: `--max-files expects a non-negative integer, got ${raw ?? ""}` };
|
|
38
|
+
}
|
|
39
|
+
out.maxFiles = parsed;
|
|
40
|
+
}
|
|
41
|
+
else if (arg === "--project-root") {
|
|
42
|
+
const value = argv[++i];
|
|
43
|
+
if (value === undefined) {
|
|
44
|
+
return { args: out, error: "--project-root expects a path" };
|
|
45
|
+
}
|
|
46
|
+
out.projectRoot = value;
|
|
47
|
+
}
|
|
48
|
+
else if (arg === "--repo") {
|
|
49
|
+
const value = argv[++i];
|
|
50
|
+
if (value === undefined) {
|
|
51
|
+
return { args: out, error: "--repo expects OWNER/REPO" };
|
|
52
|
+
}
|
|
53
|
+
out.repo = value;
|
|
54
|
+
}
|
|
55
|
+
else if (arg.startsWith("-")) {
|
|
56
|
+
return { args: out, error: `unknown flag ${JSON.stringify(arg)}` };
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
return { args: out, error: `unexpected argument ${JSON.stringify(arg)}` };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return { args: out, error: null };
|
|
63
|
+
}
|
|
64
|
+
export function pullsFromRestJson(payload) {
|
|
65
|
+
if (!Array.isArray(payload))
|
|
66
|
+
return [];
|
|
67
|
+
const pulls = [];
|
|
68
|
+
for (const row of payload) {
|
|
69
|
+
if (typeof row !== "object" || row === null)
|
|
70
|
+
continue;
|
|
71
|
+
const rec = row;
|
|
72
|
+
const head = rec.head;
|
|
73
|
+
const baseRec = rec.base;
|
|
74
|
+
if (typeof head !== "object" || head === null)
|
|
75
|
+
continue;
|
|
76
|
+
if (typeof baseRec !== "object" || baseRec === null)
|
|
77
|
+
continue;
|
|
78
|
+
const headRec = head;
|
|
79
|
+
const baseObj = baseRec;
|
|
80
|
+
const number = rec.number;
|
|
81
|
+
const htmlUrl = rec.html_url;
|
|
82
|
+
const headRef = headRec.ref;
|
|
83
|
+
const headSha = headRec.sha;
|
|
84
|
+
const baseRef = baseObj.ref;
|
|
85
|
+
if (typeof number !== "number" ||
|
|
86
|
+
typeof htmlUrl !== "string" ||
|
|
87
|
+
typeof headRef !== "string" ||
|
|
88
|
+
typeof headSha !== "string" ||
|
|
89
|
+
typeof baseRef !== "string") {
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
pulls.push({ number, htmlUrl, headRef, headSha, baseRef });
|
|
93
|
+
}
|
|
94
|
+
return pulls;
|
|
95
|
+
}
|
|
96
|
+
export function createGhSyncDefaultForge() {
|
|
97
|
+
return {
|
|
98
|
+
listOpenPulls(repo, base) {
|
|
99
|
+
const [owner, name] = splitRepo(repo);
|
|
100
|
+
const endpoint = `repos/${owner}/${name}/pulls`;
|
|
101
|
+
const result = runGhApi([
|
|
102
|
+
endpoint,
|
|
103
|
+
"--method",
|
|
104
|
+
"GET",
|
|
105
|
+
"--raw-field",
|
|
106
|
+
`base=${base}`,
|
|
107
|
+
"--raw-field",
|
|
108
|
+
"state=open",
|
|
109
|
+
"--raw-field",
|
|
110
|
+
"per_page=100",
|
|
111
|
+
]);
|
|
112
|
+
if (result.returncode !== 0) {
|
|
113
|
+
throw new Error(result.stderr || `failed to list open pulls for ${repo}`);
|
|
114
|
+
}
|
|
115
|
+
return pullsFromRestJson(JSON.parse(result.stdout || "[]"));
|
|
116
|
+
},
|
|
117
|
+
createPull(repo, input) {
|
|
118
|
+
const [owner, name] = splitRepo(repo);
|
|
119
|
+
const endpoint = `repos/${owner}/${name}/pulls`;
|
|
120
|
+
const bodyPath = join(tmpdir(), `deft-sync-default-${Date.now()}.json`);
|
|
121
|
+
writeFileSync(bodyPath, JSON.stringify({
|
|
122
|
+
title: input.title,
|
|
123
|
+
head: input.head,
|
|
124
|
+
base: input.base,
|
|
125
|
+
body: input.body,
|
|
126
|
+
}), { encoding: "utf8" });
|
|
127
|
+
const result = runGhApi(["-X", "POST", endpoint, "--input", bodyPath]);
|
|
128
|
+
if (result.returncode !== 0) {
|
|
129
|
+
throw new Error(result.stderr || `failed to create pull for ${repo}`);
|
|
130
|
+
}
|
|
131
|
+
const created = JSON.parse(result.stdout);
|
|
132
|
+
if (typeof created.number !== "number" || typeof created.html_url !== "string") {
|
|
133
|
+
throw new Error("create pull returned no number/html_url");
|
|
134
|
+
}
|
|
135
|
+
return { number: created.number, htmlUrl: created.html_url };
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
export function resolveRepoFromGit(projectRoot) {
|
|
140
|
+
const remote = defaultGitRunner(projectRoot, ["remote", "get-url", "origin"]);
|
|
141
|
+
if (remote.code !== 0)
|
|
142
|
+
return null;
|
|
143
|
+
return parseGithubOwnerRepo(remote.stdout);
|
|
144
|
+
}
|
|
145
|
+
export function runSyncDefaultCli(args, options = {}) {
|
|
146
|
+
const writeOut = options.writeOut ?? ((s) => process.stdout.write(s));
|
|
147
|
+
const writeErr = options.writeErr ?? ((s) => process.stderr.write(s));
|
|
148
|
+
if (args.help === true) {
|
|
149
|
+
writeOut(USAGE);
|
|
150
|
+
return 0;
|
|
151
|
+
}
|
|
152
|
+
const projectRoot = args.projectRoot ?? options.cwd ?? process.cwd();
|
|
153
|
+
const plan = planSyncDefault({
|
|
154
|
+
projectRoot,
|
|
155
|
+
maxFiles: args.maxFiles,
|
|
156
|
+
runGit: options.runGit,
|
|
157
|
+
});
|
|
158
|
+
if (plan.action === "noop") {
|
|
159
|
+
if (args.json === true) {
|
|
160
|
+
writeOut(`${JSON.stringify({ ...plan, opened: [], retargeted: false }, null, 2)}\n`);
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
writeOut(`${plan.message}\n`);
|
|
164
|
+
}
|
|
165
|
+
return plan.noopReason === "fetch-failed" || plan.noopReason === "diff-failed" ? 2 : 0;
|
|
166
|
+
}
|
|
167
|
+
const resolveRepo = options.resolveRepo ?? resolveRepoFromGit;
|
|
168
|
+
const repo = args.repo ?? resolveRepo(projectRoot);
|
|
169
|
+
if (repo === null && args.dryRun !== true) {
|
|
170
|
+
writeErr("scm:sync-default: could not resolve OWNER/REPO from origin; pass --repo\n");
|
|
171
|
+
return 2;
|
|
172
|
+
}
|
|
173
|
+
try {
|
|
174
|
+
const result = applySyncDefault({
|
|
175
|
+
projectRoot,
|
|
176
|
+
repo: repo ?? "owner/repo",
|
|
177
|
+
plan,
|
|
178
|
+
dryRun: args.dryRun === true,
|
|
179
|
+
runGit: options.runGit,
|
|
180
|
+
forge: args.dryRun === true ? undefined : (options.forge ?? createGhSyncDefaultForge()),
|
|
181
|
+
});
|
|
182
|
+
if (args.json === true) {
|
|
183
|
+
writeOut(`${JSON.stringify(result, null, 2)}\n`);
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
writeOut(formatSyncDefaultHuman(result));
|
|
187
|
+
}
|
|
188
|
+
return 0;
|
|
189
|
+
}
|
|
190
|
+
catch (err) {
|
|
191
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
192
|
+
writeErr(`scm:sync-default: ${message}\n`);
|
|
193
|
+
return 1;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
export function mainEntry(argv = process.argv.slice(2)) {
|
|
197
|
+
const { args, error } = parseSyncDefaultArgs(argv);
|
|
198
|
+
if (error !== null) {
|
|
199
|
+
process.stderr.write(`error: ${error}\n${USAGE}`);
|
|
200
|
+
return 2;
|
|
201
|
+
}
|
|
202
|
+
return runSyncDefaultCli(args);
|
|
203
|
+
}
|
|
204
|
+
export function main(argv = process.argv.slice(2)) {
|
|
205
|
+
return mainEntry(argv);
|
|
206
|
+
}
|
|
207
|
+
if (process.argv[1] !== undefined && fileURLToPath(import.meta.url) === process.argv[1]) {
|
|
208
|
+
process.exit(mainEntry());
|
|
209
|
+
}
|
|
210
|
+
//# sourceMappingURL=scm-sync-default.js.map
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { type HumanPresenceMintSeams } from "./human-presence-mint.js";
|
|
2
3
|
/** True when `candidate` is the same as or a descendant of `root` after realpath. */
|
|
3
4
|
export declare function isPathInsideRoot(root: string, candidate: string): boolean;
|
|
4
5
|
export interface ParsedArgs {
|
|
@@ -9,6 +10,10 @@ export interface ParsedArgs {
|
|
|
9
10
|
/** Optional override for recorded xbriefRelPath (defaults: pending→active map). */
|
|
10
11
|
xbriefRelPath: string;
|
|
11
12
|
quiet: boolean;
|
|
13
|
+
/** Explicit operator confirm for the #3110 human-presence mint (#3384). */
|
|
14
|
+
confirm: boolean;
|
|
15
|
+
/** Optional owner/name seed for preimage approvedRepos (#3385 R5). */
|
|
16
|
+
repo: string;
|
|
12
17
|
error?: string;
|
|
13
18
|
}
|
|
14
19
|
/**
|
|
@@ -18,5 +23,5 @@ export interface ParsedArgs {
|
|
|
18
23
|
*/
|
|
19
24
|
export declare function resolveApprovalXbriefRelPath(sourceRelOrAbs: string, projectRoot: string, override?: string): string | null;
|
|
20
25
|
export declare function parseArgs(argv: string[]): ParsedArgs;
|
|
21
|
-
export declare function run(argv: string[]): number;
|
|
26
|
+
export declare function run(argv: string[], seams?: HumanPresenceMintSeams): number;
|
|
22
27
|
//# sourceMappingURL=scope-record-approved-scope.d.ts.map
|
|
@@ -9,8 +9,9 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { existsSync, lstatSync, readFileSync, realpathSync } from "node:fs";
|
|
11
11
|
import { basename, join, relative, resolve, sep } from "node:path";
|
|
12
|
-
import { scopeProvenance } from "@deftai/directive-core";
|
|
12
|
+
import { scopeProvenance, slice } from "@deftai/directive-core";
|
|
13
13
|
import { isDirectEntrypoint } from "./entrypoint.js";
|
|
14
|
+
import { refuseMintWhileUatActive, refuseNonInteractiveMint, resolveHumanPresenceMintSeams, } from "./human-presence-mint.js";
|
|
14
15
|
/** True when `candidate` is the same as or a descendant of `root` after realpath. */
|
|
15
16
|
export function isPathInsideRoot(root, candidate) {
|
|
16
17
|
let rootReal;
|
|
@@ -37,13 +38,13 @@ export function isPathInsideRoot(root, candidate) {
|
|
|
37
38
|
return false;
|
|
38
39
|
return !rel.startsWith("..");
|
|
39
40
|
}
|
|
40
|
-
const {
|
|
41
|
+
const { isHumanApprovalStamp, mintApprovedScopeArtifacts } = scopeProvenance;
|
|
41
42
|
function usage() {
|
|
42
|
-
return ("usage: scope:record-approved-scope -- <xbrief-path> --actor <name> " +
|
|
43
|
-
"[--kind operator] [--project-root <dir>] [--xbrief-rel-path <posix>] [--quiet]\n" +
|
|
44
|
-
" Writes .deft/approved-scope/<plan-id>.json
|
|
45
|
-
"
|
|
46
|
-
"
|
|
43
|
+
return ("usage: scope:record-approved-scope -- <xbrief-path> --actor <name> --confirm " +
|
|
44
|
+
"[--kind operator] [--project-root <dir>] [--xbrief-rel-path <posix>] [--repo owner/name] [--quiet]\n" +
|
|
45
|
+
" Writes .deft/approved-scope/<plan-id>.json and <plan-id>.intent.json (#3205 / #3384 / #3385).\n" +
|
|
46
|
+
" --actor is display only and never authorizes mint. Mint requires a real TTY, " +
|
|
47
|
+
"controlling terminal, --confirm, and typed phrase mint (#3110). Agent/CI shells refuse.");
|
|
47
48
|
}
|
|
48
49
|
/**
|
|
49
50
|
* Map pending/ → active/ for path-bound approval records.
|
|
@@ -96,6 +97,8 @@ export function parseArgs(argv) {
|
|
|
96
97
|
kind: "operator",
|
|
97
98
|
xbriefRelPath: "",
|
|
98
99
|
quiet: false,
|
|
100
|
+
confirm: false,
|
|
101
|
+
repo: "",
|
|
99
102
|
};
|
|
100
103
|
const positionals = [];
|
|
101
104
|
for (let i = 0; i < argv.length; i += 1) {
|
|
@@ -106,6 +109,9 @@ export function parseArgs(argv) {
|
|
|
106
109
|
if (arg === "--quiet") {
|
|
107
110
|
parsed.quiet = true;
|
|
108
111
|
}
|
|
112
|
+
else if (arg === "--confirm") {
|
|
113
|
+
parsed.confirm = true;
|
|
114
|
+
}
|
|
109
115
|
else if (arg === "--project-root") {
|
|
110
116
|
const value = argv[i + 1];
|
|
111
117
|
if (value === undefined) {
|
|
@@ -150,6 +156,17 @@ export function parseArgs(argv) {
|
|
|
150
156
|
else if (arg?.startsWith("--xbrief-rel-path=")) {
|
|
151
157
|
parsed.xbriefRelPath = arg.slice("--xbrief-rel-path=".length);
|
|
152
158
|
}
|
|
159
|
+
else if (arg === "--repo") {
|
|
160
|
+
const value = argv[i + 1];
|
|
161
|
+
if (value === undefined) {
|
|
162
|
+
return { ...parsed, error: "argument --repo: expected one argument" };
|
|
163
|
+
}
|
|
164
|
+
parsed.repo = value;
|
|
165
|
+
i += 1;
|
|
166
|
+
}
|
|
167
|
+
else if (arg?.startsWith("--repo=")) {
|
|
168
|
+
parsed.repo = arg.slice("--repo=".length);
|
|
169
|
+
}
|
|
153
170
|
else if (arg?.startsWith("-")) {
|
|
154
171
|
return { ...parsed, error: `unrecognized argument: ${arg}` };
|
|
155
172
|
}
|
|
@@ -172,7 +189,7 @@ export function parseArgs(argv) {
|
|
|
172
189
|
}
|
|
173
190
|
return parsed;
|
|
174
191
|
}
|
|
175
|
-
export function run(argv) {
|
|
192
|
+
export function run(argv, seams = {}) {
|
|
176
193
|
const args = parseArgs(argv);
|
|
177
194
|
if (args.error !== undefined) {
|
|
178
195
|
process.stderr.write(`scope_record_approved_scope: ${args.error}\n`);
|
|
@@ -202,14 +219,27 @@ export function run(argv) {
|
|
|
202
219
|
process.stderr.write(`scope_record_approved_scope: failed to read xBRIEF: ${String(err)}\n`);
|
|
203
220
|
return 2;
|
|
204
221
|
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
}
|
|
209
|
-
catch {
|
|
210
|
-
process.stderr.write("scope_record_approved_scope: xBRIEF is not valid JSON\n");
|
|
222
|
+
const parsed = scopeProvenance.parseJsonRejectingDuplicateKeys(raw);
|
|
223
|
+
if (!parsed.ok) {
|
|
224
|
+
process.stderr.write(`scope_record_approved_scope: ${parsed.error}\n`);
|
|
211
225
|
return 2;
|
|
212
226
|
}
|
|
227
|
+
const payload = parsed.value;
|
|
228
|
+
const verb = "scope:record-approved-scope";
|
|
229
|
+
const uatBlocked = refuseMintWhileUatActive(verb, projectRoot);
|
|
230
|
+
if (uatBlocked !== null)
|
|
231
|
+
return uatBlocked;
|
|
232
|
+
const resolved = resolveHumanPresenceMintSeams(seams);
|
|
233
|
+
const mintBlocked = refuseNonInteractiveMint({
|
|
234
|
+
verb,
|
|
235
|
+
confirm: args.confirm,
|
|
236
|
+
isTty: resolved.isTty,
|
|
237
|
+
environ: resolved.environ,
|
|
238
|
+
hasControllingTerminal: resolved.hasControllingTerminal,
|
|
239
|
+
readInteractiveConfirm: resolved.readInteractiveConfirm,
|
|
240
|
+
});
|
|
241
|
+
if (mintBlocked !== null)
|
|
242
|
+
return mintBlocked;
|
|
213
243
|
const stamp = {
|
|
214
244
|
kind: args.kind.trim(),
|
|
215
245
|
actor: args.actor.trim(),
|
|
@@ -228,22 +258,40 @@ export function run(argv) {
|
|
|
228
258
|
"(repo-relative path required for approval binding) (#3205).\n");
|
|
229
259
|
return 2;
|
|
230
260
|
}
|
|
231
|
-
const
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
261
|
+
const repoSeed = slice.resolveProjectRepo(args.repo.trim().length > 0 ? args.repo.trim() : undefined, projectRoot);
|
|
262
|
+
// Mint writes record + preimage as one fail-closed pair. A dest-write
|
|
263
|
+
// failure restores the prior pair or leaves neither dest (#3385 residual).
|
|
264
|
+
let minted;
|
|
265
|
+
try {
|
|
266
|
+
minted = mintApprovedScopeArtifacts({
|
|
267
|
+
xbriefRelPath,
|
|
268
|
+
payload,
|
|
269
|
+
rawText: raw,
|
|
270
|
+
projectRoot,
|
|
271
|
+
humanApproval: stamp,
|
|
272
|
+
extract: {
|
|
273
|
+
projectRoot,
|
|
274
|
+
approvedReposSeed: repoSeed !== null ? [repoSeed] : [],
|
|
275
|
+
},
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
catch (err) {
|
|
279
|
+
process.stderr.write(`scope_record_approved_scope: ${String(err)}\n`);
|
|
280
|
+
return 1;
|
|
281
|
+
}
|
|
282
|
+
const { record, recordPath, intentPath } = minted;
|
|
238
283
|
if (!args.quiet) {
|
|
239
|
-
process.stdout.write(`scope_record_approved_scope: wrote ${
|
|
284
|
+
process.stdout.write(`scope_record_approved_scope: wrote ${recordPath}\n` +
|
|
285
|
+
` preimage: ${intentPath}\n` +
|
|
240
286
|
` planId: ${record.planId}\n` +
|
|
241
287
|
` xbriefRelPath: ${record.xbriefRelPath}\n` +
|
|
242
288
|
` fileScopeDigest: ${record.fileScopeDigest}\n` +
|
|
289
|
+
` intentDigest: ${record.intentDigest ?? ""}\n` +
|
|
243
290
|
` paths: ${record.fileScope.length}\n` +
|
|
244
291
|
` humanApproval: ${stamp.kind}/${stamp.actor}\n` +
|
|
245
|
-
"
|
|
246
|
-
"
|
|
292
|
+
" Read the preimage before you commit. That file is the approved intent (#3385).\n" +
|
|
293
|
+
" Next: commit record + preimage on the merge base (or a prior PR) before " +
|
|
294
|
+
"activation/expansion in the implementation change set (#3205 / #3385).\n");
|
|
247
295
|
}
|
|
248
296
|
return 0;
|
|
249
297
|
}
|
package/dist/session-start.d.ts
CHANGED
|
@@ -32,6 +32,10 @@ export interface ParsedSessionStartArgs {
|
|
|
32
32
|
* the default; also settable via DEFT_SESSION_COMPACT=1.
|
|
33
33
|
*/
|
|
34
34
|
compact: boolean;
|
|
35
|
+
/** #3433: steal the worktree occupancy lease. */
|
|
36
|
+
steal: boolean;
|
|
37
|
+
confirm: boolean;
|
|
38
|
+
occupant: string | null;
|
|
35
39
|
error?: string;
|
|
36
40
|
}
|
|
37
41
|
/** Parse session:start CLI args, mirroring scripts/session_start.py. */
|
package/dist/session-start.js
CHANGED
|
@@ -31,6 +31,9 @@ export function parseArgs(argv) {
|
|
|
31
31
|
ceremonyDepthOverride: null,
|
|
32
32
|
effortBudgetHost: {},
|
|
33
33
|
compact: false,
|
|
34
|
+
steal: false,
|
|
35
|
+
confirm: false,
|
|
36
|
+
occupant: null,
|
|
34
37
|
};
|
|
35
38
|
const dialInputs = {};
|
|
36
39
|
for (let i = 0; i < argv.length; i += 1) {
|
|
@@ -273,6 +276,23 @@ export function parseArgs(argv) {
|
|
|
273
276
|
// #3286: terse machine orientation output (opt-in; verbose remains default)
|
|
274
277
|
parsed.compact = true;
|
|
275
278
|
}
|
|
279
|
+
else if (arg === "--steal") {
|
|
280
|
+
parsed.steal = true;
|
|
281
|
+
}
|
|
282
|
+
else if (arg === "--confirm") {
|
|
283
|
+
parsed.confirm = true;
|
|
284
|
+
}
|
|
285
|
+
else if (arg === "--occupant") {
|
|
286
|
+
const value = argv[i + 1];
|
|
287
|
+
if (value === undefined) {
|
|
288
|
+
return { ...parsed, error: "argument --occupant: expected one argument" };
|
|
289
|
+
}
|
|
290
|
+
parsed.occupant = value;
|
|
291
|
+
i += 1;
|
|
292
|
+
}
|
|
293
|
+
else if (arg?.startsWith("--occupant=")) {
|
|
294
|
+
parsed.occupant = arg.slice("--occupant=".length);
|
|
295
|
+
}
|
|
276
296
|
else {
|
|
277
297
|
return { ...parsed, error: `unrecognized argument: ${arg}` };
|
|
278
298
|
}
|
|
@@ -335,6 +355,9 @@ export function run(argv) {
|
|
|
335
355
|
effortBudgetSeams: { hostDescriptor, environ: process.env },
|
|
336
356
|
// #3286: --compact flag; DEFT_SESSION_COMPACT still resolved inside core.
|
|
337
357
|
compact: args.compact ? true : undefined,
|
|
358
|
+
steal: args.steal ? true : undefined,
|
|
359
|
+
confirm: args.confirm ? true : undefined,
|
|
360
|
+
occupant: args.occupant ?? undefined,
|
|
338
361
|
...(args.ceremonyDepthOverride !== null
|
|
339
362
|
? {
|
|
340
363
|
ceremonyDial: selectCeremonyDepth({
|
package/dist/verify-ac.d.ts
CHANGED
|
@@ -20,6 +20,8 @@ export type FindActiveXbriefResult = {
|
|
|
20
20
|
readonly paths: readonly string[];
|
|
21
21
|
readonly dir: string;
|
|
22
22
|
};
|
|
23
|
+
/** Three-state exit: pass=0, fail=1, config=2. A printed pass must not leak 201. */
|
|
24
|
+
export declare function clampVerifyAcExit(ok: boolean, code: number): number;
|
|
23
25
|
/** Run the gate and return the process exit code. */
|
|
24
26
|
export declare function run(argv: string[]): number;
|
|
25
27
|
export {};
|
package/dist/verify-ac.js
CHANGED
|
@@ -11,7 +11,7 @@ import { join, resolve } from "node:path";
|
|
|
11
11
|
import { fileURLToPath } from "node:url";
|
|
12
12
|
import { resolveSessionCompletedVerifyAcTarget } from "@deftai/directive-core/check";
|
|
13
13
|
import { formatRejectedLedger, resolveLiteralAcceptanceDetailed, } from "@deftai/directive-core/literal-acceptance";
|
|
14
|
-
import { emitVerifyAcTerminalOutcome, evaluateVerifyAcFromPath, readPlanAcceptance, } from "@deftai/directive-core/product-first-done-gate";
|
|
14
|
+
import { emitVerifyAcTerminalOutcome, evaluateVerifyAcFromPath, readPlanAcceptance, resolveAcceptanceGateProfile, } from "@deftai/directive-core/product-first-done-gate";
|
|
15
15
|
/** Parse verify:ac CLI args. */
|
|
16
16
|
export function parseArgs(argv) {
|
|
17
17
|
const parsed = {
|
|
@@ -103,9 +103,20 @@ function listActiveXbriefs(projectRoot) {
|
|
|
103
103
|
dir: dirs.join(" + "),
|
|
104
104
|
};
|
|
105
105
|
}
|
|
106
|
+
/** Three-state exit: pass=0, fail=1, config=2. A printed pass must not leak 201. */
|
|
107
|
+
export function clampVerifyAcExit(ok, code) {
|
|
108
|
+
if (ok)
|
|
109
|
+
return 0;
|
|
110
|
+
if (code === 2)
|
|
111
|
+
return 2;
|
|
112
|
+
return 1;
|
|
113
|
+
}
|
|
106
114
|
/** Evaluate one or many xBRIEF paths; return worst non-zero code (fail closed). */
|
|
107
115
|
function evaluatePaths(paths, options) {
|
|
108
116
|
let worst = 0;
|
|
117
|
+
// One shared option profile per reader (#3497) — scope:complete resolves its own
|
|
118
|
+
// profile from the same table, so the two readers cannot drift apart again.
|
|
119
|
+
const profile = resolveAcceptanceGateProfile(options.softMissingXbrief ? "check" : "standalone");
|
|
109
120
|
for (const path of paths) {
|
|
110
121
|
if (!options.quiet && paths.length > 1) {
|
|
111
122
|
process.stdout.write(`verify:ac — evaluating ${path}\n`);
|
|
@@ -114,7 +125,9 @@ function evaluatePaths(paths, options) {
|
|
|
114
125
|
projectRoot: options.projectRoot,
|
|
115
126
|
quiet: options.quiet,
|
|
116
127
|
softMissingXbrief: options.softMissingXbrief,
|
|
117
|
-
checkIntegrated:
|
|
128
|
+
checkIntegrated: profile.checkIntegrated,
|
|
129
|
+
captureFromNarratives: profile.captureFromNarratives,
|
|
130
|
+
reuseMode: profile.reuseMode,
|
|
118
131
|
env: process.env,
|
|
119
132
|
});
|
|
120
133
|
if (result.message.length > 0) {
|
|
@@ -125,9 +138,10 @@ function evaluatePaths(paths, options) {
|
|
|
125
138
|
process.stderr.write(`${result.message}\n`);
|
|
126
139
|
}
|
|
127
140
|
}
|
|
128
|
-
|
|
141
|
+
const code = clampVerifyAcExit(result.ok, result.code);
|
|
142
|
+
if (code !== 0 && (worst === 0 || code > worst)) {
|
|
129
143
|
// Prefer code 1 (fail) over 2 when both present — still non-zero.
|
|
130
|
-
worst =
|
|
144
|
+
worst = code;
|
|
131
145
|
}
|
|
132
146
|
}
|
|
133
147
|
return worst;
|
|
@@ -242,14 +256,24 @@ export function run(argv) {
|
|
|
242
256
|
}
|
|
243
257
|
const acceptance = readPlanAcceptance(plan);
|
|
244
258
|
const resolved = resolveLiteralAcceptanceDetailed(plan, { captureFromNarratives: true });
|
|
259
|
+
// Executor reads plan.acceptance.commands when the #3267 ledger is empty (#3449).
|
|
260
|
+
const executorCommands = resolved.commands.length > 0
|
|
261
|
+
? resolved.commands
|
|
262
|
+
: acceptance.commands.map((c) => ({
|
|
263
|
+
command: c.command,
|
|
264
|
+
source: "explicit",
|
|
265
|
+
sourceSpan: "plan.acceptance.commands",
|
|
266
|
+
cwd: c.cwd ?? null,
|
|
267
|
+
expectedExitCode: c.expectedExitCode ?? 0,
|
|
268
|
+
}));
|
|
245
269
|
reports.push({
|
|
246
270
|
xbrief: xbriefPath,
|
|
247
271
|
source_rung: acceptance.source_rung,
|
|
248
272
|
none_stated: acceptance.none_stated,
|
|
249
273
|
acceptance_commands: acceptance.commands,
|
|
250
|
-
count:
|
|
274
|
+
count: executorCommands.length,
|
|
251
275
|
rejected_count: resolved.rejected.length,
|
|
252
|
-
commands:
|
|
276
|
+
commands: executorCommands.map((c) => ({
|
|
253
277
|
command: c.command,
|
|
254
278
|
source: c.source,
|
|
255
279
|
sourceSpan: c.sourceSpan ?? null,
|
|
@@ -3,11 +3,12 @@ interface ParsedArgs {
|
|
|
3
3
|
projectRoot: string;
|
|
4
4
|
repo: string | null;
|
|
5
5
|
tip: string | null;
|
|
6
|
+
issue: number | null;
|
|
6
7
|
quiet: boolean;
|
|
7
8
|
skipGh: boolean;
|
|
8
9
|
error?: string;
|
|
9
10
|
}
|
|
10
|
-
/** Parse verify-completed-tracked CLI args (#3264). */
|
|
11
|
+
/** Parse verify-completed-tracked CLI args (#3264 / #3476). */
|
|
11
12
|
export declare function parseArgs(argv: string[]): ParsedArgs;
|
|
12
13
|
/** Run the gate and return the process exit code. */
|
|
13
14
|
export declare function run(argv: string[]): number;
|
|
@@ -2,12 +2,21 @@
|
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
import { evaluateCompletedTracked } from "@deftai/directive-core/lifecycle";
|
|
5
|
-
|
|
5
|
+
function parseIssueNumber(raw) {
|
|
6
|
+
const trimmed = raw.startsWith("#") ? raw.slice(1) : raw;
|
|
7
|
+
if (!/^\d+$/.test(trimmed)) {
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
10
|
+
const value = Number(trimmed);
|
|
11
|
+
return Number.isInteger(value) && value > 0 ? value : null;
|
|
12
|
+
}
|
|
13
|
+
/** Parse verify-completed-tracked CLI args (#3264 / #3476). */
|
|
6
14
|
export function parseArgs(argv) {
|
|
7
15
|
const parsed = {
|
|
8
16
|
projectRoot: ".",
|
|
9
17
|
repo: null,
|
|
10
18
|
tip: null,
|
|
19
|
+
issue: null,
|
|
11
20
|
quiet: false,
|
|
12
21
|
skipGh: false,
|
|
13
22
|
};
|
|
@@ -52,6 +61,26 @@ export function parseArgs(argv) {
|
|
|
52
61
|
else if (arg?.startsWith("--tip=")) {
|
|
53
62
|
parsed.tip = arg.slice("--tip=".length);
|
|
54
63
|
}
|
|
64
|
+
else if (arg === "--issue") {
|
|
65
|
+
const value = argv[i + 1];
|
|
66
|
+
if (value === undefined) {
|
|
67
|
+
return { ...parsed, error: "argument --issue: expected one argument" };
|
|
68
|
+
}
|
|
69
|
+
const issue = parseIssueNumber(value);
|
|
70
|
+
if (issue === null) {
|
|
71
|
+
return { ...parsed, error: `argument --issue: expected a positive integer, got ${value}` };
|
|
72
|
+
}
|
|
73
|
+
parsed.issue = issue;
|
|
74
|
+
i += 1;
|
|
75
|
+
}
|
|
76
|
+
else if (arg?.startsWith("--issue=")) {
|
|
77
|
+
const value = arg.slice("--issue=".length);
|
|
78
|
+
const issue = parseIssueNumber(value);
|
|
79
|
+
if (issue === null) {
|
|
80
|
+
return { ...parsed, error: `argument --issue: expected a positive integer, got ${value}` };
|
|
81
|
+
}
|
|
82
|
+
parsed.issue = issue;
|
|
83
|
+
}
|
|
55
84
|
else {
|
|
56
85
|
return { ...parsed, error: `unrecognized argument: ${arg}` };
|
|
57
86
|
}
|
|
@@ -70,6 +99,7 @@ export function run(argv) {
|
|
|
70
99
|
quiet: args.quiet,
|
|
71
100
|
repo: args.repo,
|
|
72
101
|
tip: args.tip,
|
|
102
|
+
issue: args.issue,
|
|
73
103
|
skipGh: args.skipGh,
|
|
74
104
|
});
|
|
75
105
|
if (result.message.length > 0) {
|
|
@@ -128,6 +128,13 @@ export function run(argv) {
|
|
|
128
128
|
reason: r.reason,
|
|
129
129
|
sourceSpan: r.sourceSpan ?? null,
|
|
130
130
|
})),
|
|
131
|
+
// Prose-derived captures demoted by structured acceptance commands (#3484).
|
|
132
|
+
advisory_rejected_count: resolved.advisoryRejected.length,
|
|
133
|
+
advisory_rejected: resolved.advisoryRejected.map((r) => ({
|
|
134
|
+
command: r.command,
|
|
135
|
+
reason: r.reason,
|
|
136
|
+
sourceSpan: r.sourceSpan ?? null,
|
|
137
|
+
})),
|
|
131
138
|
};
|
|
132
139
|
process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
|
|
133
140
|
if (resolved.rejected.length > 0) {
|