continuous-improvement 3.19.0 → 3.20.4
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/.claude-plugin/marketplace.json +1 -1
- package/QUICKSTART.md +1 -1
- package/README.md +3 -2
- package/bin/check-landing-version.mjs +63 -0
- package/bin/check-scripts-citation-drift.mjs +61 -13
- package/bin/generate-plugin-manifests.mjs +3 -0
- package/bin/install.mjs +19 -11
- package/commands/verify-install.md +1 -1
- package/hooks/gateguard.mjs +22 -3
- package/hooks/query-cost-nudge.mjs +114 -0
- package/hooks/typecheck-stop.mjs +2 -1
- package/lib/plugin-metadata.mjs +7 -2
- package/lib/query-cost-gate.mjs +53 -0
- package/package.json +5 -3
- package/plugins/beginner.json +1 -1
- package/plugins/continuous-improvement/.claude-plugin/marketplace.json +1 -1
- package/plugins/continuous-improvement/.claude-plugin/plugin.json +1 -1
- package/plugins/continuous-improvement/README.md +1 -0
- package/plugins/continuous-improvement/commands/verify-install.md +1 -1
- package/plugins/continuous-improvement/hooks/gateguard.mjs +22 -3
- package/plugins/continuous-improvement/hooks/hooks.json +6 -1
- package/plugins/continuous-improvement/hooks/query-cost-nudge.mjs +114 -0
- package/plugins/continuous-improvement/hooks/typecheck-stop.mjs +2 -1
- package/plugins/continuous-improvement/lib/plugin-metadata.mjs +7 -2
- package/plugins/continuous-improvement/lib/query-cost-gate.mjs +53 -0
- package/plugins/continuous-improvement/scripts/README.md +33 -0
- package/plugins/continuous-improvement/scripts/detect-deploy-target.sh +66 -0
- package/plugins/continuous-improvement/scripts/get-deployed-sha.sh +113 -0
- package/plugins/continuous-improvement/scripts/git-state-snapshot.sh +48 -0
- package/plugins/continuous-improvement/scripts/resolve-verify-ladder.mjs +241 -0
- package/plugins/continuous-improvement/scripts/route-recommendation.mjs +178 -0
- package/plugins/continuous-improvement/scripts/route-recommendation.routes.json +213 -0
- package/plugins/continuous-improvement/scripts/run-synthetic.mjs +298 -0
- package/plugins/continuous-improvement/scripts/scan-past-mistakes.mjs +285 -0
- package/plugins/continuous-improvement/skills/deploy-receipt/SKILL.md +2 -2
- package/plugins/continuous-improvement/skills/gateguard/SKILL.md +2 -2
- package/plugins/continuous-improvement/skills/proceed-with-the-recommendation/SKILL.md +2 -2
- package/plugins/continuous-improvement/skills/reconcile/SKILL.md +1 -1
- package/plugins/continuous-improvement/skills/verification-loop/SKILL.md +5 -5
- package/plugins/continuous-improvement/skills/workspace-surface-audit/SKILL.md +1 -1
- package/plugins/continuous-improvement/skills/worktree-safety/SKILL.md +1 -1
- package/plugins/expert.json +1 -1
- package/scripts/README.md +33 -0
- package/scripts/detect-deploy-target.sh +66 -0
- package/scripts/get-deployed-sha.sh +113 -0
- package/scripts/git-state-snapshot.sh +48 -0
- package/scripts/resolve-verify-ladder.mjs +241 -0
- package/scripts/route-recommendation.mjs +178 -0
- package/scripts/route-recommendation.routes.json +213 -0
- package/scripts/run-synthetic.mjs +298 -0
- package/scripts/scan-past-mistakes.mjs +285 -0
- package/skills/deploy-receipt.md +2 -2
- package/skills/gateguard.md +2 -2
- package/skills/proceed-with-the-recommendation.md +2 -2
- package/skills/reconcile.md +1 -1
- package/skills/verification-loop.md +5 -5
- package/skills/workspace-surface-audit.md +1 -1
- package/skills/worktree-safety.md +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "continuous-improvement",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.20.4",
|
|
4
4
|
"description": "Claude Code that gets sharper every session: the persistent-memory and runtime-discipline layer built on the 7 Laws of AI Agent Discipline. It grounds every edit in real facts before it lands and, through the Mulahazah engine, turns each fix into a reusable instinct, so a lesson learned once is applied automatically next time with no re-teaching. Shipped as 27 bundled skills, instinct-aware hooks, an MCP toolset for recall and reflection, and a GitHub Action transcript linter that feeds real work history back into sharper instincts. Beginner: one /plugin install command. Expert: adds MCP tools and session hooks.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude-code",
|
|
@@ -56,10 +56,11 @@
|
|
|
56
56
|
"verify:routing-targets": "node bin/check-routing-targets.mjs",
|
|
57
57
|
"verify:doc-runtime-claims": "node bin/check-doc-runtime-claims.mjs",
|
|
58
58
|
"verify:test-imports-only": "node bin/check-test-imports-only.mjs",
|
|
59
|
-
"verify:
|
|
59
|
+
"verify:landing-version": "node bin/check-landing-version.mjs",
|
|
60
|
+
"verify:scripts-citation-drift": "node bin/check-scripts-citation-drift.mjs && node bin/check-scripts-citation-drift.mjs plugins/continuous-improvement",
|
|
60
61
|
"verify:third-party-shape": "node bin/check-third-party-shape.mjs",
|
|
61
62
|
"verify:tool-count": "node bin/check-tool-count.mjs",
|
|
62
|
-
"verify:all": "npm run verify:skill-mirror && npm run verify:skill-tiers && npm run verify:skill-law-tag && npm run verify:skill-count && npm run verify:skill-count-prose && npm run verify:command-count && npm run verify:docs-substrings && npm run verify:everything-mirror && npm run verify:routing-targets && npm run verify:doc-runtime-claims && npm run verify:test-imports-only && npm run verify:scripts-citation-drift && npm run verify:third-party-shape && npm run verify:tool-count && npm run typecheck"
|
|
63
|
+
"verify:all": "npm run verify:skill-mirror && npm run verify:skill-tiers && npm run verify:skill-law-tag && npm run verify:skill-count && npm run verify:skill-count-prose && npm run verify:command-count && npm run verify:docs-substrings && npm run verify:everything-mirror && npm run verify:routing-targets && npm run verify:doc-runtime-claims && npm run verify:test-imports-only && npm run verify:landing-version && npm run verify:scripts-citation-drift && npm run verify:third-party-shape && npm run verify:tool-count && npm run typecheck"
|
|
63
64
|
},
|
|
64
65
|
"files": [
|
|
65
66
|
".claude-plugin/",
|
|
@@ -74,6 +75,7 @@
|
|
|
74
75
|
"hooks/",
|
|
75
76
|
"commands/",
|
|
76
77
|
"skills/",
|
|
78
|
+
"scripts/",
|
|
77
79
|
"templates/",
|
|
78
80
|
"plugins/",
|
|
79
81
|
"instinct-packs/"
|
package/plugins/beginner.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "continuous-improvement",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.20.4",
|
|
4
4
|
"mode": "beginner",
|
|
5
5
|
"description": "Beginner mode: see what your agent learned, list its instincts, and request a session reflection. Bundles three grounding skills (gateguard, tdd-workflow, verification-loop) so research, memory, tests, and verification happen by default — every edit starts from facts, not guesses.",
|
|
6
6
|
"tools": [
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
{
|
|
9
9
|
"name": "continuous-improvement",
|
|
10
10
|
"description": "The persistent-memory and runtime-discipline layer for Claude Code. It remembers the corrections you already gave, grounds every edit in real facts before it lands, and — through the Mulahazah engine — turns each fix into a reusable instinct, so a lesson learned once is applied automatically next time with no re-teaching. Built on the 7 Laws of AI Agent Discipline (research, plan, verify, reflect, learn) and shipped as 27 bundled skills, instinct-aware hooks, an MCP toolset for recall and reflection, and a GitHub Action transcript linter that feeds real work history back into sharper instincts.",
|
|
11
|
-
"version": "3.
|
|
11
|
+
"version": "3.20.4",
|
|
12
12
|
"source": "./",
|
|
13
13
|
"author": {
|
|
14
14
|
"name": "naimkatiman"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "continuous-improvement",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.20.4",
|
|
4
4
|
"description": "The persistent-memory and runtime-discipline layer for Claude Code. It remembers the corrections you already gave, grounds every edit in real facts before it lands, and — through the Mulahazah engine — turns each fix into a reusable instinct, so a lesson learned once is applied automatically next time with no re-teaching. Built on the 7 Laws of AI Agent Discipline (research, plan, verify, reflect, learn) and shipped as 27 bundled skills, instinct-aware hooks, an MCP toolset for recall and reflection, and a GitHub Action transcript linter that feeds real work history back into sharper instincts.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "naimkatiman",
|
|
@@ -10,6 +10,7 @@ Included surfaces:
|
|
|
10
10
|
- `commands/`
|
|
11
11
|
- `agents/` — `code-reviewer`, `security-auditor`, `test-engineer` personas (auto-discovered Claude Code subagents; pattern from addy/agent-skills)
|
|
12
12
|
- `hooks/`
|
|
13
|
+
- `scripts/`
|
|
13
14
|
- `bin/mcp-server.mjs`
|
|
14
15
|
- `bin/observe.mjs`
|
|
15
16
|
- `bin/backfill.mjs`
|
|
@@ -41,7 +41,7 @@ The observation hook appends one row per tool call to
|
|
|
41
41
|
|
|
42
42
|
- If it exists and has at least one row — capture is recording. Record `observe: ✓`.
|
|
43
43
|
- If it is missing or empty — record `observe: ✗ (observation hook not recording —
|
|
44
|
-
on Windows confirm Git Bash
|
|
44
|
+
on Windows confirm Git Bash is installed before WSL Bash on PATH, then re-run the installer)`.
|
|
45
45
|
|
|
46
46
|
## Report
|
|
47
47
|
|
|
@@ -210,6 +210,16 @@ function buildMutatingFileReason(toolName, filePaths, stateFilePath) {
|
|
|
210
210
|
" `_gateguard_facts_presented: true`; Claude Code's strict schema rejects that, so use A or B.)",
|
|
211
211
|
].join("\n");
|
|
212
212
|
}
|
|
213
|
+
function isGitBraceSelector(value) {
|
|
214
|
+
const selector = value.trim();
|
|
215
|
+
if (/^(?:u|upstream|push|-?\d+)$/i.test(selector))
|
|
216
|
+
return true;
|
|
217
|
+
if (/^(?:now|today|yesterday|tomorrow|noon|midnight|tea)$/i.test(selector))
|
|
218
|
+
return true;
|
|
219
|
+
if (/^\d{4}-\d{1,2}-\d{1,2}(?:[ T].*)?$/.test(selector))
|
|
220
|
+
return true;
|
|
221
|
+
return !selector.startsWith("#") && /(?:^|[.\s])ago$/i.test(selector);
|
|
222
|
+
}
|
|
213
223
|
function findUnquotedBraceRef(command) {
|
|
214
224
|
let quote = null;
|
|
215
225
|
for (let i = 0; i < command.length; i++) {
|
|
@@ -224,17 +234,26 @@ function findUnquotedBraceRef(command) {
|
|
|
224
234
|
continue;
|
|
225
235
|
}
|
|
226
236
|
if (ch === "@" && command[i + 1] === "{") {
|
|
237
|
+
const braceEnd = command.indexOf("}", i + 2);
|
|
238
|
+
if (braceEnd === -1)
|
|
239
|
+
continue;
|
|
240
|
+
const selector = command.slice(i + 2, braceEnd);
|
|
241
|
+
// Deny only recognized Git selector grammar. PowerShell hashtables can
|
|
242
|
+
// contain comments, quoted braces, or arbitrary key expressions.
|
|
243
|
+
if (!isGitBraceSelector(selector)) {
|
|
244
|
+
i = braceEnd;
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
227
247
|
// Expand to the whitespace-delimited word that carries this @{ ref, then
|
|
228
248
|
// single-quote that whole word in the suggested fix.
|
|
229
249
|
let wordStart = i;
|
|
230
250
|
while (wordStart > 0 && !/\s/.test(command[wordStart - 1]))
|
|
231
251
|
wordStart--;
|
|
232
|
-
let wordEnd =
|
|
252
|
+
let wordEnd = braceEnd + 1;
|
|
233
253
|
while (wordEnd < command.length && !/\s/.test(command[wordEnd]))
|
|
234
254
|
wordEnd++;
|
|
235
255
|
const word = command.slice(wordStart, wordEnd);
|
|
236
|
-
const
|
|
237
|
-
const ref = braceEnd === -1 ? command.slice(i, wordEnd) : command.slice(i, braceEnd + 1);
|
|
256
|
+
const ref = command.slice(i, braceEnd + 1);
|
|
238
257
|
const fixed = `${command.slice(0, wordStart)}'${word}'${command.slice(wordEnd)}`;
|
|
239
258
|
return { ref, fixed };
|
|
240
259
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"description": "Gateguard fact-forcing PreToolUse, companion-preference enforcement, observation, session lifecycle, 3-section-close discipline, goal-drift Stop gate, opt-in workflow-distill Stop nudge, opt-in typecheck Stop gate, and UserPromptSubmit lazy-routing plus opt-in proactive recall-briefing hooks for continuous-improvement.",
|
|
2
|
+
"description": "Gateguard fact-forcing PreToolUse, companion-preference enforcement, observation, session lifecycle, 3-section-close discipline, goal-drift Stop gate, opt-in workflow-distill Stop nudge, opt-in typecheck Stop gate, opt-in query-cost Stop nudge, and UserPromptSubmit lazy-routing plus opt-in proactive recall-briefing hooks for continuous-improvement.",
|
|
3
3
|
"hooks": {
|
|
4
4
|
"PreToolUse": [
|
|
5
5
|
{
|
|
@@ -98,6 +98,11 @@
|
|
|
98
98
|
"type": "command",
|
|
99
99
|
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/typecheck-stop.mjs\"",
|
|
100
100
|
"timeout": 30
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
"type": "command",
|
|
104
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/query-cost-nudge.mjs\"",
|
|
105
|
+
"timeout": 5
|
|
101
106
|
}
|
|
102
107
|
]
|
|
103
108
|
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* query-cost-nudge.mts — Stop hook that nudges a D1-aware query-cost audit when
|
|
4
|
+
* the working tree has changed DB/query files at turn end (RISA 5 / G5).
|
|
5
|
+
*
|
|
6
|
+
* The `database-reviewer` agent already carries the cost checklist, but nothing
|
|
7
|
+
* dispatched it on DB edits, so cost regressions (the surprise-D1-bill class)
|
|
8
|
+
* shipped unaudited. This injects a once-per-session reminder via Stop
|
|
9
|
+
* `additionalContext` — a Stop hook (not PostToolUse) because only PreToolUse /
|
|
10
|
+
* UserPromptSubmit / Stop / SubagentStop support additionalContext; PostToolUse
|
|
11
|
+
* could only emit a user-facing systemMessage that never reaches the model.
|
|
12
|
+
*
|
|
13
|
+
* Opt-in via CLAUDE_QUERY_COST_NUDGE=on (default off). Once-per-session dedup
|
|
14
|
+
* keyed on the Stop stdin session_id (falls back to a per-day key) — without it,
|
|
15
|
+
* additionalContext "keeps the turn going" and would re-fire every turn while
|
|
16
|
+
* the DB files stay dirty. Never blocks; fail-open on any error.
|
|
17
|
+
*/
|
|
18
|
+
import { execFileSync } from "node:child_process";
|
|
19
|
+
import { createHash } from "node:crypto";
|
|
20
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
21
|
+
import { homedir } from "node:os";
|
|
22
|
+
import { dirname, join } from "node:path";
|
|
23
|
+
import { buildQueryCostReminder, changedQueryPaths, parseChangedFiles, resolveQueryCostNudge, } from "../lib/query-cost-gate.mjs";
|
|
24
|
+
function readStdin() {
|
|
25
|
+
try {
|
|
26
|
+
return readFileSync(0, "utf8");
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return "";
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function resolveHome() {
|
|
33
|
+
return process.env.HOME || process.env.USERPROFILE || homedir();
|
|
34
|
+
}
|
|
35
|
+
function resolveProjectRoot() {
|
|
36
|
+
const fromEnv = process.env.CLAUDE_PROJECT_DIR;
|
|
37
|
+
if (fromEnv && fromEnv.trim())
|
|
38
|
+
return fromEnv.trim();
|
|
39
|
+
try {
|
|
40
|
+
const root = execFileSync("git", ["rev-parse", "--show-toplevel"], {
|
|
41
|
+
encoding: "utf8",
|
|
42
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
43
|
+
}).trim();
|
|
44
|
+
if (root)
|
|
45
|
+
return root;
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
// not in a git repo
|
|
49
|
+
}
|
|
50
|
+
return "global";
|
|
51
|
+
}
|
|
52
|
+
function collectChangedFiles(root) {
|
|
53
|
+
const run = (args) => {
|
|
54
|
+
try {
|
|
55
|
+
return execFileSync("git", args, { cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return "";
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
return [
|
|
62
|
+
...parseChangedFiles(run(["diff", "--name-only", "--diff-filter=ACMR"])),
|
|
63
|
+
...parseChangedFiles(run(["diff", "--cached", "--name-only", "--diff-filter=ACMR"])),
|
|
64
|
+
...parseChangedFiles(run(["ls-files", "--others", "--exclude-standard"])),
|
|
65
|
+
];
|
|
66
|
+
}
|
|
67
|
+
// Marker key: the sanitized session_id when present (the normal case), else a
|
|
68
|
+
// per-day key so a missing id self-heals daily instead of blocking forever.
|
|
69
|
+
function markerKey(sessionId) {
|
|
70
|
+
const sanitized = (sessionId ?? "").replace(/[^A-Za-z0-9_-]/g, "_").slice(0, 64);
|
|
71
|
+
return sanitized || `day-${new Date().toISOString().slice(0, 10)}`;
|
|
72
|
+
}
|
|
73
|
+
function markerPath(home, projectRoot, sessionId) {
|
|
74
|
+
const hash = createHash("sha256").update(projectRoot).digest("hex").slice(0, 12);
|
|
75
|
+
return join(home, ".claude", "instincts", hash, "query-cost-nudge", `${markerKey(sessionId)}.nudged`);
|
|
76
|
+
}
|
|
77
|
+
function main() {
|
|
78
|
+
if (resolveQueryCostNudge(process.env.CLAUDE_QUERY_COST_NUDGE) === "off")
|
|
79
|
+
return;
|
|
80
|
+
let sessionId;
|
|
81
|
+
try {
|
|
82
|
+
const payload = JSON.parse(readStdin());
|
|
83
|
+
if (typeof payload.session_id === "string")
|
|
84
|
+
sessionId = payload.session_id;
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
// stdin optional — proceed without a session id (per-day dedup)
|
|
88
|
+
}
|
|
89
|
+
const root = resolveProjectRoot();
|
|
90
|
+
if (root === "global")
|
|
91
|
+
return; // no repo → nothing to diff
|
|
92
|
+
const changed = changedQueryPaths(collectChangedFiles(root));
|
|
93
|
+
if (changed.length === 0)
|
|
94
|
+
return;
|
|
95
|
+
const marker = markerPath(resolveHome(), root, sessionId);
|
|
96
|
+
try {
|
|
97
|
+
if (existsSync(marker))
|
|
98
|
+
return; // already nudged this session
|
|
99
|
+
mkdirSync(dirname(marker), { recursive: true });
|
|
100
|
+
writeFileSync(marker, `${new Date().toISOString()}\n`);
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
// if the marker can't be written, nudge anyway (no dedup) rather than stay silent
|
|
104
|
+
}
|
|
105
|
+
process.stdout.write(`${JSON.stringify({
|
|
106
|
+
hookSpecificOutput: { hookEventName: "Stop", additionalContext: buildQueryCostReminder(changed) },
|
|
107
|
+
})}\n`);
|
|
108
|
+
}
|
|
109
|
+
try {
|
|
110
|
+
main();
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
// fail open — never trap a turn on a hook bug
|
|
114
|
+
}
|
|
@@ -51,7 +51,8 @@ function collectChangedFiles(root) {
|
|
|
51
51
|
};
|
|
52
52
|
const unstaged = run(["diff", "--name-only", "--diff-filter=ACMR"]);
|
|
53
53
|
const staged = run(["diff", "--cached", "--name-only", "--diff-filter=ACMR"]);
|
|
54
|
-
|
|
54
|
+
const untracked = run(["ls-files", "--others", "--exclude-standard"]);
|
|
55
|
+
return [...parseChangedFiles(unstaged), ...parseChangedFiles(staged), ...parseChangedFiles(untracked)];
|
|
55
56
|
}
|
|
56
57
|
function hasNpmTypecheckScript(root) {
|
|
57
58
|
try {
|
|
@@ -505,6 +505,11 @@ export function getPluginHooksConfig() {
|
|
|
505
505
|
// internal timeout it fails open (allow) rather than blocking.
|
|
506
506
|
timeout: 30,
|
|
507
507
|
};
|
|
508
|
+
const queryCostNudgeCommand = {
|
|
509
|
+
type: "command",
|
|
510
|
+
command: "node \"${CLAUDE_PLUGIN_ROOT}/hooks/query-cost-nudge.mjs\"",
|
|
511
|
+
timeout: 5,
|
|
512
|
+
};
|
|
508
513
|
const routePromptCommand = {
|
|
509
514
|
type: "command",
|
|
510
515
|
command: "node \"${CLAUDE_PLUGIN_ROOT}/hooks/route-prompt.mjs\"",
|
|
@@ -516,7 +521,7 @@ export function getPluginHooksConfig() {
|
|
|
516
521
|
timeout: 5,
|
|
517
522
|
};
|
|
518
523
|
return {
|
|
519
|
-
description: "Gateguard fact-forcing PreToolUse, companion-preference enforcement, observation, session lifecycle, 3-section-close discipline, goal-drift Stop gate, opt-in workflow-distill Stop nudge, opt-in typecheck Stop gate, and UserPromptSubmit lazy-routing plus opt-in proactive recall-briefing hooks for continuous-improvement.",
|
|
524
|
+
description: "Gateguard fact-forcing PreToolUse, companion-preference enforcement, observation, session lifecycle, 3-section-close discipline, goal-drift Stop gate, opt-in workflow-distill Stop nudge, opt-in typecheck Stop gate, opt-in query-cost Stop nudge, and UserPromptSubmit lazy-routing plus opt-in proactive recall-briefing hooks for continuous-improvement.",
|
|
520
525
|
hooks: {
|
|
521
526
|
// gateguard runs FIRST on PreToolUse so its block decision short-circuits
|
|
522
527
|
// before companion-preference sees the call. companion-preference runs
|
|
@@ -540,7 +545,7 @@ export function getPluginHooksConfig() {
|
|
|
540
545
|
UserPromptSubmit: [{ hooks: [routePromptCommand, recallBriefingCommand] }],
|
|
541
546
|
SessionStart: [{ hooks: [sessionCommand] }],
|
|
542
547
|
SessionEnd: [{ hooks: [sessionCommand] }],
|
|
543
|
-
Stop: [{ hooks: [threeSectionCloseCommand, goalDriftStopCommand, workflowDistillCommand, typecheckStopCommand] }],
|
|
548
|
+
Stop: [{ hooks: [threeSectionCloseCommand, goalDriftStopCommand, workflowDistillCommand, typecheckStopCommand, queryCostNudgeCommand] }],
|
|
544
549
|
},
|
|
545
550
|
};
|
|
546
551
|
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure helpers for the query-cost nudge Stop hook (RISA 5 / G5).
|
|
3
|
+
*
|
|
4
|
+
* The hook (src/hooks/query-cost-nudge.mts) does the I/O — git, the per-session
|
|
5
|
+
* dedup marker, emitting the reminder. Everything here is pure and unit-tested.
|
|
6
|
+
*
|
|
7
|
+
* Opt-in via CLAUDE_QUERY_COST_NUDGE=on (default off). When on and the working
|
|
8
|
+
* tree has changed DB/query files at turn end, the hook injects a once-per-session
|
|
9
|
+
* reminder (via Stop `additionalContext`) to run a D1-aware cost audit before
|
|
10
|
+
* finishing — the surprise-D1-bill class the insights flagged. Never blocks.
|
|
11
|
+
*/
|
|
12
|
+
export function resolveQueryCostNudge(raw) {
|
|
13
|
+
return (raw ?? "").trim().toLowerCase() === "on" ? "on" : "off";
|
|
14
|
+
}
|
|
15
|
+
// DB / query source paths. Case-insensitive; the caller passes forward-slash
|
|
16
|
+
// paths straight from `git diff --name-only` (git always uses forward slashes).
|
|
17
|
+
const QUERY_PATH_RES = [
|
|
18
|
+
/\.sql$/i,
|
|
19
|
+
/\.prisma$/i,
|
|
20
|
+
/(^|\/)migrations?\//i,
|
|
21
|
+
/(^|\/)db\//i,
|
|
22
|
+
/(^|\/)schema\.(ts|js|mjs|cjs|cts|mts|prisma|sql)$/i,
|
|
23
|
+
/drizzle/i,
|
|
24
|
+
];
|
|
25
|
+
export function isQueryPath(filePath) {
|
|
26
|
+
const path = (filePath ?? "").replace(/\\/g, "/").trim();
|
|
27
|
+
if (path === "")
|
|
28
|
+
return false;
|
|
29
|
+
return QUERY_PATH_RES.some((re) => re.test(path));
|
|
30
|
+
}
|
|
31
|
+
export function parseChangedFiles(gitOutput) {
|
|
32
|
+
return (gitOutput ?? "")
|
|
33
|
+
.split(/\r?\n/)
|
|
34
|
+
.map((line) => line.trim())
|
|
35
|
+
.filter((line) => line.length > 0);
|
|
36
|
+
}
|
|
37
|
+
export function changedQueryPaths(files) {
|
|
38
|
+
return files.filter((file) => isQueryPath(file));
|
|
39
|
+
}
|
|
40
|
+
export function buildQueryCostReminder(paths) {
|
|
41
|
+
const list = paths.slice(0, 8).map((path) => path.replace(/\\/g, "/")).join(", ");
|
|
42
|
+
return [
|
|
43
|
+
"<system-reminder>",
|
|
44
|
+
`Query-cost check: you changed DB/query file(s) this session (${list}).`,
|
|
45
|
+
"Before finishing, audit cost — dispatch the database-reviewer agent, or check directly:",
|
|
46
|
+
" - EXPLAIN QUERY PLAN each new/changed query; no full table SCAN on a hot path.",
|
|
47
|
+
" - Every WHERE / JOIN / ORDER BY column is index-covered; no N+1 loops.",
|
|
48
|
+
" - D1: rows_read is billed per row SCANNED (not returned) — add covering indexes and cache hot reads (KV) instead of re-querying.",
|
|
49
|
+
" - No unbounded query — add LIMIT / pagination.",
|
|
50
|
+
"One-time per-session reminder (CLAUDE_QUERY_COST_NUDGE).",
|
|
51
|
+
"</system-reminder>",
|
|
52
|
+
].join("\n");
|
|
53
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# `scripts/` — deterministic primitives skills cite
|
|
2
|
+
|
|
3
|
+
This directory holds small, hand-authored scripts that skills (and hooks) cite instead of restating fixed operations inline. The motivation is the skills audit's "deterministic vs non-deterministic" axis: when a step inside a skill is the same operation every time (a fixed git command, a regex parse, a path lookup), it does not belong in the LLM-driven part of the skill — it belongs here, where the script runs the same way every invocation, costs no tokens, and stays in one place so multiple skills can share it.
|
|
4
|
+
|
|
5
|
+
## Conventions
|
|
6
|
+
|
|
7
|
+
- **Hand-authored, no `.mts` source.** Files in `scripts/` are not part of the `tsc` build pipeline (see [`CLAUDE.md`](../CLAUDE.md) → "Build pipeline"). The pipeline owns `bin/`, `lib/`, `test/`. `scripts/` is the bare-metal home for shell scripts and one-off Node utilities skills cite directly.
|
|
8
|
+
- **One concern per script.** Each script does one thing and prints either machine-readable output (JSON envelope, single value) or a stable fenced block. Skills consume the output; they do not re-derive it.
|
|
9
|
+
- **Cross-platform shape.** Bash scripts run via Git Bash on Windows, native bash on macOS/Linux. Tests for bash scripts skip when bash is not on PATH (see `src/test/hook.test.mts` for the established skip pattern).
|
|
10
|
+
- **Cite from skills.** When a skill needs a primitive, the skill body cites the script path (`scripts/<name>.<ext>`) and quotes the expected output shape once. The skill does not restate the script's inner mechanics.
|
|
11
|
+
|
|
12
|
+
## Inventory
|
|
13
|
+
|
|
14
|
+
| Script | Purpose | Cited by |
|
|
15
|
+
|---|---|---|
|
|
16
|
+
| `git-state-snapshot.sh` | JSON envelope `{head, upstream, dirty, root, branch}` for the current git working tree | `skills/gateguard.md` (Parallel-Actor Gate), `skills/worktree-safety.md` (Root + branch), `skills/workspace-surface-audit.md` (Environment Grain — parallel-actor row), `skills/reconcile.md` (Detect a Concurrent Writer) |
|
|
17
|
+
| `detect-deploy-target.sh` | Detect the auto-deploy provider for the repo at the current working directory (or first arg). Prints one of `railway`/`cloudflare`/`vercel`/`netlify`/`fly`/`appengine`/`apprunner`/`gha-deploy`/`none`. Always exits 0. | `skills/verification-loop.md` (Phase 8 deploy-receipt handoff), `skills/deploy-receipt.md` (When to Activate gate) |
|
|
18
|
+
| `get-deployed-sha.sh` | Per-provider deployed-SHA extraction. Default mode runs the CLI; `--show-command <provider>` prints the pipeline shape without executing (useful for citation, dry-run, tests). | `skills/verification-loop.md` (Phase 8), `skills/deploy-receipt.md` (Route A — provider CLI extraction) |
|
|
19
|
+
| `resolve-verify-ladder.mjs` | Resolve the per-project verification ladder for Phase 0 of `verification-loop`. Encodes the four-step priority — manifest > package.json sniff > per-language toolchain > ask-operator. Default mode prints the fenced block the skill displays; `--json` mode emits a JSON object for machine consumption. | `skills/verification-loop.md` (Phase 0) |
|
|
20
|
+
| `scan-past-mistakes.mjs` | Scan the three Past-Mistake Acknowledgment Gate surfaces — `~/.claude/instincts/<hash>/observations.jsonl` (last N failure/correction rows), `~/.claude/projects/<hash>/memory/feedback_*.md`, and `<root>/CLAUDE.md` "## Past Mistakes" table. Active-in-scope judgment is the LLM's job; the script provides quotes + citations only. | `skills/proceed-with-the-recommendation.md` (Phase 0 Rule 1) |
|
|
21
|
+
| `route-recommendation.mjs` + `route-recommendation.routes.json` | Match a recommendation item to its preferred-skill chain + inline fallback from the Phase 3 routing table (29 rows). Data file is the programmatic source of truth; the skill's table is documentation that mirrors it. Default mode prints a match block; `--json` for machine consumption; `--list` enumerates all rows. | `skills/proceed-with-the-recommendation.md` (Phase 3 routing table) |
|
|
22
|
+
| `run-synthetic.mjs` | Phase 9 runner: invoke every `*.synthetic.{sh,mjs,ts,py}` in `synthetic-checks/`, inject `BASE_URL`/`BASELINE_URL`/`EXPECTED_SHA`/`DEPLOY_BRANCH`/`RECEIPT_TIMESTAMP` (unset vars pass through as `""`), capture stdout/stderr/exit per check, aggregate. Default mode prints the report block; `--json` for machine consumption; `--fail-fast` halts on first drift or timeout; `--timeout <sec>` per-check wall-clock cap; `--show-command` dry-run. Exit 0 all-pass, 1 drift, 2 config error, 3 usage. | `skills/verification-loop.md` (Phase 9 — production-vs-baseline diff) |
|
|
23
|
+
|
|
24
|
+
When a new script lands here, add a row to this table in the same PR and cite the script from at least one skill — otherwise the script is dead code on arrival.
|
|
25
|
+
|
|
26
|
+
## Relationship to other locations
|
|
27
|
+
|
|
28
|
+
- `bin/` — generated CLI entrypoints (`.mjs` from `src/bin/*.mts`). Do not hand-edit; see [`CLAUDE.md`](../CLAUDE.md) → "Build pipeline".
|
|
29
|
+
- `lib/` — generated library code (`.mjs` from `src/lib/*.mts`). Same rule.
|
|
30
|
+
- `hooks/` — generated PreToolUse / PostToolUse / Stop hooks (`.mjs` from `src/hooks/*.mts`). Wired in `plugins/continuous-improvement/hooks/hooks.json`.
|
|
31
|
+
- `scripts/` — this directory. Hand-authored primitives skills cite.
|
|
32
|
+
|
|
33
|
+
When deciding where a new piece of code belongs: if it implements a runtime hook the harness will call, it goes in `src/hooks/`. If it's a verification or CLI entrypoint the plugin or `npm run` invokes, it goes in `src/bin/`. If it's reusable logic shared between those, it goes in `src/lib/`. If it's a small primitive a skill body cites by path (and the operator or harness runs ad hoc), it goes here.
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# scripts/detect-deploy-target.sh
|
|
3
|
+
#
|
|
4
|
+
# Detect the auto-deploy provider for the repo rooted at the current working
|
|
5
|
+
# directory (or the first argument, if supplied). Composability primitive used
|
|
6
|
+
# by skills and hooks that need to know "does this repo auto-deploy, and if so
|
|
7
|
+
# from where?" without each restating the file-marker table.
|
|
8
|
+
#
|
|
9
|
+
# Output: one of
|
|
10
|
+
# railway | cloudflare | vercel | netlify | fly | appengine | apprunner |
|
|
11
|
+
# gha-deploy | none
|
|
12
|
+
#
|
|
13
|
+
# Resolution priority (first match wins):
|
|
14
|
+
# 1. railway.toml | railway.json → railway
|
|
15
|
+
# 2. wrangler.toml | wrangler.jsonc → cloudflare
|
|
16
|
+
# 3. vercel.json | .vercel/ → vercel
|
|
17
|
+
# 4. netlify.toml → netlify
|
|
18
|
+
# 5. fly.toml → fly
|
|
19
|
+
# 6. app.yaml → appengine
|
|
20
|
+
# 7. apprunner.yaml → apprunner
|
|
21
|
+
# 8. .github/workflows/*.{yml,yaml} containing "deploy:" job → gha-deploy
|
|
22
|
+
# 9. nothing matched → none
|
|
23
|
+
#
|
|
24
|
+
# Always exits 0. `none` is a valid result, not an error condition.
|
|
25
|
+
#
|
|
26
|
+
# Cited by:
|
|
27
|
+
# - skills/verification-loop.md Phase 8 (deploy-receipt handoff trigger)
|
|
28
|
+
# - skills/deploy-receipt.md "When to Activate" gate
|
|
29
|
+
|
|
30
|
+
set -u
|
|
31
|
+
|
|
32
|
+
ROOT="${1:-$PWD}"
|
|
33
|
+
|
|
34
|
+
emit() {
|
|
35
|
+
printf '%s\n' "$1"
|
|
36
|
+
exit 0
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
# Order is the contract — earlier rows shadow later ones when multiple
|
|
40
|
+
# markers exist in the same repo.
|
|
41
|
+
[ -f "$ROOT/railway.toml" ] && emit railway
|
|
42
|
+
[ -f "$ROOT/railway.json" ] && emit railway
|
|
43
|
+
[ -f "$ROOT/wrangler.toml" ] && emit cloudflare
|
|
44
|
+
[ -f "$ROOT/wrangler.jsonc" ] && emit cloudflare
|
|
45
|
+
[ -f "$ROOT/vercel.json" ] && emit vercel
|
|
46
|
+
[ -d "$ROOT/.vercel" ] && emit vercel
|
|
47
|
+
[ -f "$ROOT/netlify.toml" ] && emit netlify
|
|
48
|
+
[ -f "$ROOT/fly.toml" ] && emit fly
|
|
49
|
+
[ -f "$ROOT/app.yaml" ] && emit appengine
|
|
50
|
+
[ -f "$ROOT/apprunner.yaml" ] && emit apprunner
|
|
51
|
+
|
|
52
|
+
# GitHub Actions deploy workflow: scan .github/workflows/*.{yml,yaml} for a
|
|
53
|
+
# job whose key is literally `deploy:`. Cheap and conservative — false
|
|
54
|
+
# positives (a non-deploy job named "deploy") are unlikely; false negatives
|
|
55
|
+
# (a deploy job named something else) require the repo to declare the
|
|
56
|
+
# provider via one of the file markers above instead.
|
|
57
|
+
if [ -d "$ROOT/.github/workflows" ]; then
|
|
58
|
+
if grep -lE '^[[:space:]]*deploy:[[:space:]]*$' \
|
|
59
|
+
"$ROOT/.github/workflows/"*.yml \
|
|
60
|
+
"$ROOT/.github/workflows/"*.yaml 2>/dev/null \
|
|
61
|
+
| head -n 1 | grep -q .; then
|
|
62
|
+
emit gha-deploy
|
|
63
|
+
fi
|
|
64
|
+
fi
|
|
65
|
+
|
|
66
|
+
emit none
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# scripts/get-deployed-sha.sh
|
|
3
|
+
#
|
|
4
|
+
# Print the currently-deployed commit SHA for an auto-deploy provider, or
|
|
5
|
+
# print the CLI command shape (with `--show-command`) without executing it.
|
|
6
|
+
# Composability primitive that owns the per-provider CLI knowledge so skills
|
|
7
|
+
# can cite one path instead of restating the 5-provider extraction table.
|
|
8
|
+
#
|
|
9
|
+
# Usage:
|
|
10
|
+
# bash scripts/get-deployed-sha.sh <provider>
|
|
11
|
+
# bash scripts/get-deployed-sha.sh --show-command <provider>
|
|
12
|
+
#
|
|
13
|
+
# Providers: railway | cloudflare | vercel | netlify | fly
|
|
14
|
+
#
|
|
15
|
+
# Default mode: runs the provider CLI, pipes through jq, prints the SHA on
|
|
16
|
+
# stdout. Requires the CLI to be installed and authenticated; exits 3 with a
|
|
17
|
+
# clear error if the CLI is missing.
|
|
18
|
+
#
|
|
19
|
+
# --show-command mode: prints the command pipeline that would run, without
|
|
20
|
+
# executing it. Useful for skill citations, dry-runs, and tests that should
|
|
21
|
+
# not require live CLI auth.
|
|
22
|
+
#
|
|
23
|
+
# Exit codes:
|
|
24
|
+
# 0 — SHA printed (default mode) or command printed (--show-command mode)
|
|
25
|
+
# 2 — missing or unknown provider (usage error)
|
|
26
|
+
# 3 — required CLI not installed (default mode only)
|
|
27
|
+
# non-zero — CLI failure (passed through)
|
|
28
|
+
#
|
|
29
|
+
# Cited by:
|
|
30
|
+
# - skills/verification-loop.md Phase 8 (deploy-receipt handoff trigger)
|
|
31
|
+
# - skills/deploy-receipt.md Route A (provider CLI extraction)
|
|
32
|
+
|
|
33
|
+
set -u
|
|
34
|
+
|
|
35
|
+
SHOW_COMMAND=false
|
|
36
|
+
PROVIDER=""
|
|
37
|
+
|
|
38
|
+
while [ $# -gt 0 ]; do
|
|
39
|
+
case "$1" in
|
|
40
|
+
--show-command)
|
|
41
|
+
SHOW_COMMAND=true
|
|
42
|
+
shift
|
|
43
|
+
;;
|
|
44
|
+
-h|--help)
|
|
45
|
+
sed -n '2,/^$/p' "$0" >&2
|
|
46
|
+
exit 0
|
|
47
|
+
;;
|
|
48
|
+
*)
|
|
49
|
+
if [ -z "$PROVIDER" ]; then
|
|
50
|
+
PROVIDER="$1"
|
|
51
|
+
else
|
|
52
|
+
printf 'usage: get-deployed-sha.sh [--show-command] <provider>\n' >&2
|
|
53
|
+
exit 2
|
|
54
|
+
fi
|
|
55
|
+
shift
|
|
56
|
+
;;
|
|
57
|
+
esac
|
|
58
|
+
done
|
|
59
|
+
|
|
60
|
+
if [ -z "$PROVIDER" ]; then
|
|
61
|
+
printf 'usage: get-deployed-sha.sh [--show-command] <provider>\n' >&2
|
|
62
|
+
exit 2
|
|
63
|
+
fi
|
|
64
|
+
|
|
65
|
+
# Single source of truth for the per-provider command pipeline. Each value is
|
|
66
|
+
# the literal pipeline that would run; the jq filter extracts the SHA.
|
|
67
|
+
case "$PROVIDER" in
|
|
68
|
+
railway)
|
|
69
|
+
CMD='railway status --json | jq -r .deployments[0].meta.commitHash'
|
|
70
|
+
CLI=railway
|
|
71
|
+
;;
|
|
72
|
+
cloudflare)
|
|
73
|
+
CMD='wrangler deployments list --json | jq -r .[0].metadata.deployment_trigger.metadata.commit_hash'
|
|
74
|
+
CLI=wrangler
|
|
75
|
+
;;
|
|
76
|
+
vercel)
|
|
77
|
+
CMD='vercel inspect "$(vercel ls --json | jq -r .[0].url)" --json | jq -r .gitSource.sha'
|
|
78
|
+
CLI=vercel
|
|
79
|
+
;;
|
|
80
|
+
netlify)
|
|
81
|
+
CMD='netlify api listSiteDeploys --data="{\"site_id\":\"$NETLIFY_SITE_ID\"}" | jq -r .[0].commit_ref'
|
|
82
|
+
CLI=netlify
|
|
83
|
+
;;
|
|
84
|
+
fly)
|
|
85
|
+
CMD='fly releases --json | jq -r .[0].commit_sha'
|
|
86
|
+
CLI=fly
|
|
87
|
+
;;
|
|
88
|
+
*)
|
|
89
|
+
printf 'unknown or unsupported provider: %s\n' "$PROVIDER" >&2
|
|
90
|
+
printf 'supported: railway | cloudflare | vercel | netlify | fly\n' >&2
|
|
91
|
+
exit 2
|
|
92
|
+
;;
|
|
93
|
+
esac
|
|
94
|
+
|
|
95
|
+
if [ "$SHOW_COMMAND" = "true" ]; then
|
|
96
|
+
printf '%s\n' "$CMD"
|
|
97
|
+
exit 0
|
|
98
|
+
fi
|
|
99
|
+
|
|
100
|
+
if ! command -v "$CLI" >/dev/null 2>&1; then
|
|
101
|
+
printf 'required CLI "%s" not installed for provider "%s"\n' "$CLI" "$PROVIDER" >&2
|
|
102
|
+
exit 3
|
|
103
|
+
fi
|
|
104
|
+
|
|
105
|
+
if ! command -v jq >/dev/null 2>&1; then
|
|
106
|
+
printf 'required CLI "jq" not installed\n' >&2
|
|
107
|
+
exit 3
|
|
108
|
+
fi
|
|
109
|
+
|
|
110
|
+
# Execute the pipeline. eval is intentional — the per-provider CMD contains
|
|
111
|
+
# pipes and command substitution that need shell interpretation. CMDs are
|
|
112
|
+
# sourced from the literal table above, not user input.
|
|
113
|
+
eval "$CMD"
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# scripts/git-state-snapshot.sh
|
|
3
|
+
#
|
|
4
|
+
# Emit a single-line JSON envelope describing the current git working-tree
|
|
5
|
+
# state. Composability primitive used by skills that need to baseline or check
|
|
6
|
+
# git state without each restating the same 3-command triple.
|
|
7
|
+
#
|
|
8
|
+
# Fields:
|
|
9
|
+
# - head: short SHA returned by `git rev-parse --short HEAD`
|
|
10
|
+
# - upstream: short SHA of `@{u}` if the branch tracks an upstream, else "none"
|
|
11
|
+
# - dirty: integer count of lines from `git status --porcelain` (0 == clean)
|
|
12
|
+
# - root: absolute path from `git rev-parse --show-toplevel`
|
|
13
|
+
# - branch: `git symbolic-ref --short HEAD`, else "detached"
|
|
14
|
+
#
|
|
15
|
+
# Outside a git repository the script prints `{"error":"not-a-git-repo"}` and
|
|
16
|
+
# exits 1. All other failures are treated as a non-git-repo condition rather
|
|
17
|
+
# than emitting a partial envelope.
|
|
18
|
+
#
|
|
19
|
+
# Cited by:
|
|
20
|
+
# - skills/gateguard.md (Parallel-Actor Gate baseline + divergence)
|
|
21
|
+
# - skills/worktree-safety.md (Root + branch alignment)
|
|
22
|
+
# - skills/workspace-surface-audit.md (Environment Grain — parallel-actor row)
|
|
23
|
+
|
|
24
|
+
set -u
|
|
25
|
+
|
|
26
|
+
head=$(git rev-parse --short HEAD 2>/dev/null) || {
|
|
27
|
+
printf '{"error":"not-a-git-repo"}\n'
|
|
28
|
+
exit 1
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if upstream=$(git rev-parse --short '@{u}' 2>/dev/null); then
|
|
32
|
+
upstream_field=$(printf '"%s"' "$upstream")
|
|
33
|
+
else
|
|
34
|
+
upstream_field='"none"'
|
|
35
|
+
fi
|
|
36
|
+
|
|
37
|
+
dirty=$(git status --porcelain 2>/dev/null | wc -l | tr -d ' ')
|
|
38
|
+
|
|
39
|
+
root=$(git rev-parse --show-toplevel 2>/dev/null || printf 'unknown')
|
|
40
|
+
|
|
41
|
+
if branch=$(git symbolic-ref --short HEAD 2>/dev/null); then
|
|
42
|
+
branch_field=$(printf '"%s"' "$branch")
|
|
43
|
+
else
|
|
44
|
+
branch_field='"detached"'
|
|
45
|
+
fi
|
|
46
|
+
|
|
47
|
+
printf '{"head":"%s","upstream":%s,"dirty":%s,"root":"%s","branch":%s}\n' \
|
|
48
|
+
"$head" "$upstream_field" "$dirty" "$root" "$branch_field"
|