jules-orchestrator-kit 0.74.0 → 0.75.1
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/CHANGELOG.md +27 -1
- package/JULES_RULES_TEMPLATE.md +3 -0
- package/README.md +4 -2
- package/bin/agentctl.mjs +395 -23
- package/package.json +1 -1
- package/scripts/ci-scope-guard.mjs +226 -23
- package/scripts/run-tests.mjs +5 -1
- package/src/bidi-guard.mjs +190 -15
- package/src/ci-templates.mjs +184 -21
- package/src/config.mjs +40 -1
- package/src/engine.mjs +342 -48
- package/src/evidence.mjs +22 -5
- package/src/git.mjs +15 -0
- package/src/migrate.mjs +493 -0
- package/src/ops/command-registry.mjs +93 -4
- package/src/ops/doctor-registry.mjs +184 -32
- package/src/ops/pr-harvest.mjs +22 -2
- package/src/ops/toolchain-diagnostics.mjs +253 -0
- package/src/ops/tui-menu.mjs +192 -0
- package/src/process-tree.mjs +146 -0
- package/src/prompt-guard.mjs +9 -0
- package/src/provider.mjs +14 -0
- package/src/scaffold.mjs +49 -1
- package/src/scope-guard.mjs +73 -5
- package/src/secret-scanner.mjs +14 -2
- package/src/security.mjs +41 -30
- package/src/self-audit.mjs +237 -3
- package/src/state.mjs +123 -13
- package/src/uninstall.mjs +227 -0
- package/src/wizard-init.mjs +11 -0
- package/src/wizard-task.mjs +17 -22
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Undo a default `agentctl init`: remove kit-owned config and the runtime
|
|
3
|
+
* `.gitignore` block. Optional `--force` also removes legacy scaffold dirs.
|
|
4
|
+
*
|
|
5
|
+
* See docs/uninstall.md — this deliberately does not delete AGENTS.md,
|
|
6
|
+
* SPEC.md, or other paths that may contain project-owned content.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { existsSync, readFileSync, writeFileSync, rmSync, readdirSync, rmdirSync } from "node:fs";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
import {
|
|
12
|
+
RUNTIME_GITIGNORE_HEADER,
|
|
13
|
+
stripRuntimeGitignoreBlock,
|
|
14
|
+
} from "./scaffold.mjs";
|
|
15
|
+
|
|
16
|
+
/** Legacy scaffold directories owned by `init --force` / older 0.x releases. */
|
|
17
|
+
export const FORCE_REMOVE_DIRS = [
|
|
18
|
+
".agent/rules",
|
|
19
|
+
".agent/prompts",
|
|
20
|
+
".agent/workflows",
|
|
21
|
+
".agent/jules-queue",
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @param {string} content
|
|
26
|
+
* @returns {boolean}
|
|
27
|
+
*/
|
|
28
|
+
export function looksLikeKitConfig(content) {
|
|
29
|
+
const text = String(content);
|
|
30
|
+
if (text.includes("Agent Orchestrator Kit Config")) return true;
|
|
31
|
+
// Minimal shape init writes: a versioned manifest with a provider key.
|
|
32
|
+
return /^version:\s*\d+/m.test(text) && /^provider:\s*\S+/m.test(text);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @param {string} root
|
|
37
|
+
* @returns {{ installed: boolean, hasConfig: boolean, hasGitignoreBlock: boolean, configPath: string|null }}
|
|
38
|
+
*/
|
|
39
|
+
export function detectKitInstallation(root) {
|
|
40
|
+
const configPath = join(root, ".agent", "config.yml");
|
|
41
|
+
let hasConfig = false;
|
|
42
|
+
if (existsSync(configPath)) {
|
|
43
|
+
try {
|
|
44
|
+
hasConfig = looksLikeKitConfig(readFileSync(configPath, "utf-8"));
|
|
45
|
+
} catch (_) {
|
|
46
|
+
hasConfig = false;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
let hasGitignoreBlock = false;
|
|
51
|
+
const gitignorePath = join(root, ".gitignore");
|
|
52
|
+
if (existsSync(gitignorePath)) {
|
|
53
|
+
try {
|
|
54
|
+
hasGitignoreBlock = readFileSync(gitignorePath, "utf-8").includes(RUNTIME_GITIGNORE_HEADER);
|
|
55
|
+
} catch (_) {
|
|
56
|
+
hasGitignoreBlock = false;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
installed: hasConfig || hasGitignoreBlock,
|
|
62
|
+
hasConfig,
|
|
63
|
+
hasGitignoreBlock,
|
|
64
|
+
configPath: hasConfig ? ".agent/config.yml" : null,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* @param {string} root
|
|
70
|
+
* @param {{ force?: boolean }} [options]
|
|
71
|
+
* @returns {{
|
|
72
|
+
* ok: boolean,
|
|
73
|
+
* installed: boolean,
|
|
74
|
+
* willRemove: string[],
|
|
75
|
+
* willCleanGitignore: boolean,
|
|
76
|
+
* forceDirs: string[],
|
|
77
|
+
* error?: string,
|
|
78
|
+
* }}
|
|
79
|
+
*/
|
|
80
|
+
export function planUninstall(root, options = {}) {
|
|
81
|
+
const force = Boolean(options.force);
|
|
82
|
+
const detection = detectKitInstallation(root);
|
|
83
|
+
if (!detection.installed) {
|
|
84
|
+
return {
|
|
85
|
+
ok: false,
|
|
86
|
+
installed: false,
|
|
87
|
+
willRemove: [],
|
|
88
|
+
willCleanGitignore: false,
|
|
89
|
+
forceDirs: [],
|
|
90
|
+
error: "No kit installation detected (missing .agent/config.yml and runtime gitignore block).",
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const willRemove = [];
|
|
95
|
+
if (detection.hasConfig) willRemove.push(".agent/config.yml");
|
|
96
|
+
|
|
97
|
+
const forceDirs = [];
|
|
98
|
+
if (force) {
|
|
99
|
+
for (const rel of FORCE_REMOVE_DIRS) {
|
|
100
|
+
if (existsSync(join(root, rel))) forceDirs.push(rel);
|
|
101
|
+
}
|
|
102
|
+
willRemove.push(...forceDirs);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const willCleanGitignore = detection.hasGitignoreBlock;
|
|
106
|
+
if (willCleanGitignore) willRemove.push(".gitignore");
|
|
107
|
+
|
|
108
|
+
// `.agent/` itself is only removed when it would be empty after the planned
|
|
109
|
+
// deletions — reported so dry-run matches real execution.
|
|
110
|
+
const agentDir = join(root, ".agent");
|
|
111
|
+
if (existsSync(agentDir) && wouldAgentDirBeEmpty(root, {
|
|
112
|
+
removeConfig: detection.hasConfig,
|
|
113
|
+
forceDirs,
|
|
114
|
+
})) {
|
|
115
|
+
willRemove.push(".agent");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
ok: true,
|
|
120
|
+
installed: true,
|
|
121
|
+
willRemove: [...new Set(willRemove)],
|
|
122
|
+
willCleanGitignore,
|
|
123
|
+
forceDirs,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* @param {string} root
|
|
129
|
+
* @param {{ removeConfig: boolean, forceDirs: string[] }} planned
|
|
130
|
+
*/
|
|
131
|
+
function wouldAgentDirBeEmpty(root, planned) {
|
|
132
|
+
const agentDir = join(root, ".agent");
|
|
133
|
+
if (!existsSync(agentDir)) return false;
|
|
134
|
+
let entries;
|
|
135
|
+
try {
|
|
136
|
+
entries = readdirSync(agentDir);
|
|
137
|
+
} catch (_) {
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
const removing = new Set();
|
|
141
|
+
if (planned.removeConfig) removing.add("config.yml");
|
|
142
|
+
for (const rel of planned.forceDirs) {
|
|
143
|
+
if (rel.startsWith(".agent/")) removing.add(rel.slice(".agent/".length).split("/")[0]);
|
|
144
|
+
}
|
|
145
|
+
return entries.every((name) => removing.has(name));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* @param {string} root
|
|
150
|
+
* @param {{ force?: boolean, dryRun?: boolean }} [options]
|
|
151
|
+
* @returns {{
|
|
152
|
+
* ok: boolean,
|
|
153
|
+
* dryRun: boolean,
|
|
154
|
+
* installed: boolean,
|
|
155
|
+
* willRemove?: string[],
|
|
156
|
+
* removed?: string[],
|
|
157
|
+
* willCleanGitignore?: boolean,
|
|
158
|
+
* cleanedGitignore?: boolean,
|
|
159
|
+
* error?: string,
|
|
160
|
+
* }}
|
|
161
|
+
*/
|
|
162
|
+
export function uninstallKit(root, options = {}) {
|
|
163
|
+
const dryRun = Boolean(options.dryRun);
|
|
164
|
+
const force = Boolean(options.force);
|
|
165
|
+
const plan = planUninstall(root, { force });
|
|
166
|
+
if (!plan.ok) {
|
|
167
|
+
return { ok: false, dryRun, installed: false, error: plan.error };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (dryRun) {
|
|
171
|
+
return {
|
|
172
|
+
ok: true,
|
|
173
|
+
dryRun: true,
|
|
174
|
+
installed: true,
|
|
175
|
+
willRemove: plan.willRemove,
|
|
176
|
+
willCleanGitignore: plan.willCleanGitignore,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const removed = [];
|
|
181
|
+
|
|
182
|
+
const configAbs = join(root, ".agent", "config.yml");
|
|
183
|
+
if (plan.willRemove.includes(".agent/config.yml") && existsSync(configAbs)) {
|
|
184
|
+
rmSync(configAbs, { force: true });
|
|
185
|
+
removed.push(".agent/config.yml");
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
for (const rel of plan.forceDirs) {
|
|
189
|
+
const abs = join(root, rel);
|
|
190
|
+
if (existsSync(abs)) {
|
|
191
|
+
rmSync(abs, { recursive: true, force: true });
|
|
192
|
+
removed.push(rel);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
let cleanedGitignore = false;
|
|
197
|
+
const gitignorePath = join(root, ".gitignore");
|
|
198
|
+
if (plan.willCleanGitignore && existsSync(gitignorePath)) {
|
|
199
|
+
const current = readFileSync(gitignorePath, "utf-8");
|
|
200
|
+
const { text, stripped } = stripRuntimeGitignoreBlock(current);
|
|
201
|
+
if (stripped) {
|
|
202
|
+
writeFileSync(gitignorePath, text, "utf-8");
|
|
203
|
+
cleanedGitignore = true;
|
|
204
|
+
removed.push(".gitignore");
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const agentDir = join(root, ".agent");
|
|
209
|
+
if (existsSync(agentDir)) {
|
|
210
|
+
try {
|
|
211
|
+
if (readdirSync(agentDir).length === 0) {
|
|
212
|
+
rmdirSync(agentDir);
|
|
213
|
+
removed.push(".agent");
|
|
214
|
+
}
|
|
215
|
+
} catch (_) {
|
|
216
|
+
// Leave the directory if it is not empty or not removable.
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return {
|
|
221
|
+
ok: true,
|
|
222
|
+
dryRun: false,
|
|
223
|
+
installed: true,
|
|
224
|
+
removed: [...new Set(removed)],
|
|
225
|
+
cleanedGitignore,
|
|
226
|
+
};
|
|
227
|
+
}
|
package/src/wizard-init.mjs
CHANGED
|
@@ -122,6 +122,17 @@ export function planInit(root = process.cwd(), options = {}) {
|
|
|
122
122
|
// scaffolded limits always match what the runtime will later enforce.
|
|
123
123
|
const tierPresetLimits = TIER_PROFILES[tierName] || TIER_PROFILES[FALLBACK_TIER];
|
|
124
124
|
|
|
125
|
+
// Same fail-closed rule as tier: an explicit `--profile` that is not one of
|
|
126
|
+
// PROFILE_NAMES must not silently become `standard`. Guessing wrong used to
|
|
127
|
+
// scaffold the everyday gate under a name the operator never chose.
|
|
128
|
+
if (options.profile && !PROFILE_NAMES.includes(String(options.profile).toLowerCase())) {
|
|
129
|
+
const rawProfile = String(options.profile).toLowerCase();
|
|
130
|
+
if (validTiers.includes(rawProfile)) {
|
|
131
|
+
throw new Error(`Invalid profile "${options.profile}". Valid profiles: ${PROFILE_NAMES.join(", ")}. Did you mean '--tier ${options.profile}'?`);
|
|
132
|
+
}
|
|
133
|
+
throw new Error(`Invalid profile "${options.profile}". Valid profiles: ${PROFILE_NAMES.join(", ")}.`);
|
|
134
|
+
}
|
|
135
|
+
|
|
125
136
|
// Preserve existing config if present (ignored when pristine requested)
|
|
126
137
|
let existingConfig = {};
|
|
127
138
|
if (!options.pristine) {
|
package/src/wizard-task.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { writeFileSync, mkdirSync, existsSync } from "node:fs";
|
|
2
|
-
import { resolve
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
3
|
import { untrackedOnboardingArtifacts } from "./git.mjs";
|
|
4
4
|
import { loadConfig } from "./config.mjs";
|
|
5
5
|
import { gate } from "./engine.mjs";
|
|
@@ -220,33 +220,22 @@ export function planTaskCreate(root = process.cwd(), inputObj = {}) {
|
|
|
220
220
|
const rawId = inputObj.id || `TASK-${Date.now().toString(36).toUpperCase()}`;
|
|
221
221
|
const taskId = String(rawId).replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
222
222
|
|
|
223
|
-
const
|
|
224
|
-
const hasWrangler =
|
|
225
|
-
existsSync(join(root, "wrangler.jsonc")) ||
|
|
226
|
-
existsSync(join(root, "wrangler.json")) ||
|
|
227
|
-
existsSync(join(root, "wrangler.toml"));
|
|
228
|
-
|
|
223
|
+
const configInvariants = Array.isArray(config.invariants) ? config.invariants : [];
|
|
229
224
|
const autoInvariants = [
|
|
230
225
|
"Preserve existing signatures, surrounding logic, and coding style.",
|
|
231
226
|
"Do not modify files outside scope.allow.",
|
|
227
|
+
...configInvariants,
|
|
232
228
|
];
|
|
233
|
-
if (hasAstro) {
|
|
234
|
-
autoInvariants.push("Zero-JS runtime-princip: skicka noll onödig klient-JS i src/ och använd plana <img> framför Astro <Image>.");
|
|
235
|
-
}
|
|
236
|
-
if (hasWrangler) {
|
|
237
|
-
autoInvariants.push("Edge runtime constraints: inga otillåtna Node-imports (t.ex. node:fs, node:path, sharp) i workerd runtime-kod.");
|
|
238
|
-
}
|
|
239
229
|
const invariants = Array.isArray(inputObj.invariants) && inputObj.invariants.length > 0
|
|
240
230
|
? inputObj.invariants
|
|
241
231
|
: autoInvariants;
|
|
242
232
|
|
|
243
|
-
const
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
const mcpDirectives = inputObj.mcpDirectives || inputObj.mcp_directives || (autoMcpDirectives.length > 0 ? autoMcpDirectives : undefined);
|
|
233
|
+
const configDirectives = Array.isArray(config.mcp_directives)
|
|
234
|
+
? config.mcp_directives
|
|
235
|
+
: Array.isArray(config.mcpDirectives)
|
|
236
|
+
? config.mcpDirectives
|
|
237
|
+
: [];
|
|
238
|
+
const mcpDirectives = inputObj.mcpDirectives || inputObj.mcp_directives || (configDirectives.length > 0 ? configDirectives : undefined);
|
|
250
239
|
const mcpDirectiveLine = mcpDirectives && mcpDirectives.length > 0
|
|
251
240
|
? `MCP DIRECTIVE: Mandating pre-execution documentation lookup via [${mcpDirectives.join(" | ")}] before modifying code.\n\n`
|
|
252
241
|
: "";
|
|
@@ -476,7 +465,7 @@ export async function runTaskCreateWizard(root = process.cwd(), options = {}) {
|
|
|
476
465
|
});
|
|
477
466
|
|
|
478
467
|
// Perform Gate Preflight
|
|
479
|
-
const gateRes = await gate({ root, mode: "working-tree" });
|
|
468
|
+
const gateRes = await gate({ root, mode: "working-tree", allowProtected: Boolean(options.allowProtected) });
|
|
480
469
|
if (!gateRes.ok && (gateRes.code === 3 || gateRes.code === 6) && !options.allowGateFailure) {
|
|
481
470
|
const scopeViolations = gateRes.phases[0]?.violations || [];
|
|
482
471
|
// A cold-start repository has nothing committed yet: every path init just
|
|
@@ -491,7 +480,13 @@ export async function runTaskCreateWizard(root = process.cwd(), options = {}) {
|
|
|
491
480
|
const realViolations = scopeViolations.filter((v) => !v.file.startsWith(".agent/")) || [];
|
|
492
481
|
const installArtifacts = new Set(untrackedOnboardingArtifacts(root, scopeViolations.map((v) => v.file)));
|
|
493
482
|
const blockers = realViolations.filter((v) => !installArtifacts.has(v.file));
|
|
494
|
-
if (blockers.length > 0
|
|
483
|
+
if (blockers.length > 0) {
|
|
484
|
+
const fileList = blockers.map((b) => b.file).join(", ");
|
|
485
|
+
throw new Error(
|
|
486
|
+
`Gate Preflight Rejected Task: Repository contains scope violations in protected/denied paths: ${fileList} (Exit 3).\n To permit protected paths for this task, re-run with --allow-protected.`
|
|
487
|
+
);
|
|
488
|
+
}
|
|
489
|
+
if (gateRes.code === 6) {
|
|
495
490
|
throw new Error(`Gate Preflight Rejected Task: Repository contains scope or secret violations (Exit ${gateRes.code}).`);
|
|
496
491
|
}
|
|
497
492
|
}
|