arkgate 4.6.6 → 4.6.7

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.
@@ -0,0 +1,173 @@
1
+ /**
2
+ * ark-check CLI flag parsing.
3
+ */
4
+ import path from 'node:path';
5
+ import { discoverLocalBaseRef, normalizePolicyBaseRef } from './policy-delta-io.mjs';
6
+
7
+ export function resolveDesignDeltaBaseRef(root, explicit, env = process.env) {
8
+ const flag = typeof explicit === 'string' ? explicit.trim() : '';
9
+ if (flag) return flag;
10
+ const envRef = normalizePolicyBaseRef(env.ARK_POLICY_BASE_REF);
11
+ if (envRef) return envRef;
12
+ const githubBase = typeof env.GITHUB_BASE_REF === 'string' ? env.GITHUB_BASE_REF.trim() : '';
13
+ if (githubBase) return `origin/${githubBase}`;
14
+ return discoverLocalBaseRef(root) || undefined;
15
+ }
16
+
17
+ export function parseArgs(argv) {
18
+ const args = {
19
+ root: process.cwd(),
20
+ config: 'ark.config.json',
21
+ manifest: undefined,
22
+ printConfig: undefined,
23
+ tsconfig: undefined,
24
+ json: false,
25
+ strictConfig: false,
26
+ strictMerge: false,
27
+ requireGates: false,
28
+ requireWriteHook: undefined,
29
+ init: false,
30
+ installAgentGates: false,
31
+ compact: false,
32
+ tools: undefined,
33
+ force: false,
34
+ skillsOnly: false,
35
+ baseline: undefined,
36
+ policyBase: undefined,
37
+ policyBaseRef: undefined,
38
+ policyAck: undefined, failOnNewSmells: false, baseRef: undefined,
39
+ contractSession: false,
40
+ contractDiff: false,
41
+ changed: false,
42
+ against: undefined,
43
+ base: undefined,
44
+ persona: undefined,
45
+ author: undefined,
46
+ failUngoverned: false,
47
+ updateBaseline: false,
48
+ noCache: false,
49
+ resident: false,
50
+ coverage: false,
51
+ migrateCommands: false,
52
+ doctor: false,
53
+ plan: false,
54
+ recommend: false,
55
+ writePlan: false,
56
+ listPolicyPacks: false,
57
+ applyPolicyPack: undefined,
58
+ watch: false,
59
+ beginner: false,
60
+ openReport: false,
61
+ noOpenReport: false,
62
+ version: false,
63
+ help: false,
64
+ all: false,
65
+ followConfigRoot: false,
66
+ };
67
+ const requireValue = (flag, index) => {
68
+ const value = argv[index + 1];
69
+ if (value === undefined || value.startsWith('-')) {
70
+ throw new Error(`Missing value for ${flag}. Run arkgate-check --help for usage.`);
71
+ }
72
+ return value;
73
+ };
74
+ for (let i = 2; i < argv.length; i += 1) {
75
+ const arg = argv[i];
76
+ if (arg === '--json') args.json = true;
77
+ else if (arg === '--strict' || arg === '--strict-merge') {
78
+ args.strictConfig = true;
79
+ args.requireGates = true;
80
+ args.strictMerge = true;
81
+ }
82
+ else if (arg === '--strict-config') args.strictConfig = true;
83
+ else if (arg === '--require-gates') {
84
+ args.requireGates = true;
85
+ args.strictConfig = true;
86
+ }
87
+ else if (arg === '--require-write-hook') {
88
+ args.requireWriteHook = requireValue(arg, i++).trim().toLowerCase();
89
+ }
90
+ else if (arg === '--init') args.init = true;
91
+ else if (arg === '--preset') args.preset = requireValue(arg, i++);
92
+ else if (arg === '--install-agent-gates') args.installAgentGates = true;
93
+ else if (arg === '--compact') args.compact = true;
94
+ else if (arg === '--tools') {
95
+ // Consume the next arg only when it isn't another flag (same rule as --baseline),
96
+ // so `--tools --force` can't silently eat --force as a "tool name".
97
+ const next = argv[i + 1];
98
+ if (next !== undefined && !next.startsWith('-')) {
99
+ i += 1;
100
+ args.tools = next
101
+ .split(',')
102
+ .map((tool) => tool.trim().toLowerCase())
103
+ .filter(Boolean);
104
+ } else {
105
+ args.tools = []; // flag without a value — rejected in runInstallAgentGates
106
+ }
107
+ }
108
+ else if (arg === '--force') args.force = true;
109
+ else if (arg === '--follow-config-root') args.followConfigRoot = true;
110
+ else if (arg === '--skills-only') args.skillsOnly = true;
111
+ else if (arg === '--coverage') args.coverage = true;
112
+ else if (arg === '--doctor') args.doctor = true;
113
+ else if (arg === '--plan') args.plan = true;
114
+ else if (arg === '--rules-inventory') args.rulesInventory = true;
115
+ else if (arg === '--recommend') args.recommend = true;
116
+ else if (arg === '--write-plan') args.writePlan = true;
117
+ else if (arg === '--list-policy-packs') args.listPolicyPacks = true;
118
+ else if (arg === '--apply-policy-pack') args.applyPolicyPack = requireValue(arg, i++);
119
+ else if (arg === '--suggest-include') args.suggestInclude = true;
120
+ else if (arg === '--adopt-contract') args.adoptContract = true;
121
+ else if (arg === '--migrate-contract') args.migrateContract = true;
122
+ else if (arg === '--ratchet-cores') args.ratchetCores = true;
123
+ else if (arg === '--write') args.write = true;
124
+ else if (arg === '--watch') args.watch = true;
125
+ else if (arg === '--beginner') args.beginner = true;
126
+ else if (arg === '--codex-home') args.codexHome = true;
127
+ else if (arg === '--claude-home') args.claudeHome = true;
128
+ else if (arg === '--grok-home') args.grokHome = true;
129
+ else if (arg === '--agent-homes') {
130
+ args.agentHomes = true;
131
+ args.codexHome = true;
132
+ args.claudeHome = true;
133
+ args.grokHome = true;
134
+ }
135
+ else if (arg === '--migrate-commands') args.migrateCommands = true;
136
+ else if (arg === '--no-cache') args.noCache = true;
137
+ else if (arg === '--resident') args.resident = true;
138
+ else if (arg === '--report') {
139
+ const next = argv[i + 1];
140
+ args.report = next && !next.startsWith('-') ? argv[++i] : 'ark-report.html';
141
+ }
142
+ else if (arg === '--reset-origin') args.resetOrigin = true;
143
+ else if (arg === '--no-archive') args.noArchive = true;
144
+ else if (arg === '--open') args.openReport = true;
145
+ else if (arg === '--no-open') args.noOpenReport = true;
146
+ else if (arg === '--baseline' || arg === '--update-baseline') {
147
+ if (arg === '--update-baseline') args.updateBaseline = true;
148
+ // optional path value: consume the next arg only when it isn't another flag
149
+ const next = argv[i + 1];
150
+ args.baseline = next && !next.startsWith('-') ? argv[++i] : '.ark-baseline.json';
151
+ }
152
+ else if (arg === '--policy-base') args.policyBase = requireValue(arg, i++);
153
+ else if (arg === '--policy-base-ref') args.policyBaseRef = requireValue(arg, i++);
154
+ else if (arg === '--policy-ack') args.policyAck = requireValue(arg, i++); else if (arg === '--fail-on-new-smells') args.failOnNewSmells = true; else if (arg === '--base-ref') args.baseRef = requireValue(arg, i++);
155
+ else if (arg === '--contract-session') args.contractSession = true;
156
+ else if (arg === '--contract-diff') args.contractDiff = true;
157
+ else if (arg === '--changed') args.changed = true;
158
+ else if (arg === '--against') args.against = requireValue(arg, i++);
159
+ else if (arg === '--base') args.base = requireValue(arg, i++);
160
+ else if (arg === '--persona') args.persona = requireValue(arg, i++);
161
+ else if (arg === '--author') args.author = requireValue(arg, i++);
162
+ else if (arg === '--root') args.root = path.resolve(requireValue(arg, i++));
163
+ else if (arg === '--config') args.config = requireValue(arg, i++);
164
+ else if (arg === '--manifest') args.manifest = requireValue(arg, i++);
165
+ else if (arg === '--print-config') args.printConfig = requireValue(arg, i++);
166
+ else if (arg === '--tsconfig') args.tsconfig = requireValue(arg, i++);
167
+ else if (arg === '--help' || arg === '-h') args.help = true;
168
+ else if (arg === '--all') args.all = true;
169
+ else if (arg === '--version' || arg === '-V') args.version = true;
170
+ else throw new Error(`Unknown argument: ${arg}. Run arkgate-check --help for usage.`);
171
+ }
172
+ return args;
173
+ }
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Convention-based ark.config detection used by --init.
3
+ */
4
+ import fs from 'node:fs';
5
+ import path from 'node:path';
6
+ import {
7
+ DEFAULT_DOMAIN_FORBIDDEN_GLOBALS,
8
+ DEFAULT_INTENT_PREFIXES,
9
+ DEFAULT_LAYER_DIRECTORIES,
10
+ DEFAULT_RULES,
11
+ } from '../ark-shared.mjs';
12
+ import { normalize, walk } from './scan-files.mjs';
13
+ import { suggestLayerForDir } from './suggestions.mjs';
14
+
15
+ /**
16
+ * Infer an ark.config.json from the directories that actually exist in the project,
17
+ * using the same layer→directory conventions as the eleven-layer template. A directory
18
+ * only counts when it contains at least one source file, so an empty scaffold dir can't
19
+ * produce a layer whose pattern matches nothing (which --strict-config would fail).
20
+ */
21
+ export function detectConfig(root) {
22
+ const srcDir = fs.existsSync(path.join(root, 'src')) ? 'src' : '.';
23
+ const layers = [];
24
+
25
+ for (const entry of DEFAULT_INTENT_PREFIXES) {
26
+ const directories = (DEFAULT_LAYER_DIRECTORIES[entry.layer] ?? []).filter(
27
+ (directory) => walk(path.join(root, srcDir, directory), [], { root }).length > 0
28
+ );
29
+ if (directories.length === 0) continue;
30
+ layers.push({
31
+ name: entry.layer,
32
+ patterns: directories.map((directory) => `${normalize(path.join(srcDir, directory))}/**`),
33
+ intentPrefixes: entry.prefixes,
34
+ ...(entry.layer === 'DomainModel'
35
+ ? { forbiddenGlobals: DEFAULT_DOMAIN_FORBIDDEN_GLOBALS }
36
+ : {}),
37
+ });
38
+ }
39
+
40
+ const names = new Set(layers.map((layer) => layer.name));
41
+ const rules = DEFAULT_RULES.filter((rule) => names.has(rule.from) && names.has(rule.to));
42
+
43
+ return { srcDir, config: { include: [srcDir], layers, rules } };
44
+ }
45
+
46
+ /** Top-level directories under srcDir not covered by any detected layer pattern. */
47
+ export function uncoveredDirectories(root, srcDir, layers) {
48
+ const base = path.join(root, srcDir);
49
+ if (!fs.existsSync(base)) return [];
50
+ return fs
51
+ .readdirSync(base, { withFileTypes: true })
52
+ .filter(
53
+ (entry) =>
54
+ entry.isDirectory() &&
55
+ entry.name !== 'node_modules' &&
56
+ entry.name !== 'dist' &&
57
+ !entry.name.startsWith('.')
58
+ )
59
+ .map((entry) => entry.name)
60
+ .filter((name) => {
61
+ const prefix = `${normalize(path.join(srcDir, name))}/`;
62
+ return !layers.some((layer) =>
63
+ layer.patterns.some((pattern) => pattern.startsWith(prefix))
64
+ );
65
+ });
66
+ }
67
+
68
+ export function proposeForUncovered(root, srcDir, layers) {
69
+ const proposals = [];
70
+ for (const top of uncoveredDirectories(root, srcDir, layers)) {
71
+ const direct = suggestLayerForDir(top);
72
+ if (direct) {
73
+ proposals.push({ dir: `${srcDir}/${top}`, ...direct });
74
+ continue;
75
+ }
76
+ let children = [];
77
+ try {
78
+ children = fs
79
+ .readdirSync(path.join(root, srcDir, top), { withFileTypes: true })
80
+ .filter((e) => e.isDirectory() && e.name !== 'node_modules' && !e.name.startsWith('.'))
81
+ .map((e) => e.name);
82
+ } catch {
83
+ /* not a readable directory — treat as unrecognized below */
84
+ }
85
+ if (children.length > 0) {
86
+ // Descend: propose per child so a mixed `lib/` yields lib/repositories → Persistence
87
+ // AND flags lib/db as unrecognized, instead of dropping the parts Ark can't place.
88
+ for (const child of children) {
89
+ const hit = suggestLayerForDir(child);
90
+ proposals.push(
91
+ hit
92
+ ? { dir: `${srcDir}/${top}/${child}`, ...hit }
93
+ : { dir: `${srcDir}/${top}/${child}`, unrecognized: true }
94
+ );
95
+ }
96
+ } else {
97
+ proposals.push({ dir: `${srcDir}/${top}`, unrecognized: true });
98
+ }
99
+ }
100
+ return proposals;
101
+ }
@@ -0,0 +1,80 @@
1
+ /**
2
+ * ark-check --watch loop (polling fallback when fs.watch is unavailable).
3
+ */
4
+ import { spawnSync } from 'node:child_process';
5
+ import fs from 'node:fs';
6
+ import path from 'node:path';
7
+
8
+ function watchFingerprint(target) {
9
+ const pending = [target];
10
+ const entries = [];
11
+ while (pending.length > 0) {
12
+ const current = pending.pop();
13
+ let stat;
14
+ try {
15
+ stat = fs.statSync(current);
16
+ } catch {
17
+ continue;
18
+ }
19
+ entries.push(`${current}:${stat.mtimeMs}:${stat.size}`);
20
+ if (!stat.isDirectory()) continue;
21
+ try {
22
+ for (const name of fs.readdirSync(current)) pending.push(path.join(current, name));
23
+ } catch {
24
+ // A concurrent delete is represented by the next fingerprint.
25
+ }
26
+ }
27
+ return entries.sort().join('|');
28
+ }
29
+
30
+ function watchByPolling(target, onChange) {
31
+ let previous = watchFingerprint(target);
32
+ setInterval(() => {
33
+ const current = watchFingerprint(target);
34
+ if (current === previous) return;
35
+ previous = current;
36
+ onChange();
37
+ }, 250);
38
+ }
39
+
40
+ export async function runWatchMode(args, { cliPath, loadConfig, dim }) {
41
+ const argv = process.argv.slice(2).filter((token) => token !== '--watch');
42
+ let debounce;
43
+ const rerun = () => {
44
+ clearTimeout(debounce);
45
+ debounce = setTimeout(() => {
46
+ const result = spawnSync(process.execPath, [cliPath, ...argv], {
47
+ cwd: args.root,
48
+ stdio: 'inherit',
49
+ env: process.env,
50
+ });
51
+ process.exitCode = result.status ?? 1;
52
+ }, 300);
53
+ };
54
+
55
+ let config;
56
+ try {
57
+ config = loadConfig(args.root, args.config);
58
+ } catch (error) {
59
+ console.error(error instanceof Error ? error.message : String(error));
60
+ process.exitCode = 2;
61
+ return;
62
+ }
63
+
64
+ for (const entry of config.include ?? []) {
65
+ const target = path.join(args.root, entry);
66
+ if (!fs.existsSync(target)) continue;
67
+ try {
68
+ const watcher = fs.watch(target, { recursive: true }, rerun);
69
+ watcher.on('error', () => {
70
+ watcher.close();
71
+ watchByPolling(target, rerun);
72
+ });
73
+ } catch {
74
+ watchByPolling(target, rerun);
75
+ }
76
+ }
77
+
78
+ console.log(dim('Watching governed paths for changes… (Ctrl+C to stop)'));
79
+ await new Promise(() => {});
80
+ }
@@ -22,6 +22,8 @@ export const HOT_PATH_COMMIT_LIMIT = 200;
22
22
  export const HOT_PATH_LIST_CAP = 8;
23
23
  /** Minimum change hits before a path is “elevated”. */
24
24
  export const HOT_PATH_MIN_HITS = 3;
25
+ /** Kill hung git instead of stalling CI. */
26
+ export const SPAWN_TIMEOUT_MS = 8000;
25
27
 
26
28
  /**
27
29
  * Best-effort recent-churn paths from git history.
@@ -40,6 +42,7 @@ export function computeHotPathAdvisory(root, opts = {}) {
40
42
  encoding: 'utf8',
41
43
  maxBuffer: 8 * 1024 * 1024,
42
44
  stdio: ['ignore', 'pipe', 'pipe'],
45
+ timeout: SPAWN_TIMEOUT_MS,
43
46
  });
44
47
 
45
48
  const emptyUnavailable = (reason) => ({