jules-orchestrator-kit 0.41.0 → 0.42.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/README.md +25 -10
- package/bin/agentctl.mjs +267 -34
- package/bin/init.js +12 -86
- package/package.json +1 -1
- package/src/engine.mjs +51 -1
- package/src/git.mjs +20 -1
- package/src/scaffold.mjs +283 -0
- package/src/security.mjs +164 -24
- package/src/task-optimizer.mjs +1 -1
package/README.md
CHANGED
|
@@ -51,16 +51,28 @@
|
|
|
51
51
|
Get running in any repository in 3 commands (zero configuration required):
|
|
52
52
|
|
|
53
53
|
```bash
|
|
54
|
-
# 1.
|
|
54
|
+
# 1. Scaffold config, AGENTS.md, role prompts and guardrails
|
|
55
|
+
# (auto-detects Python, Rust, Go, Node, PHP, etc.)
|
|
55
56
|
npx jules-orchestrator-kit init
|
|
57
|
+
```
|
|
56
58
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
+
```bash
|
|
60
|
+
# 2. Commit what init wrote — .agent/config.yml is on the gate's deny list by
|
|
61
|
+
# design, so leaving it uncommitted makes the first gate reject your tree
|
|
62
|
+
git add .agent AGENTS.md .gitignore && git commit -m "chore: add agent config"
|
|
63
|
+
```
|
|
59
64
|
|
|
60
|
-
|
|
61
|
-
|
|
65
|
+
```bash
|
|
66
|
+
# 3. Author a scoped, verified task envelope with guardrails & secret scrubbing
|
|
67
|
+
npx jules-orchestrator-kit task create
|
|
62
68
|
```
|
|
63
69
|
|
|
70
|
+
> [!TIP]
|
|
71
|
+
> **Not sure what to run next?**
|
|
72
|
+
> `agentctl` with no arguments reads the repository state and prints the single
|
|
73
|
+
> next step — missing git repo, missing API key, empty queue, tasks ready to
|
|
74
|
+
> dispatch — instead of a wall of commands.
|
|
75
|
+
|
|
64
76
|
> [!TIP]
|
|
65
77
|
> **Prefer a global CLI?**
|
|
66
78
|
> Install globally to access `agentctl` directly:
|
|
@@ -131,7 +143,7 @@ To maximize PR merge rates, dispatch tasks according to deterministic boundaries
|
|
|
131
143
|
* **Fail-Closed Security & Secret Redaction:** Evaluates explicit Deny rules before Allow rules against canonicalized, case-folded paths. Redacts high-entropy keys and base64-encoded credentials (such as Kubernetes `Secret` manifests).
|
|
132
144
|
* **Complexity & Cost Router:** Zero-dependency heuristic classifier (`src/router.mjs`) routing mechanical tasks to lightweight models while reserving primary models for complex refactors, with a `node --check` syntax-verification gate that transparently escalates a FAST-tier result to the primary provider if it left broken JS on disk.
|
|
133
145
|
* **Terminal UI & Diagnostic Matrix (`agentctl doctor`):** Interactive terminal dashboard, task sidecar manager, and automated transactional self-repair.
|
|
134
|
-
* **Verified Test Suite:** Tested with **
|
|
146
|
+
* **Verified Test Suite:** Tested with **717 unit tests across 85 suites passing in < 10.0s**.
|
|
135
147
|
|
|
136
148
|
<br/>
|
|
137
149
|
|
|
@@ -146,18 +158,19 @@ To maximize PR merge rates, dispatch tasks according to deterministic boundaries
|
|
|
146
158
|
|
|
147
159
|
| Command | Usage | Description | Exit Codes |
|
|
148
160
|
| :--- | :--- | :--- | :--- |
|
|
149
|
-
| `init` | `agentctl init [--interactive] [--tier pro]` | Interactive onboarding wizard & stack detector
|
|
161
|
+
| `init` | `agentctl init [--interactive] [--tier pro] [--force]` | Interactive onboarding wizard & stack detector. Generates `.agent/config.yml` and scaffolds `AGENTS.md`, the role prompts, the guardrails and the runtime `.gitignore` entries. Existing files are preserved unless `--force`. | `0` (Created) |
|
|
150
162
|
| `budget` | `agentctl budget [--by-user] [--json] [reset]` | Reports rolling 24h task budget, quota headroom, and per-developer task attribution without external auth servers. | `0` (Status), `2` (Arg Error) |
|
|
151
|
-
| `task create` | `agentctl task create [--title <t>] [
|
|
163
|
+
| `task create` | `agentctl task create [<prompt>] [--title <t>] [-p <prompt>] [-f <file>] [--template <id>] [--role <name>] [--tier fast\|complex]` | Interactively authors & scopes falsifiable task envelopes with secret scrubbing, preflight gate checks, and DAG dependency wiring. | `0` (Queued), `1` (Secret/Unfalsifiable) |
|
|
152
164
|
| `task template` | `agentctl task template [<id>] [--list] [--json]` | Lists and synthesizes pre-calibrated task envelopes (Web, Deep Think & Agent Hardening: `web-cwv`, `web-wcag`, `web-seo`, `web-playwright`, `agent-dead-code-audit`, `web-flaky-heal`, `web-i18n`, `web-ai-access`, `agent-qa-mutation`, `agent-ci-falsify`, `agent-service-isolate`, `agent-error-paths`, `agent-security-audit`, `deep-debug`, `deep-feature`, `deep-optimize`, `deep-harden`). | `0` (Listed/Synthesized) |
|
|
153
|
-
| `dispatch` | `agentctl dispatch [-p <prompt>] [-f <file>] [-r <role>] [-t <tier>] [--author <name>] [--check-premise] [--auto-pr] [--repoless] [--dry-run]` | Dispatches autonomous task to the active provider with pre-flight idempotency checks, payload limits, and role prompt resolution. | `0` (Dispatched), `1` (Error) |
|
|
165
|
+
| `dispatch` | `agentctl dispatch [<prompt>] [-p <prompt>] [-f <file>] [-r <role>] [-t <tier>] [--author <name>] [--check-premise] [--auto-pr] [--repoless] [--dry-run]` | Dispatches autonomous task to the active provider with pre-flight idempotency checks, payload limits, and role prompt resolution. `--dry-run` stops short of the provider call and reports itself as a rehearsal rather than a dispatch. | `0` (Dispatched), `1` (Error) |
|
|
154
166
|
| `plan approve` | `agentctl plan approve <sessionId> [--dry-run] [--json]` | Approves pending execution plan for an active Jules session (`:approvePlan`) with automatic 404/503 retry backoff. | `0` (Approved), `1` (Error) |
|
|
155
167
|
| `session get` | `agentctl session get <sessionId> [--dry-run] [--json]` | Retrieves live session lifecycle state from provider REST API with token rotation. | `0` (Fetched), `1` (Error) |
|
|
156
168
|
| `pr harvest` | `agentctl pr harvest [--tier r0,r1] [--limit <n>] [--auto] [--allow-no-checks] [--dry-run]` | Discovers open agent PRs, evaluates CI checks & risk tiers, and auto-squashes green low-risk changes autonomously. A PR reporting **no** CI checks is skipped unless `--allow-no-checks` is passed, and an unavailable changed-file list blocks rather than classifying as low risk. | `0` (Triaged/Merged), `1` (Error) |
|
|
157
169
|
| `doctor` | `agentctl doctor [--json]` | Diagnostic DAG check runner & automated transactional self-repair engine. | `0` (Healthy), `1` (Failures) |
|
|
158
170
|
| `queue` | `agentctl queue [--dag] [--concurrency <n>] [--dry-run] [--json]` | Consumes and executes task envelopes in `.agent/jules-queue/` with Kahn's DAG dependency resolution. Non-task files (manifests, `README.md`) are skipped, and `--dry-run` previews without moving anything. | `0` (Complete) |
|
|
159
171
|
| `swarm` | `agentctl swarm [--json]` | Runs parallel multi-agent swarm across worker slots with PID liveness detection. | `0` (Complete) |
|
|
160
|
-
| `gate` / `audit`| `agentctl
|
|
172
|
+
| `check` / `gate` / `audit`| `agentctl check [--mode working-tree] [--fix] [--json] [--json-report <path>]` | Runs security, secret scanning, rules budget audit, and tiered verification gates (with declarative assertion support) against working tree or branch. | `0` (Approved), `1` (Budget/Arg), `3` (Scope), `4` (Verify), `5` (Diff >75K), `6` (Secret), `8` (Flaky) |
|
|
173
|
+
| `rules` | `agentctl rules <check\|compile> [--out <path>] [--json]` | Audits instruction files against character/line budgets or compiles unified rules block with SHA-256 and length anti-truncation sentinels. | `0` (Valid/Compiled), `1` (Violations) |
|
|
161
174
|
| `assert` | `agentctl assert [--dir <d>] [--file <f>] [--max-mb <n>] [--gzip] [--targets <g>] [--patterns <p>] [--json] [--json-report <p>]` | Runs declarative zero-dependency verification assertion primitives (`assert:dir-size`, `assert:file-size`, `assert:file-patterns`, `assert:exists`). | `0` (Passed), `1` (Assertion Failed) |
|
|
162
175
|
| `rollback` | `agentctl rollback [sessionId \| --latest]` | Restores exact commit, uncommitted files, and cleans orphan task worktrees from pre-flight checkpoints. | `0` (Restored), `1` (Error) |
|
|
163
176
|
| `resume` | `agentctl resume <sessionId> --response "<reply>"` | Streams engineer response back into active Google Jules warm session context window. | `0` (Resumed), `1` (Error) |
|
|
@@ -356,6 +369,8 @@ const result = await fast.dispatch({ prompt: "Fix a typo." }, { root: process.cw
|
|
|
356
369
|
|
|
357
370
|
| Feature | Module / Command | Architectural Description | Status |
|
|
358
371
|
| :--- | :--- | :--- | :---: |
|
|
372
|
+
| **Diagnostics That Reach the Operator** | `src/security.mjs`, `src/engine.mjs`, `bin/agentctl.mjs` | Secret findings name the file and line, a failed verify stage reports its command, exit code and output, and `queue`/`swarm` name each failed task and exit `1` rather than reporting success for a run that dispatched nothing. | **v0.41.1** *(Shipped)* |
|
|
373
|
+
| **One Scaffolding Path & First-Install Fixes** | `src/scaffold.mjs`, `src/security.mjs` | `agentctl init` and `jules-init` scaffold from one source and write the runtime `.gitignore` entries, so the kit's own bookkeeping no longer reaches its own gate; a lockfile bump no longer fails closed as a secret leak. | **v0.41.1** *(Shipped)* |
|
|
359
374
|
| **Queue Runner Fidelity** | `src/dag-engine.mjs`, `src/engine.mjs` | Queue selection is by task shape rather than file extension, so manifests and READMEs are skipped instead of dispatched, and `--dry-run` leaves the queue untouched. | **v0.38.2** *(Shipped)* |
|
|
360
375
|
| **Release Gate Enforcement & Wizard Smoke Test** | `.github/workflows/jules-audit.yml`, `scripts/release.mjs`, `test/wizard-smoke.test.mjs` | Doc-sync gate runs in CI rather than by hand, releases block on a green CI matrix for `HEAD`, per-test deadlines turn a hang into a failure, and the real `init` wizard is driven end to end over a fake TTY. | **v0.38.1** *(Shipped)* |
|
|
361
376
|
| **Multi-OS CI Matrix & TUI Hardening** | `scripts/run-tests.mjs`, `src/state.mjs`, `src/git.mjs` | Automated 9-job CI matrix across Linux, macOS, and Windows on Node 20/22/24 with raw-mode TUI resilience and native Windows command quoting. | **v0.38.0** *(Shipped)* |
|
package/bin/agentctl.mjs
CHANGED
|
@@ -41,7 +41,9 @@ Usage: agentctl <command> [options]
|
|
|
41
41
|
|
|
42
42
|
Commands:
|
|
43
43
|
dispatch | create Dispatch a single task to an AI agent (--role <name>, --tier fast|complex, --check-premise)
|
|
44
|
+
check Run all-in-one CI security, rules, and stack verification gate
|
|
44
45
|
gate | audit Run CI security and verification gate against current branch
|
|
46
|
+
rules <action> Audit rule token budgets or compile rule sentinels (check | compile)
|
|
45
47
|
queue Run pending task queue (--dag, --concurrency <n>)
|
|
46
48
|
swarm Run parallel task swarm
|
|
47
49
|
mcp Start stdio Model Context Protocol (MCP) server
|
|
@@ -76,6 +78,9 @@ Commands:
|
|
|
76
78
|
version Output agentctl version
|
|
77
79
|
|
|
78
80
|
Options:
|
|
81
|
+
--prompt, -p Task prompt text — dispatch, task create and task optimize
|
|
82
|
+
also accept it as a positional argument
|
|
83
|
+
--prompt-file, -f Read the prompt from a file (-f is --fix on task optimize)
|
|
79
84
|
--role, -r Specify specialist agent role (overseer | bolt | sentinel | janitor)
|
|
80
85
|
--tier Force routing tier when router.enabled (fast | complex) — see .agent/config.yml router:
|
|
81
86
|
--check-premise Verify task goal/oracle passes locally before burning API budget
|
|
@@ -91,6 +96,96 @@ Options:
|
|
|
91
96
|
`);
|
|
92
97
|
}
|
|
93
98
|
|
|
99
|
+
/**
|
|
100
|
+
* Resolve the prompt text a command was given, from any of the three forms.
|
|
101
|
+
*
|
|
102
|
+
* The commands that take a prompt each accepted a different subset: `dispatch`
|
|
103
|
+
* took a flag, a file or a positional; `task create` took only `--prompt`; and
|
|
104
|
+
* `task optimize` took only a positional. The form an operator learned on one
|
|
105
|
+
* command then failed on the next — loudly on `task create "do the thing"`,
|
|
106
|
+
* which reported a missing prompt while holding one, and silently on
|
|
107
|
+
* `task optimize --prompt "..."`, which optimised an empty string.
|
|
108
|
+
*
|
|
109
|
+
* @param {Record<string, unknown>} values Parsed flags.
|
|
110
|
+
* @param {string[]} [positionals] Remaining free arguments.
|
|
111
|
+
* @returns {string} The prompt, or "" when none was supplied.
|
|
112
|
+
*/
|
|
113
|
+
function resolvePromptInput(values, positionals = []) {
|
|
114
|
+
const file = values["prompt-file"] || values.file;
|
|
115
|
+
if (file) {
|
|
116
|
+
if (!existsSync(file)) {
|
|
117
|
+
console.error(`Error: prompt file not found: ${file}`);
|
|
118
|
+
process.exit(1);
|
|
119
|
+
}
|
|
120
|
+
return readFileSync(file, "utf-8");
|
|
121
|
+
}
|
|
122
|
+
if (values.prompt) return String(values.prompt);
|
|
123
|
+
return positionals.join(" ").trim();
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Renders the outcome of a queue or swarm run and returns the exit code.
|
|
128
|
+
*
|
|
129
|
+
* The per-task failures used to be dropped on the floor. A run where every
|
|
130
|
+
* task was rejected — no API key is the common one — still printed
|
|
131
|
+
* "Processed 3 task(s)." and exited 0, so neither an operator nor a CI job
|
|
132
|
+
* could tell a dispatched queue from a dead one. The failures were already in
|
|
133
|
+
* the result object the whole time; only `--json` ever showed them.
|
|
134
|
+
*
|
|
135
|
+
* @param {{ processed?: number, results?: Array<{file?: string, ok?: boolean, status?: string, error?: string}> }} outcome
|
|
136
|
+
* @returns {number} 0 when every task succeeded, 1 when any failed.
|
|
137
|
+
*/
|
|
138
|
+
function reportRunOutcome(outcome) {
|
|
139
|
+
const items = Array.isArray(outcome?.results) ? outcome.results : [];
|
|
140
|
+
const failed = items.filter((r) => r && r.ok === false);
|
|
141
|
+
const succeeded = items.length - failed.length;
|
|
142
|
+
|
|
143
|
+
console.log(`\nProcessed ${outcome?.processed ?? items.length} task(s): ${succeeded} ok, ${failed.length} failed.`);
|
|
144
|
+
|
|
145
|
+
if (failed.length > 0) {
|
|
146
|
+
console.error(`\n❌ ${failed.length} task(s) did not dispatch:`);
|
|
147
|
+
for (const f of failed) {
|
|
148
|
+
const reason = f.error || f.status || "Unknown error";
|
|
149
|
+
console.error(` - ${f.file || f.taskId || "task"}: ${reason}`);
|
|
150
|
+
}
|
|
151
|
+
// A failed task is left in the queue rather than moved to completed/, so
|
|
152
|
+
// fixing the cause and re-running is the whole recovery procedure.
|
|
153
|
+
console.error(`\n These tasks are still queued. Fix the cause above and re-run.`);
|
|
154
|
+
return 1;
|
|
155
|
+
}
|
|
156
|
+
if (items.some((r) => r && r.dryRun)) {
|
|
157
|
+
console.log(` Dry run — no provider call was made and nothing was dispatched.`);
|
|
158
|
+
}
|
|
159
|
+
return 0;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Render the stage, exit code and captured output of a failed verify phase.
|
|
164
|
+
*
|
|
165
|
+
* VERIFY is the one gate phase whose failure the operator has to fix in their
|
|
166
|
+
* own code, and it was the only one that printed nothing beyond "❌ FAIL" —
|
|
167
|
+
* the command's output was captured, hashed into the evidence manifest, and
|
|
168
|
+
* then discarded before anyone could read it.
|
|
169
|
+
*
|
|
170
|
+
* @param {{ stageId?: string, command?: string|null, exitCode?: number|null, stdout?: string, stderr?: string, diagnostics?: string[] }} failure
|
|
171
|
+
*/
|
|
172
|
+
const VERIFY_OUTPUT_TAIL_LINES = 20;
|
|
173
|
+
|
|
174
|
+
function printVerifyFailure(failure) {
|
|
175
|
+
const exit = failure.exitCode === null || failure.exitCode === undefined ? "n/a" : failure.exitCode;
|
|
176
|
+
console.log(` - Stage: ${failure.stageId || "verify"} (exit ${exit})`);
|
|
177
|
+
if (failure.command) console.log(` - Command: ${failure.command}`);
|
|
178
|
+
for (const d of failure.diagnostics || []) console.log(` - ${d}`);
|
|
179
|
+
|
|
180
|
+
// stderr is where a failing suite says what it expected; stdout is the
|
|
181
|
+
// fallback for the runners that report everything there.
|
|
182
|
+
const output = (failure.stderr || "").trim() || (failure.stdout || "").trim();
|
|
183
|
+
if (!output) return;
|
|
184
|
+
const lines = output.split("\n");
|
|
185
|
+
const tail = lines.slice(-VERIFY_OUTPUT_TAIL_LINES);
|
|
186
|
+
console.log(` - Output${tail.length < lines.length ? ` (last ${VERIFY_OUTPUT_TAIL_LINES} of ${lines.length} lines)` : ""}:`);
|
|
187
|
+
for (const line of tail) console.log(` ${line}`);
|
|
188
|
+
}
|
|
94
189
|
|
|
95
190
|
async function main() {
|
|
96
191
|
if (command === "--help" || command === "-h") {
|
|
@@ -140,7 +235,7 @@ async function main() {
|
|
|
140
235
|
switch (command) {
|
|
141
236
|
case "dispatch":
|
|
142
237
|
case "create": {
|
|
143
|
-
const { values } = parseArgs({
|
|
238
|
+
const { values, positionals } = parseArgs({
|
|
144
239
|
args: args.slice(1),
|
|
145
240
|
options: {
|
|
146
241
|
title: { type: "string", short: "t" },
|
|
@@ -162,20 +257,28 @@ async function main() {
|
|
|
162
257
|
allowPositionals: true,
|
|
163
258
|
});
|
|
164
259
|
|
|
165
|
-
|
|
166
|
-
if (values["prompt-file"] && existsSync(values["prompt-file"])) {
|
|
167
|
-
promptContent = readFileSync(values["prompt-file"], "utf-8");
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
if (!promptContent && args[1] && !args[1].startsWith("-")) {
|
|
171
|
-
promptContent = args.slice(1).join(" ");
|
|
172
|
-
}
|
|
173
|
-
|
|
260
|
+
const promptContent = resolvePromptInput(values, positionals);
|
|
174
261
|
if (!promptContent) {
|
|
175
|
-
console.error("Error:
|
|
262
|
+
console.error("Error: a prompt is required — pass it as --prompt, --prompt-file, or a positional argument.");
|
|
176
263
|
process.exit(1);
|
|
177
264
|
}
|
|
178
265
|
|
|
266
|
+
// `task create --role` has always rejected a role it cannot resolve;
|
|
267
|
+
// `dispatch --role` used to drop it without a word and hand the work to a
|
|
268
|
+
// generic agent, so the two commands disagreed about what the same flag
|
|
269
|
+
// means. An explicitly typed role is a statement of intent — failing here
|
|
270
|
+
// is the only way the operator learns the prompt file is missing.
|
|
271
|
+
if (values.role) {
|
|
272
|
+
const { resolveRolePrompt } = await import("../src/role-resolver.mjs");
|
|
273
|
+
if (!resolveRolePrompt(root, values.role, { config })) {
|
|
274
|
+
console.error(
|
|
275
|
+
`Error: Unknown agent role '${values.role}'. Expected matching prompt file in .agent/prompts/ (e.g. Overseer, Bolt, Sentinel, Janitor).`
|
|
276
|
+
);
|
|
277
|
+
console.error(` Run 'agentctl init' to scaffold the shipped role prompts.`);
|
|
278
|
+
process.exit(1);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
179
282
|
const task = {
|
|
180
283
|
title: values.title || "CLI Dispatch Task",
|
|
181
284
|
prompt: promptContent,
|
|
@@ -206,6 +309,20 @@ async function main() {
|
|
|
206
309
|
if (session.status === "ALREADY_SATISFIED" || session.skipped) {
|
|
207
310
|
console.log(`\n⚡ Task Already Satisfied (skipped dispatch):`);
|
|
208
311
|
console.log(` Reason: ${session.reason || "Verification oracle already passing on base branch."}`);
|
|
312
|
+
} else if (values["dry-run"]) {
|
|
313
|
+
// The dry run reached the provider adapter and stopped short of the
|
|
314
|
+
// call. Printing the same "Dispatched Successfully!" banner as a
|
|
315
|
+
// real dispatch made the two indistinguishable in a terminal, so a
|
|
316
|
+
// rehearsal read as work in flight and the operator waited for a
|
|
317
|
+
// session that was never going to exist.
|
|
318
|
+
console.log(`\n🧪 Dry Run — nothing was dispatched.`);
|
|
319
|
+
console.log(` Title : ${task.title}`);
|
|
320
|
+
console.log(` Provider : ${session.provider || config.provider || "jules"}`);
|
|
321
|
+
if (task.role) console.log(` Role : ${task.role}`);
|
|
322
|
+
if (session._routeTier) {
|
|
323
|
+
console.log(` Router Tier : ${session._routeTier} (${session._routeReason || "n/a"})`);
|
|
324
|
+
}
|
|
325
|
+
console.log(`\n Re-run without --dry-run to dispatch for real.`);
|
|
209
326
|
} else {
|
|
210
327
|
console.log(`\n✅ Task Dispatched Successfully!`);
|
|
211
328
|
console.log(` Session ID : ${session.id}`);
|
|
@@ -228,6 +345,7 @@ async function main() {
|
|
|
228
345
|
break;
|
|
229
346
|
}
|
|
230
347
|
|
|
348
|
+
case "check":
|
|
231
349
|
case "gate":
|
|
232
350
|
case "audit": {
|
|
233
351
|
const { values } = parseArgs({
|
|
@@ -274,13 +392,32 @@ async function main() {
|
|
|
274
392
|
p.violations.forEach((v) => console.log(` - Violation: ${v.file} (Rule: ${v.rule})`));
|
|
275
393
|
}
|
|
276
394
|
if (p.findings) {
|
|
277
|
-
p.findings.forEach((f) => console.log(` -
|
|
395
|
+
p.findings.forEach((f) => console.log(` - [${f.severity}] ${f.type}: ${f.description}`));
|
|
396
|
+
}
|
|
397
|
+
if (!p.ok && p.failure) {
|
|
398
|
+
printVerifyFailure(p.failure);
|
|
278
399
|
}
|
|
279
400
|
}
|
|
280
401
|
console.log(`-----------------------------------------------------`);
|
|
281
402
|
console.log(`Overall Result: ${res.ok ? "APPROVED (Exit 0)" : `REJECTED (Exit ${res.code})`}\n`);
|
|
282
403
|
if (!res.ok) {
|
|
283
|
-
|
|
404
|
+
const failedPhase = res.phases.find((p) => !p.ok)?.phase;
|
|
405
|
+
// Exit 3 is also what a strictTestLock tamper verdict returns, so the
|
|
406
|
+
// code alone cannot pick the hint — a scope remediation for a rewritten
|
|
407
|
+
// test file sends the operator to the wrong flag entirely.
|
|
408
|
+
if (res.repairs) {
|
|
409
|
+
console.log(`💡 Remediation Hint (Exit 4 OODA Repair Exhausted):`);
|
|
410
|
+
console.log(` • Automated self-repair could not pass tests cleanly.`);
|
|
411
|
+
console.log(` • Review error fingerprints via: agentctl doctor\n`);
|
|
412
|
+
} else if (res.flakyVerdict?.verdict === "QUARANTINED") {
|
|
413
|
+
console.log(`💡 Remediation Hint (Exit 8 Flaky Test Quarantined):`);
|
|
414
|
+
console.log(` • This command has alternated between pass and fail across recent runs.`);
|
|
415
|
+
console.log(` • Fix the test's non-determinism — re-running will not clear the verdict.\n`);
|
|
416
|
+
} else if (failedPhase === "verify" || failedPhase === "evidence") {
|
|
417
|
+
console.log(`💡 Remediation Hint (Exit ${res.code} Verification Failed):`);
|
|
418
|
+
console.log(` • The stage above exited non-zero. Reproduce it locally, then re-run the gate.`);
|
|
419
|
+
console.log(` • To let agentctl attempt the repair loop itself, pass: agentctl gate --fix\n`);
|
|
420
|
+
} else if (res.code === 3) {
|
|
284
421
|
console.log(`💡 Remediation Hint (Exit 3 Scope Violation):`);
|
|
285
422
|
console.log(` • To allow protected files in this run, pass: agentctl gate --allow-protected`);
|
|
286
423
|
console.log(` • Or remove protected/denied paths from the diff before dispatching.\n`);
|
|
@@ -292,10 +429,6 @@ async function main() {
|
|
|
292
429
|
console.log(`💡 Remediation Hint (Exit 6 Secret Leak Prevented):`);
|
|
293
430
|
console.log(` • High-entropy credential or secret detected in patch.`);
|
|
294
431
|
console.log(` • Scrub credential from source and rotate any exposed keys immediately.\n`);
|
|
295
|
-
} else if (res.code === 4) {
|
|
296
|
-
console.log(`💡 Remediation Hint (Exit 4 OODA Repair Exhausted):`);
|
|
297
|
-
console.log(` • Automated self-repair could not pass tests cleanly.`);
|
|
298
|
-
console.log(` • Review error fingerprints via: agentctl doctor\n`);
|
|
299
432
|
}
|
|
300
433
|
}
|
|
301
434
|
}
|
|
@@ -417,9 +550,10 @@ async function main() {
|
|
|
417
550
|
});
|
|
418
551
|
if (values.json) {
|
|
419
552
|
console.log(JSON.stringify(results, null, 2));
|
|
420
|
-
|
|
421
|
-
|
|
553
|
+
const anyFailed = (results.results || []).some((r) => r && r.ok === false);
|
|
554
|
+
process.exit(anyFailed ? 1 : 0);
|
|
422
555
|
}
|
|
556
|
+
process.exit(reportRunOutcome(results));
|
|
423
557
|
}
|
|
424
558
|
process.exit(0);
|
|
425
559
|
break;
|
|
@@ -439,8 +573,7 @@ async function main() {
|
|
|
439
573
|
prompt: readFileSync(join(queueDir, f), "utf-8"),
|
|
440
574
|
}));
|
|
441
575
|
const results = await run(tasks, { root, config, concurrency: config.limits.concurrency || 3 });
|
|
442
|
-
|
|
443
|
-
process.exit(0);
|
|
576
|
+
process.exit(reportRunOutcome(results));
|
|
444
577
|
break;
|
|
445
578
|
}
|
|
446
579
|
|
|
@@ -679,6 +812,7 @@ async function main() {
|
|
|
679
812
|
tier: { type: "string", short: "t" },
|
|
680
813
|
json: { type: "boolean", short: "j" },
|
|
681
814
|
"dry-run": { type: "boolean", short: "d" },
|
|
815
|
+
force: { type: "boolean", short: "f" },
|
|
682
816
|
},
|
|
683
817
|
allowPositionals: true,
|
|
684
818
|
});
|
|
@@ -694,13 +828,38 @@ async function main() {
|
|
|
694
828
|
allowDefaults: true,
|
|
695
829
|
});
|
|
696
830
|
|
|
831
|
+
// The wizard writes the manifest; the assets the CLI's documented
|
|
832
|
+
// features actually need — AGENTS.md, the role prompts, the guardrails,
|
|
833
|
+
// the gitignore entries — used to be scaffolded only by the separate
|
|
834
|
+
// `jules-init` binary that the README's quickstart never mentions.
|
|
835
|
+
const { scaffoldRepoAssets } = await import("../src/scaffold.mjs");
|
|
836
|
+
const scaffold = scaffoldRepoAssets(root, { force: values.force });
|
|
837
|
+
|
|
697
838
|
if (values.json) {
|
|
698
|
-
console.log(JSON.stringify(res, null, 2));
|
|
839
|
+
console.log(JSON.stringify({ ...res, scaffold }, null, 2));
|
|
699
840
|
} else {
|
|
700
841
|
console.log(`✅ Onboarding complete! Manifest generated at ${res.configPath}`);
|
|
701
842
|
console.log(` Tier: ${res.plan.tier.toUpperCase()} (${res.plan.limits.concurrency} worker(s), ${res.plan.limits.daily_tasks} daily tasks)`);
|
|
702
843
|
console.log(` Verification Test Command : "${res.plan.verify.test}"`);
|
|
703
844
|
console.log(` Active Presets : ${res.plan.presets.join(", ")}`);
|
|
845
|
+
for (const item of scaffold.created) {
|
|
846
|
+
console.log(` Scaffolded : ${item}`);
|
|
847
|
+
}
|
|
848
|
+
if (scaffold.gitignore.length > 0) {
|
|
849
|
+
console.log(` Ignored runtime state : ${scaffold.gitignore.length} entries added to .gitignore`);
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
// `.agent/config.yml` and `.agent/jules.yml` are both on the gate's deny
|
|
853
|
+
// list, by design — the agent must not edit its own rules. Leaving them
|
|
854
|
+
// uncommitted meant the very first `agentctl gate` rejected the working
|
|
855
|
+
// tree for files init had just written, which reads as the tool
|
|
856
|
+
// catching the user cheating on step three.
|
|
857
|
+
console.log(`\n Commit the manifest so the gate does not read it as an agent edit:`);
|
|
858
|
+
console.log(` git add .agent AGENTS.md .gitignore && git commit -m "chore: add agent config"`);
|
|
859
|
+
|
|
860
|
+
const { resolveNextStep, renderNextStep } = await import("../src/ops/next-step.mjs");
|
|
861
|
+
const next = resolveNextStep(root);
|
|
862
|
+
console.log(renderNextStep({ version: VERSION, root, next, budgetLine: "" }));
|
|
704
863
|
}
|
|
705
864
|
process.exit(0);
|
|
706
865
|
break;
|
|
@@ -709,11 +868,12 @@ async function main() {
|
|
|
709
868
|
case "task": {
|
|
710
869
|
const subCommand = args[1] || "create";
|
|
711
870
|
if (subCommand === "create") {
|
|
712
|
-
const { values } = parseArgs({
|
|
871
|
+
const { values, positionals } = parseArgs({
|
|
713
872
|
args: args.slice(2),
|
|
714
873
|
options: {
|
|
715
874
|
title: { type: "string", short: "t" },
|
|
716
875
|
prompt: { type: "string", short: "p" },
|
|
876
|
+
"prompt-file": { type: "string", short: "f" },
|
|
717
877
|
role: { type: "string", short: "r" },
|
|
718
878
|
tier: { type: "string" },
|
|
719
879
|
template: { type: "string" },
|
|
@@ -732,7 +892,7 @@ async function main() {
|
|
|
732
892
|
const { runTaskCreateWizard } = await import("../src/wizard-task.mjs");
|
|
733
893
|
const res = await runTaskCreateWizard(root, {
|
|
734
894
|
title: values.title,
|
|
735
|
-
prompt: values
|
|
895
|
+
prompt: resolvePromptInput(values, positionals),
|
|
736
896
|
role: values.role,
|
|
737
897
|
tier: values.tier,
|
|
738
898
|
template: values.template,
|
|
@@ -806,6 +966,12 @@ async function main() {
|
|
|
806
966
|
args: args.slice(2),
|
|
807
967
|
options: {
|
|
808
968
|
fix: { type: "boolean", short: "f" },
|
|
969
|
+
prompt: { type: "string", short: "p" },
|
|
970
|
+
// No short form here: `-f` is already --fix on this subcommand, and
|
|
971
|
+
// silently meaning two different things would be worse than one
|
|
972
|
+
// command having a flag short of full parity. `--file` predates
|
|
973
|
+
// `--prompt-file` and stays as an alias.
|
|
974
|
+
"prompt-file": { type: "string" },
|
|
809
975
|
file: { type: "string" },
|
|
810
976
|
dir: { type: "string", short: "d" },
|
|
811
977
|
web: { type: "boolean", short: "w" },
|
|
@@ -818,16 +984,7 @@ async function main() {
|
|
|
818
984
|
|
|
819
985
|
const { scorePromptFalsifiability, optimizeTaskPrompt } = await import("../src/task-optimizer.mjs");
|
|
820
986
|
const targetDir = values.dir ? resolve(values.dir) : root;
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
if (values.file) {
|
|
824
|
-
if (existsSync(values.file)) {
|
|
825
|
-
promptText = readFileSync(values.file, "utf-8");
|
|
826
|
-
} else {
|
|
827
|
-
console.error(`Error: File '${values.file}' does not exist.`);
|
|
828
|
-
process.exit(1);
|
|
829
|
-
}
|
|
830
|
-
}
|
|
987
|
+
const promptText = resolvePromptInput(values, positionals);
|
|
831
988
|
|
|
832
989
|
if (values.fix) {
|
|
833
990
|
const opt = optimizeTaskPrompt(promptText, { rootDir: targetDir, verifyCmd: values["verify-cmd"], web: values.web });
|
|
@@ -910,6 +1067,12 @@ async function main() {
|
|
|
910
1067
|
intent: { type: "string", short: "i" },
|
|
911
1068
|
handover: { type: "boolean", default: true },
|
|
912
1069
|
json: { type: "boolean", short: "j" },
|
|
1070
|
+
// Documented in the README as `rollback [sessionId | --latest]` but
|
|
1071
|
+
// never registered, so the documented spelling died in parseArgs
|
|
1072
|
+
// before restoreCheckpoint — which has always accepted it — was
|
|
1073
|
+
// reached. Restoring the newest checkpoint is also the no-argument
|
|
1074
|
+
// default, so the flag is explicit-intent sugar rather than a mode.
|
|
1075
|
+
latest: { type: "boolean" },
|
|
913
1076
|
},
|
|
914
1077
|
allowPositionals: true,
|
|
915
1078
|
});
|
|
@@ -1728,6 +1891,76 @@ async function main() {
|
|
|
1728
1891
|
break;
|
|
1729
1892
|
}
|
|
1730
1893
|
|
|
1894
|
+
case "rules": {
|
|
1895
|
+
const subcmd = args[1] || "check";
|
|
1896
|
+
const { checkRulesBudget, compileRules } = await import("../src/rules_budget.mjs");
|
|
1897
|
+
|
|
1898
|
+
if (subcmd === "check") {
|
|
1899
|
+
const { values } = parseArgs({
|
|
1900
|
+
args: args.slice(2),
|
|
1901
|
+
options: {
|
|
1902
|
+
json: { type: "boolean", short: "j" },
|
|
1903
|
+
"max-chars": { type: "string" },
|
|
1904
|
+
"max-lines": { type: "string" },
|
|
1905
|
+
},
|
|
1906
|
+
allowPositionals: true,
|
|
1907
|
+
});
|
|
1908
|
+
|
|
1909
|
+
const res = checkRulesBudget(root, {
|
|
1910
|
+
maxChars: values["max-chars"] ? Number(values["max-chars"]) : undefined,
|
|
1911
|
+
maxLines: values["max-lines"] ? Number(values["max-lines"]) : undefined,
|
|
1912
|
+
});
|
|
1913
|
+
|
|
1914
|
+
if (values.json) {
|
|
1915
|
+
console.log(JSON.stringify(res, null, 2));
|
|
1916
|
+
} else {
|
|
1917
|
+
console.log("\n📏 agentctl Rules Budget & Line Audit");
|
|
1918
|
+
console.log("-----------------------------------------------------");
|
|
1919
|
+
if (res.ok) {
|
|
1920
|
+
console.log("✅ All agent rule files are within safe character (<10,000) and line (<250) limits.");
|
|
1921
|
+
} else {
|
|
1922
|
+
console.log("❌ RULES BUDGET VIOLATIONS DETECTED:");
|
|
1923
|
+
for (const v of res.violations) {
|
|
1924
|
+
console.log(` - ${v.path}: ${v.reason}`);
|
|
1925
|
+
}
|
|
1926
|
+
console.log("\n💡 Remediation: Trim prose rules or convert textual learnings into AST lints / assertions.");
|
|
1927
|
+
}
|
|
1928
|
+
console.log("-----------------------------------------------------\n");
|
|
1929
|
+
}
|
|
1930
|
+
process.exit(res.ok ? 0 : 1);
|
|
1931
|
+
} else if (subcmd === "compile") {
|
|
1932
|
+
const { values } = parseArgs({
|
|
1933
|
+
args: args.slice(2),
|
|
1934
|
+
options: {
|
|
1935
|
+
out: { type: "string", short: "o" },
|
|
1936
|
+
json: { type: "boolean", short: "j" },
|
|
1937
|
+
},
|
|
1938
|
+
allowPositionals: true,
|
|
1939
|
+
});
|
|
1940
|
+
|
|
1941
|
+
const res = compileRules(root);
|
|
1942
|
+
if (values.out) {
|
|
1943
|
+
const { writeFileSync } = await import("node:fs");
|
|
1944
|
+
const { resolve } = await import("node:path");
|
|
1945
|
+
writeFileSync(resolve(root, values.out), res.compiled, "utf-8");
|
|
1946
|
+
if (values.json) {
|
|
1947
|
+
console.log(JSON.stringify({ ok: true, out: values.out, sha256: res.sha256, bodyLen: res.bodyLen, sources: res.sources }, null, 2));
|
|
1948
|
+
} else {
|
|
1949
|
+
console.log(`✅ Compiled ${res.sources.length} rule source(s) into ${values.out} (SHA-256: ${res.sha256.slice(0, 12)}..., ${res.bodyLen} bytes)`);
|
|
1950
|
+
}
|
|
1951
|
+
} else if (values.json) {
|
|
1952
|
+
console.log(JSON.stringify(res, null, 2));
|
|
1953
|
+
} else {
|
|
1954
|
+
console.log(res.compiled);
|
|
1955
|
+
}
|
|
1956
|
+
process.exit(0);
|
|
1957
|
+
} else {
|
|
1958
|
+
console.error(`Unknown rules action: "${subcmd}". Usage: agentctl rules [check | compile]`);
|
|
1959
|
+
process.exit(1);
|
|
1960
|
+
}
|
|
1961
|
+
break;
|
|
1962
|
+
}
|
|
1963
|
+
|
|
1731
1964
|
default:
|
|
1732
1965
|
console.error(`Unknown command: ${command}`);
|
|
1733
1966
|
printHelp();
|
package/bin/init.js
CHANGED
|
@@ -117,53 +117,17 @@ if (detected.testCmd || detected.buildCmd) {
|
|
|
117
117
|
console.log(` - Build Command: ${detected.buildCmd || "(none)"}`);
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
-
// 2. Scaffold AGENTS.md
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
}
|
|
129
|
-
} else {
|
|
130
|
-
const existingContent = fs.readFileSync(agentsFile, "utf-8");
|
|
131
|
-
if (!existingContent.includes("<MCP_DIRECTIVE>")) {
|
|
132
|
-
if (fs.existsSync(julesRulesSource)) {
|
|
133
|
-
const templateContent = fs.readFileSync(julesRulesSource, "utf-8");
|
|
134
|
-
fs.appendFileSync(agentsFile, `\n\n---\n\n${templateContent}`, "utf-8");
|
|
135
|
-
console.log("✅ Appended Google Jules directives to existing AGENTS.md");
|
|
136
|
-
}
|
|
137
|
-
} else {
|
|
138
|
-
console.log("ℹ️ AGENTS.md already contains Jules directives (skipped overwrite).");
|
|
139
|
-
}
|
|
120
|
+
// 2-3. Scaffold AGENTS.md, .agent/ structure, role prompts, rules and workflows.
|
|
121
|
+
// Shared with `agentctl init` so the two entry points cannot scaffold different
|
|
122
|
+
// repositories — which is exactly what they used to do, with the README's
|
|
123
|
+
// quickstart pointing at the one that scaffolded less.
|
|
124
|
+
const { scaffoldRepoAssets } = await import("../src/scaffold.mjs");
|
|
125
|
+
const scaffolded = scaffoldRepoAssets(targetDir, { force: isForce });
|
|
126
|
+
for (const item of scaffolded.created) {
|
|
127
|
+
console.log(`✅ Created: ${item}`);
|
|
140
128
|
}
|
|
141
129
|
|
|
142
|
-
// 3. Scaffold .agent/ structure
|
|
143
130
|
const agentDir = path.join(targetDir, ".agent");
|
|
144
|
-
const rulesDir = path.join(agentDir, "rules");
|
|
145
|
-
const queueDir = path.join(agentDir, "jules-queue");
|
|
146
|
-
const completedQueueDir = path.join(queueDir, "completed");
|
|
147
|
-
const workflowsDir = path.join(agentDir, "workflows");
|
|
148
|
-
const promptsDir = path.join(agentDir, "prompts");
|
|
149
|
-
|
|
150
|
-
[agentDir, rulesDir, queueDir, completedQueueDir, workflowsDir, promptsDir].forEach((d) => {
|
|
151
|
-
if (!fs.existsSync(d)) fs.mkdirSync(d, { recursive: true });
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
// Scaffold .agent/prompts files
|
|
155
|
-
const sourcePromptsDir = path.join(kitRoot, ".agent/prompts");
|
|
156
|
-
if (fs.existsSync(sourcePromptsDir)) {
|
|
157
|
-
const promptFiles = fs.readdirSync(sourcePromptsDir);
|
|
158
|
-
promptFiles.forEach((file) => {
|
|
159
|
-
const srcPrompt = path.join(sourcePromptsDir, file);
|
|
160
|
-
const destPrompt = path.join(promptsDir, file);
|
|
161
|
-
if (!fs.existsSync(destPrompt) || isForce) {
|
|
162
|
-
fs.copyFileSync(srcPrompt, destPrompt);
|
|
163
|
-
}
|
|
164
|
-
});
|
|
165
|
-
console.log("✅ Created: .agent/prompts presets (Overseer, Bolt, Sentinel, Janitor, Task_Template)");
|
|
166
|
-
}
|
|
167
131
|
|
|
168
132
|
// Scaffold .agent/jules.yml
|
|
169
133
|
const yamlConfigPath = path.join(agentDir, "jules.yml");
|
|
@@ -178,22 +142,6 @@ forbidden_paths: [".github/**", "**/.env*", "**/*.pem", "**/lock-manager*"]
|
|
|
178
142
|
console.log("✅ Created: .agent/jules.yml");
|
|
179
143
|
}
|
|
180
144
|
|
|
181
|
-
// Scaffold .agent/rules/dynamic-guardrails.json
|
|
182
|
-
const dgcSource = path.join(kitRoot, ".agent/rules/dynamic-guardrails.json");
|
|
183
|
-
const dgcTarget = path.join(rulesDir, "dynamic-guardrails.json");
|
|
184
|
-
if ((!fs.existsSync(dgcTarget) || isForce) && fs.existsSync(dgcSource)) {
|
|
185
|
-
fs.copyFileSync(dgcSource, dgcTarget);
|
|
186
|
-
console.log("✅ Created: .agent/rules/dynamic-guardrails.json");
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
// Scaffold .agent/workflows/jules-review.md
|
|
190
|
-
const reviewSource = path.join(kitRoot, ".agent/workflows/jules-review.md");
|
|
191
|
-
const reviewTarget = path.join(workflowsDir, "jules-review.md");
|
|
192
|
-
if ((!fs.existsSync(reviewTarget) || isForce) && fs.existsSync(reviewSource)) {
|
|
193
|
-
fs.copyFileSync(reviewSource, reviewTarget);
|
|
194
|
-
console.log("✅ Created: .agent/workflows/jules-review.md");
|
|
195
|
-
}
|
|
196
|
-
|
|
197
145
|
// Scaffold .github/workflows/jules-audit.yml
|
|
198
146
|
const githubWorkflowsDir = path.join(targetDir, ".github/workflows");
|
|
199
147
|
const auditWfSource = path.join(kitRoot, ".github/workflows/jules-audit.yml");
|
|
@@ -275,32 +223,10 @@ if (fs.existsSync(targetPkgPath) && targetDir !== kitRoot) {
|
|
|
275
223
|
}
|
|
276
224
|
}
|
|
277
225
|
|
|
278
|
-
// 5b.
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
".agent/history/",
|
|
283
|
-
".agent/state/",
|
|
284
|
-
".agent/jules-queue/.state/",
|
|
285
|
-
".agent/jules-queue/failed/",
|
|
286
|
-
".agent/jules-queue/.processing/",
|
|
287
|
-
".agent/jules-queue/*.md",
|
|
288
|
-
"!.agent/jules-queue/README.md"
|
|
289
|
-
];
|
|
290
|
-
|
|
291
|
-
let gitignoreContent = fs.existsSync(targetGitignorePath)
|
|
292
|
-
? fs.readFileSync(targetGitignorePath, "utf-8")
|
|
293
|
-
: "";
|
|
294
|
-
|
|
295
|
-
const missingEntries = requiredGitignoreEntries.filter(
|
|
296
|
-
(entry) => !gitignoreContent.includes(entry)
|
|
297
|
-
);
|
|
298
|
-
|
|
299
|
-
if (missingEntries.length > 0) {
|
|
300
|
-
const prefix = gitignoreContent && !gitignoreContent.endsWith("\n") ? "\n" : "";
|
|
301
|
-
const addedBlock = `${prefix}# Jules Orchestrator Runtime State & Credentials\n${missingEntries.join("\n")}\n`;
|
|
302
|
-
fs.appendFileSync(targetGitignorePath, addedBlock, "utf-8");
|
|
303
|
-
console.log("✅ Added required security entries to .gitignore");
|
|
226
|
+
// 5b. The .gitignore entries are written by scaffoldRepoAssets above, so the
|
|
227
|
+
// two entry points cannot disagree about which runtime paths stay untracked.
|
|
228
|
+
if (scaffolded.gitignore.length > 0) {
|
|
229
|
+
console.log(`✅ Added ${scaffolded.gitignore.length} runtime state entries to .gitignore`);
|
|
304
230
|
}
|
|
305
231
|
|
|
306
232
|
console.log("\n🎉 Google Jules Orchestration Kit successfully initialized!");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jules-orchestrator-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.42.0",
|
|
4
4
|
"description": "Zero-dependency safety gatekeeper, test oracle generator, and multi-agent coordination protocol for Google Jules (jules) autonomous agents.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|