@deftai/directive 0.119.8 → 0.119.10

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.
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import { resolve } from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
- import { evaluateReviewMonitorGate, REVIEW_MONITOR_HELP, verifyResultToJson, } from "@deftai/directive-core/review-monitor";
4
+ import { bindLivePhaseCorrectWait, evaluateMergePathArm, } from "@deftai/directive-core/dist/pr-watch/main.js";
5
+ import { EXIT_CONFIG_ERROR, EXIT_NOT_READY, evaluateReviewMonitorGate, isTier1, REVIEW_MONITOR_HELP, verifyResultToJson, } from "@deftai/directive-core/review-monitor";
5
6
  const CALL_SITES = new Set([
6
7
  "solo",
7
8
  "swarm-phase5-6",
@@ -18,6 +19,10 @@ export function parseVerifyReviewMonitorArgs(argv) {
18
19
  approach3: false,
19
20
  approach3Warned: false,
20
21
  emitJson: false,
22
+ mergePathArm: false,
23
+ liveWait: false,
24
+ explicitFinish: false,
25
+ stickyLease: false,
21
26
  help: false,
22
27
  };
23
28
  for (let i = 0; i < argv.length; i += 1) {
@@ -28,6 +33,18 @@ export function parseVerifyReviewMonitorArgs(argv) {
28
33
  if (arg === "--json") {
29
34
  acc.emitJson = true;
30
35
  }
36
+ else if (arg === "--merge-path-arm") {
37
+ acc.mergePathArm = true;
38
+ }
39
+ else if (arg === "--live-wait") {
40
+ acc.liveWait = true;
41
+ }
42
+ else if (arg === "--explicit-finish") {
43
+ acc.explicitFinish = true;
44
+ }
45
+ else if (arg === "--sticky-lease") {
46
+ acc.stickyLease = true;
47
+ }
31
48
  else if (arg === "--approach3") {
32
49
  acc.approach3 = true;
33
50
  }
@@ -117,6 +134,15 @@ export function run(argv) {
117
134
  const args = parseVerifyReviewMonitorArgs(argv);
118
135
  if (args.help) {
119
136
  process.stdout.write(REVIEW_MONITOR_HELP);
137
+ process.stdout.write("\n#4882 / #5020 merge-path arm observer (optional):\n" +
138
+ " --merge-path-arm Fail closed when neither live wait nor explicit finish\n" +
139
+ " --live-wait Require still-running wait evidence for this PR\n" +
140
+ " (Tier 1: lease + active polling heartbeat with pid liveness;\n" +
141
+ " lease+flag alone is not armed — #5020)\n" +
142
+ " --explicit-finish Attest option-C BLOCKED/FAILED finish for this PR\n" +
143
+ " --sticky-lease Attest a fresh sticky lease (not sufficient alone)\n" +
144
+ " Prefer Approach 1 / native pr:watch or post-CLEAN pr:wait-mergeable-and-merge;\n" +
145
+ " homemade line-parsed --json is not an arm.\n");
120
146
  return 0;
121
147
  }
122
148
  if (args.error !== undefined) {
@@ -129,6 +155,8 @@ export function run(argv) {
129
155
  process.stderr.write("Try: task verify:review-monitor -- --help\n");
130
156
  return 2;
131
157
  }
158
+ // Always evaluate the gate first so --project-root / repo config errors stay
159
+ // exit 2 even when --merge-path-arm would otherwise fail closed as unarmed.
132
160
  const result = evaluateReviewMonitorGate({
133
161
  pr: args.pr,
134
162
  projectRoot: resolve(args.projectRoot),
@@ -139,15 +167,78 @@ export function run(argv) {
139
167
  approach3Warned: args.approach3Warned,
140
168
  environ: process.env,
141
169
  });
170
+ let arm = null;
171
+ if (args.mergePathArm) {
172
+ // Bind --live-wait to lease (#5018) + process-liveness heartbeat (#5020) on Tier 1.
173
+ const leaseEvidence = result.monitorRecord !== null;
174
+ const liveBind = bindLivePhaseCorrectWait({
175
+ liveWaitFlag: args.liveWait,
176
+ tierIs1: isTier1(result.tier),
177
+ leaseEvidence,
178
+ heartbeatActive: result.heartbeatActive,
179
+ pr: args.pr,
180
+ });
181
+ arm = evaluateMergePathArm({
182
+ livePhaseCorrectWait: liveBind.livePhaseCorrectWait,
183
+ explicitFinish: args.explicitFinish,
184
+ stickyLeaseActive: args.stickyLease || leaseEvidence,
185
+ });
186
+ if (args.liveWait &&
187
+ !liveBind.livePhaseCorrectWait &&
188
+ !args.explicitFinish &&
189
+ !arm.armed &&
190
+ liveBind.message !== null) {
191
+ arm = {
192
+ armed: false,
193
+ reason: "unarmed_stand_down",
194
+ message: liveBind.message,
195
+ };
196
+ }
197
+ }
142
198
  if (args.emitJson) {
143
- process.stdout.write(`${JSON.stringify(verifyResultToJson(result), null, 2)}\n`);
199
+ const payload = verifyResultToJson(result);
200
+ if (arm !== null) {
201
+ payload.merge_path_arm = {
202
+ armed: arm.armed,
203
+ reason: arm.reason,
204
+ message: arm.message,
205
+ live_wait: args.liveWait,
206
+ explicit_finish: args.explicitFinish,
207
+ sticky_lease: args.stickyLease,
208
+ lease_evidence: result.monitorRecord !== null,
209
+ heartbeat_active: result.heartbeatActive,
210
+ };
211
+ // Combined gate+arm: unarmed fails closed even when the monitor gate is ready.
212
+ if (!arm.armed && result.exitCode !== EXIT_CONFIG_ERROR) {
213
+ payload.ready = false;
214
+ payload.exit_code = EXIT_NOT_READY;
215
+ // Gate-ready → arm diagnosis; both failing → keep both messages.
216
+ payload.message = result.exitCode === 0 ? arm.message : `${result.message}\n${arm.message}`;
217
+ }
218
+ }
219
+ process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
220
+ }
221
+ else if (result.exitCode === EXIT_CONFIG_ERROR) {
222
+ process.stderr.write(`${result.message}\n`);
223
+ }
224
+ else if (arm !== null && !arm.armed) {
225
+ process.stderr.write(`${arm.message}\n`);
144
226
  }
145
227
  else if (result.exitCode === 0) {
146
228
  process.stdout.write(`${result.message}\n`);
229
+ if (arm !== null) {
230
+ process.stdout.write(`${arm.message}\n`);
231
+ }
147
232
  }
148
233
  else {
149
234
  process.stderr.write(`${result.message}\n`);
150
235
  }
236
+ if (result.exitCode === EXIT_CONFIG_ERROR) {
237
+ return EXIT_CONFIG_ERROR;
238
+ }
239
+ if (arm !== null && !arm.armed) {
240
+ return EXIT_NOT_READY;
241
+ }
151
242
  return result.exitCode;
152
243
  }
153
244
  if (process.argv[1] !== undefined && fileURLToPath(import.meta.url) === process.argv[1]) {
@@ -1,6 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * CLI for verify:scope-provenance (#3145).
3
+ * CLI for verify:scope-provenance (#3145 / #4956 / #4774).
4
+ *
5
+ * Membership against the declared allowlist (mint / brief file_scope) is
6
+ * evaluated inside evaluateScopeProvenance; this CLI only forwards
7
+ * --base-ref / --enforce.
4
8
  */
5
9
  import { resolve } from "node:path";
6
10
  import { scopeProvenance } from "@deftai/directive-core";
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { resolve } from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
- import { emitBypassWarning, emitVerifyJson, formatCacheFreshDeferSoftPath, formatRitualRecoveryInstruction, verifySessionRitual, } from "@deftai/directive-core/session";
4
+ import { emitBypassWarning, emitVerifyJson, formatCacheFreshDeferSoftPath, occupancyAwareDenialMessage, verifySessionRitual, } from "@deftai/directive-core/session";
5
5
  function parsePosture(value) {
6
6
  if (value === "read-only")
7
7
  return "read-only";
@@ -121,8 +121,10 @@ export function run(argv, deps = {}) {
121
121
  }
122
122
  }
123
123
  else if (result.code === 1) {
124
- const recovery = formatRitualRecoveryInstruction(result.recoveryTier ?? "cold");
125
- const lines = [result.message, recovery];
124
+ const recoveryTier = result.recoveryTier ?? "cold";
125
+ const lines = [
126
+ occupancyAwareDenialMessage(projectRoot, result.message, { env: process.env }, recoveryTier),
127
+ ];
126
128
  if (isCacheFreshFailureMessage(result.message)) {
127
129
  lines.push(formatCacheFreshDeferSoftPath());
128
130
  }
@@ -0,0 +1,39 @@
1
+ /** Fallback when package.json cannot be read. Keep aligned with the bin map. */
2
+ export declare const PACKAGE_BIN_NAMES: readonly ["directive", "deft", "deft-ts", "deft-hook", "deft-verify-encoding"];
3
+ /** Node owns these shims. This package must not delete them. */
4
+ export declare const NODE_OWNED_SHIM_NAMES: readonly ["npx", "npm"];
5
+ export interface Ps1ShimRemoval {
6
+ readonly removed: readonly string[];
7
+ readonly failed: readonly string[];
8
+ }
9
+ export declare function packageIsUnderNodeModules(pkgDir: string): boolean;
10
+ /** bin-links layout. Valid only when {@link packageIsUnderNodeModules} is true. */
11
+ export declare function nodeModulesDir(pkgDir: string): string;
12
+ /** Same directory bin-links uses for this package root. */
13
+ export declare function linkerBinDir(opts: {
14
+ readonly pkgDir: string;
15
+ readonly global: boolean;
16
+ readonly platform: string;
17
+ }): string;
18
+ export declare function isGlobalInstall(env: Readonly<Record<string, string | undefined>>): boolean;
19
+ export declare function candidateBinDirs(opts: {
20
+ readonly pkgDir: string;
21
+ readonly env: Readonly<Record<string, string | undefined>>;
22
+ readonly platform: string;
23
+ }): string[];
24
+ export declare function removeGeneratedPs1Shims(opts: {
25
+ readonly binDir: string;
26
+ readonly binNames: readonly string[];
27
+ readonly platform: string;
28
+ readonly protectedNames?: readonly string[];
29
+ }): Ps1ShimRemoval;
30
+ /** True when cmd-shim text records a target path inside this install. */
31
+ export declare function cmdShimTargetsPackage(cmdText: string, binDir: string, pkgDir: string): boolean;
32
+ export declare function readPackageBinNames(packageJsonText: string): string[];
33
+ export declare function removeInstalledWindowsPs1Shims(opts: {
34
+ readonly pkgDir: string;
35
+ readonly env: Readonly<Record<string, string | undefined>>;
36
+ readonly platform: string;
37
+ readonly readText?: (path: string) => string;
38
+ }): Ps1ShimRemoval;
39
+ //# sourceMappingURL=windows-bin-ps1.d.ts.map
@@ -0,0 +1,224 @@
1
+ /**
2
+ * npm's Windows bin linker (cmd-shim) always writes a .ps1 beside the .cmd.
3
+ * PowerShell selects that .ps1 and stops when script execution is refused, so
4
+ * the bare name never reaches the .cmd. This runs from postinstall, after
5
+ * that link, and removes only this package's generated .ps1 files.
6
+ * A .ps1 is removed only when its sibling .cmd records a target inside this
7
+ * install, so an ambient PNPM_HOME from another install is left alone.
8
+ * It does not change npx.ps1, npm.ps1, or the operator's execution policy.
9
+ */
10
+ import { existsSync, readFileSync, realpathSync, unlinkSync } from "node:fs";
11
+ import { basename, dirname, join, win32 } from "node:path";
12
+ /** Fallback when package.json cannot be read. Keep aligned with the bin map. */
13
+ export const PACKAGE_BIN_NAMES = [
14
+ "directive",
15
+ "deft",
16
+ "deft-ts",
17
+ "deft-hook",
18
+ "deft-verify-encoding",
19
+ ];
20
+ /** Node owns these shims. This package must not delete them. */
21
+ export const NODE_OWNED_SHIM_NAMES = ["npx", "npm"];
22
+ export function packageIsUnderNodeModules(pkgDir) {
23
+ const parent = dirname(pkgDir);
24
+ if (basename(parent) === "node_modules")
25
+ return true;
26
+ return basename(dirname(parent)) === "node_modules";
27
+ }
28
+ /** bin-links layout. Valid only when {@link packageIsUnderNodeModules} is true. */
29
+ export function nodeModulesDir(pkgDir) {
30
+ const scopeOrNm = dirname(pkgDir);
31
+ if (basename(scopeOrNm) === "node_modules")
32
+ return scopeOrNm;
33
+ return dirname(scopeOrNm);
34
+ }
35
+ /** Same directory bin-links uses for this package root. */
36
+ export function linkerBinDir(opts) {
37
+ const nm = nodeModulesDir(opts.pkgDir);
38
+ if (!opts.global)
39
+ return join(nm, ".bin");
40
+ const prefix = dirname(nm);
41
+ if (opts.platform === "win32")
42
+ return prefix;
43
+ return join(prefix, "bin");
44
+ }
45
+ export function isGlobalInstall(env) {
46
+ return env.npm_config_global === "true";
47
+ }
48
+ function dirKey(dir, platform) {
49
+ const norm = dir.replace(/[\\/]+/g, "/");
50
+ if (platform === "win32")
51
+ return norm.toLowerCase();
52
+ return norm;
53
+ }
54
+ function ancestorBinDirs(pkgDir, stopDir, platform) {
55
+ const stop = dirKey(stopDir, platform);
56
+ const start = dirKey(pkgDir, platform);
57
+ const stopPrefix = stop.endsWith("/") ? stop : `${stop}/`;
58
+ if (start !== stop && !start.startsWith(stopPrefix)) {
59
+ return [join(stopDir, "node_modules", ".bin")];
60
+ }
61
+ const out = [];
62
+ let dir = pkgDir;
63
+ for (;;) {
64
+ out.push(join(dir, "node_modules", ".bin"));
65
+ if (dirKey(dir, platform) === stop)
66
+ break;
67
+ const parent = dirname(dir);
68
+ if (parent === dir)
69
+ break;
70
+ dir = parent;
71
+ }
72
+ return out;
73
+ }
74
+ export function candidateBinDirs(opts) {
75
+ const global = isGlobalInstall(opts.env);
76
+ const dirs = [];
77
+ if (packageIsUnderNodeModules(opts.pkgDir)) {
78
+ dirs.push(linkerBinDir({ pkgDir: opts.pkgDir, global, platform: opts.platform }));
79
+ }
80
+ if (global) {
81
+ const prefix = opts.env.npm_config_prefix;
82
+ if (prefix !== undefined && prefix.length > 0) {
83
+ dirs.push(opts.platform === "win32" ? prefix : join(prefix, "bin"));
84
+ }
85
+ // Candidate only. Removal still requires the .cmd to target this install.
86
+ const pnpmHome = opts.env.PNPM_HOME;
87
+ if (pnpmHome !== undefined && pnpmHome.length > 0)
88
+ dirs.push(pnpmHome);
89
+ }
90
+ else {
91
+ const localPrefix = opts.env.npm_config_local_prefix;
92
+ if (localPrefix !== undefined && localPrefix.length > 0) {
93
+ dirs.push(...ancestorBinDirs(opts.pkgDir, localPrefix, opts.platform));
94
+ }
95
+ }
96
+ const seen = new Set();
97
+ const out = [];
98
+ for (const dir of dirs) {
99
+ const key = dirKey(dir, opts.platform);
100
+ if (seen.has(key))
101
+ continue;
102
+ seen.add(key);
103
+ out.push(dir);
104
+ }
105
+ return out;
106
+ }
107
+ function isSafeBinName(name) {
108
+ if (name.length === 0)
109
+ return false;
110
+ if (name.includes("/") || name.includes("\\") || name.includes(".."))
111
+ return false;
112
+ return true;
113
+ }
114
+ export function removeGeneratedPs1Shims(opts) {
115
+ if (opts.platform !== "win32")
116
+ return { removed: [], failed: [] };
117
+ const protect = new Set((opts.protectedNames ?? NODE_OWNED_SHIM_NAMES).map((name) => name.toLowerCase()));
118
+ const removed = [];
119
+ const failed = [];
120
+ for (const name of opts.binNames) {
121
+ if (!isSafeBinName(name) || protect.has(name.toLowerCase()))
122
+ continue;
123
+ const ps1 = join(opts.binDir, `${name}.ps1`);
124
+ const cmd = join(opts.binDir, `${name}.cmd`);
125
+ if (!existsSync(cmd) || !existsSync(ps1))
126
+ continue;
127
+ try {
128
+ unlinkSync(ps1);
129
+ removed.push(ps1);
130
+ }
131
+ catch {
132
+ failed.push(ps1);
133
+ }
134
+ }
135
+ return { removed, failed };
136
+ }
137
+ function normalizedWinPath(p) {
138
+ const norm = win32.normalize(p).replace(/[\\/]+/g, "\\");
139
+ const trimmed = norm.length > 3 && norm.endsWith("\\") ? norm.slice(0, -1) : norm;
140
+ return trimmed.toLowerCase();
141
+ }
142
+ function pathIsInside(root, target) {
143
+ const rootKey = normalizedWinPath(root);
144
+ const targetKey = normalizedWinPath(target);
145
+ return targetKey === rootKey || targetKey.startsWith(`${rootKey}\\`);
146
+ }
147
+ /** cmd-shim text is Windows-shaped. realpath needs host separators or a junction never matches. */
148
+ function hostPath(p) {
149
+ return p.replace(/\\/g, "/");
150
+ }
151
+ function realpathIsInside(root, target) {
152
+ try {
153
+ return pathIsInside(realpathSync(hostPath(root)), realpathSync(hostPath(target)));
154
+ }
155
+ catch {
156
+ return false;
157
+ }
158
+ }
159
+ /** True when cmd-shim text records a target path inside this install. */
160
+ export function cmdShimTargetsPackage(cmdText, binDir, pkgDir) {
161
+ const re = /%(?:dp0%|~dp0)(?:\\+|\/+)([^"\r\n\s]+)/gi;
162
+ for (const match of cmdText.matchAll(re)) {
163
+ const rel = match[1];
164
+ if (rel === undefined || rel.length === 0)
165
+ continue;
166
+ const resolved = win32.normalize(win32.join(binDir, rel.replace(/\//g, "\\")));
167
+ if (pathIsInside(pkgDir, resolved) || realpathIsInside(pkgDir, resolved))
168
+ return true;
169
+ }
170
+ return false;
171
+ }
172
+ function cmdLinksCurrentInstall(binDir, name, pkgDir) {
173
+ if (!isSafeBinName(name))
174
+ return false;
175
+ let text;
176
+ try {
177
+ text = readFileSync(join(binDir, `${name}.cmd`), "utf8");
178
+ }
179
+ catch {
180
+ return false;
181
+ }
182
+ return cmdShimTargetsPackage(text, binDir, pkgDir);
183
+ }
184
+ export function readPackageBinNames(packageJsonText) {
185
+ let parsed;
186
+ try {
187
+ parsed = JSON.parse(packageJsonText);
188
+ }
189
+ catch {
190
+ return [];
191
+ }
192
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed))
193
+ return [];
194
+ const bin = parsed.bin;
195
+ if (bin === null || typeof bin !== "object" || Array.isArray(bin))
196
+ return [];
197
+ return Object.keys(bin);
198
+ }
199
+ export function removeInstalledWindowsPs1Shims(opts) {
200
+ if (opts.platform !== "win32")
201
+ return { removed: [], failed: [] };
202
+ const readText = opts.readText ?? ((path) => readFileSync(path, "utf8"));
203
+ let names;
204
+ try {
205
+ names = readPackageBinNames(readText(join(opts.pkgDir, "package.json")));
206
+ }
207
+ catch {
208
+ names = PACKAGE_BIN_NAMES;
209
+ }
210
+ const removed = [];
211
+ const failed = [];
212
+ for (const binDir of candidateBinDirs(opts)) {
213
+ const linked = names.filter((name) => cmdLinksCurrentInstall(binDir, name, opts.pkgDir));
214
+ const result = removeGeneratedPs1Shims({
215
+ binDir,
216
+ binNames: linked,
217
+ platform: opts.platform,
218
+ });
219
+ removed.push(...result.removed);
220
+ failed.push(...result.failed);
221
+ }
222
+ return { removed, failed };
223
+ }
224
+ //# sourceMappingURL=windows-bin-ps1.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deftai/directive",
3
- "version": "0.119.8",
3
+ "version": "0.119.10",
4
4
  "description": "Directive CLI — npm install path for the Deft Directive framework.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://deftai.github.io/directive/",
@@ -22,7 +22,8 @@
22
22
  },
23
23
  "files": [
24
24
  "dist/**/*.js",
25
- "dist/**/*.d.ts"
25
+ "dist/**/*.d.ts",
26
+ "scripts/remove-windows-ps1-shims.mjs"
26
27
  ],
27
28
  "repository": {
28
29
  "type": "git",
@@ -34,10 +35,11 @@
34
35
  "provenance": true
35
36
  },
36
37
  "dependencies": {
37
- "@deftai/directive-core": "^0.119.8",
38
- "@deftai/directive-content": "^0.119.8"
38
+ "@deftai/directive-core": "^0.119.10",
39
+ "@deftai/directive-content": "^0.119.10"
39
40
  },
40
41
  "scripts": {
41
- "build": "tsc -b"
42
+ "build": "tsc -b",
43
+ "postinstall": "node ./scripts/remove-windows-ps1-shims.mjs"
42
44
  }
43
45
  }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * postinstall entry (#4654). The published tarball includes dist/.
3
+ * A workspace install before tsc has no linked shims yet, so a missing
4
+ * dist file is a no-op rather than a failed install.
5
+ */
6
+ import { existsSync } from "node:fs";
7
+ import { dirname, join } from "node:path";
8
+ import { fileURLToPath, pathToFileURL } from "node:url";
9
+
10
+ const pkgDir = dirname(dirname(fileURLToPath(import.meta.url)));
11
+ const entry = join(pkgDir, "dist", "windows-bin-ps1.js");
12
+
13
+ if (existsSync(entry)) {
14
+ const mod = await import(pathToFileURL(entry).href);
15
+ const result = mod.removeInstalledWindowsPs1Shims({
16
+ pkgDir,
17
+ env: process.env,
18
+ platform: process.platform,
19
+ });
20
+ if (result.failed.length > 0) {
21
+ process.stderr.write(
22
+ `remove-windows-ps1-shims: could not remove ${result.failed.join(", ")}\n`,
23
+ );
24
+ process.exitCode = 1;
25
+ }
26
+ }