cleargate 0.5.0 → 0.6.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/dist/MANIFEST.json +30 -16
- package/dist/cli.cjs +486 -51
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +481 -47
- package/dist/cli.js.map +1 -1
- package/dist/templates/cleargate-planning/.claude/agents/architect.md +24 -0
- package/dist/templates/cleargate-planning/.claude/agents/developer.md +24 -0
- package/dist/templates/cleargate-planning/.claude/agents/reporter.md +74 -0
- package/dist/templates/cleargate-planning/.claude/hooks/pre-edit-gate.sh +162 -0
- package/dist/templates/cleargate-planning/.claude/hooks/session-start.sh +10 -7
- package/dist/templates/cleargate-planning/.claude/hooks/stamp-and-gate.sh +9 -8
- package/dist/templates/cleargate-planning/.claude/hooks/token-ledger.sh +36 -13
- package/dist/templates/cleargate-planning/.claude/settings.json +9 -0
- package/dist/templates/cleargate-planning/.cleargate/knowledge/cleargate-protocol.md +55 -0
- package/dist/templates/cleargate-planning/.cleargate/knowledge/readiness-gates.md +7 -7
- package/dist/templates/cleargate-planning/.cleargate/scripts/assert_story_files.mjs +137 -40
- package/dist/templates/cleargate-planning/.cleargate/scripts/close_sprint.mjs +93 -0
- package/dist/templates/cleargate-planning/.cleargate/scripts/constants.mjs +8 -4
- package/dist/templates/cleargate-planning/.cleargate/scripts/init_sprint.mjs +9 -1
- package/dist/templates/cleargate-planning/.cleargate/scripts/pre_gate_common.sh +74 -0
- package/dist/templates/cleargate-planning/.cleargate/scripts/pre_gate_runner.sh +65 -1
- package/dist/templates/cleargate-planning/.cleargate/scripts/state.schema.json +31 -8
- package/dist/templates/cleargate-planning/.cleargate/scripts/update_state.mjs +93 -8
- package/dist/templates/cleargate-planning/.cleargate/templates/Sprint Plan Template.md +19 -4
- package/dist/templates/cleargate-planning/.cleargate/templates/hotfix.md +58 -0
- package/dist/templates/cleargate-planning/.cleargate/templates/sprint_report.md +32 -2
- package/dist/templates/cleargate-planning/.cleargate/templates/story.md +3 -1
- package/dist/templates/cleargate-planning/CLAUDE.md +1 -1
- package/dist/templates/cleargate-planning/MANIFEST.json +30 -16
- package/package.json +1 -1
- package/templates/cleargate-planning/.claude/agents/architect.md +24 -0
- package/templates/cleargate-planning/.claude/agents/developer.md +24 -0
- package/templates/cleargate-planning/.claude/agents/reporter.md +74 -0
- package/templates/cleargate-planning/.claude/hooks/pre-edit-gate.sh +162 -0
- package/templates/cleargate-planning/.claude/hooks/session-start.sh +10 -7
- package/templates/cleargate-planning/.claude/hooks/stamp-and-gate.sh +9 -8
- package/templates/cleargate-planning/.claude/hooks/token-ledger.sh +36 -13
- package/templates/cleargate-planning/.claude/settings.json +9 -0
- package/templates/cleargate-planning/.cleargate/knowledge/cleargate-protocol.md +55 -0
- package/templates/cleargate-planning/.cleargate/knowledge/readiness-gates.md +7 -7
- package/templates/cleargate-planning/.cleargate/scripts/assert_story_files.mjs +137 -40
- package/templates/cleargate-planning/.cleargate/scripts/close_sprint.mjs +93 -0
- package/templates/cleargate-planning/.cleargate/scripts/constants.mjs +8 -4
- package/templates/cleargate-planning/.cleargate/scripts/init_sprint.mjs +9 -1
- package/templates/cleargate-planning/.cleargate/scripts/pre_gate_common.sh +74 -0
- package/templates/cleargate-planning/.cleargate/scripts/pre_gate_runner.sh +65 -1
- package/templates/cleargate-planning/.cleargate/scripts/state.schema.json +31 -8
- package/templates/cleargate-planning/.cleargate/scripts/update_state.mjs +93 -8
- package/templates/cleargate-planning/.cleargate/templates/Sprint Plan Template.md +19 -4
- package/templates/cleargate-planning/.cleargate/templates/hotfix.md +58 -0
- package/templates/cleargate-planning/.cleargate/templates/sprint_report.md +32 -2
- package/templates/cleargate-planning/.cleargate/templates/story.md +3 -1
- package/templates/cleargate-planning/CLAUDE.md +1 -1
- package/templates/cleargate-planning/MANIFEST.json +30 -16
package/dist/cli.js
CHANGED
|
@@ -14,7 +14,7 @@ import { Command } from "commander";
|
|
|
14
14
|
// package.json
|
|
15
15
|
var package_default = {
|
|
16
16
|
name: "cleargate",
|
|
17
|
-
version: "0.
|
|
17
|
+
version: "0.6.0",
|
|
18
18
|
private: false,
|
|
19
19
|
type: "module",
|
|
20
20
|
description: "Planning framework for Claude Code agents \u2014 sprint/epic/story protocol, four-agent loop (architect/developer/qa/reporter), Karpathy-style awareness wiki.",
|
|
@@ -1275,10 +1275,16 @@ async function stampHandler(file, opts, cli) {
|
|
|
1275
1275
|
import * as fs13 from "fs";
|
|
1276
1276
|
import * as path13 from "path";
|
|
1277
1277
|
import { fileURLToPath as fileURLToPath5 } from "url";
|
|
1278
|
+
import { spawnSync as spawnSync3 } from "child_process";
|
|
1278
1279
|
|
|
1279
1280
|
// src/init/copy-payload.ts
|
|
1280
1281
|
import * as fs5 from "fs";
|
|
1281
1282
|
import * as path4 from "path";
|
|
1283
|
+
var PIN_PLACEHOLDER = "__CLEARGATE_VERSION__";
|
|
1284
|
+
var HOOK_FILES_WITH_PIN = /* @__PURE__ */ new Set([
|
|
1285
|
+
".claude/hooks/stamp-and-gate.sh",
|
|
1286
|
+
".claude/hooks/session-start.sh"
|
|
1287
|
+
]);
|
|
1282
1288
|
function listFilesRecursive(dir) {
|
|
1283
1289
|
const results = [];
|
|
1284
1290
|
function walk(current, rel) {
|
|
@@ -1306,10 +1312,15 @@ function copyPayload(payloadDir, targetCwd, opts) {
|
|
|
1306
1312
|
const srcPath = path4.join(payloadDir, relPath);
|
|
1307
1313
|
const dstPath = path4.join(targetCwd, relPath);
|
|
1308
1314
|
fs5.mkdirSync(path4.dirname(dstPath), { recursive: true });
|
|
1309
|
-
|
|
1315
|
+
let srcContent = fs5.readFileSync(srcPath);
|
|
1316
|
+
if (opts.pinVersion && HOOK_FILES_WITH_PIN.has(relPath)) {
|
|
1317
|
+
const text = srcContent.toString("utf8").replaceAll(PIN_PLACEHOLDER, opts.pinVersion);
|
|
1318
|
+
srcContent = text;
|
|
1319
|
+
}
|
|
1320
|
+
const srcBuffer = typeof srcContent === "string" ? Buffer.from(srcContent, "utf8") : srcContent;
|
|
1310
1321
|
if (fs5.existsSync(dstPath)) {
|
|
1311
1322
|
const dstContent = fs5.readFileSync(dstPath);
|
|
1312
|
-
if (
|
|
1323
|
+
if (srcBuffer.equals(dstContent)) {
|
|
1313
1324
|
report.skipped++;
|
|
1314
1325
|
report.actions.push({ action: "skipped", relPath });
|
|
1315
1326
|
continue;
|
|
@@ -1319,11 +1330,11 @@ function copyPayload(payloadDir, targetCwd, opts) {
|
|
|
1319
1330
|
report.actions.push({ action: "skipped", relPath });
|
|
1320
1331
|
continue;
|
|
1321
1332
|
}
|
|
1322
|
-
fs5.writeFileSync(dstPath,
|
|
1333
|
+
fs5.writeFileSync(dstPath, srcBuffer);
|
|
1323
1334
|
report.overwritten++;
|
|
1324
1335
|
report.actions.push({ action: "overwritten", relPath });
|
|
1325
1336
|
} else {
|
|
1326
|
-
fs5.writeFileSync(dstPath,
|
|
1337
|
+
fs5.writeFileSync(dstPath, srcBuffer);
|
|
1327
1338
|
report.created++;
|
|
1328
1339
|
report.actions.push({ action: "created", relPath });
|
|
1329
1340
|
}
|
|
@@ -2156,7 +2167,7 @@ async function readDriftState(projectRoot) {
|
|
|
2156
2167
|
import * as readline3 from "readline";
|
|
2157
2168
|
async function promptYesNo(question, defaultYes, opts) {
|
|
2158
2169
|
const stdoutFn = opts?.stdout ?? ((s) => process.stdout.write(s));
|
|
2159
|
-
stdoutFn(question + "
|
|
2170
|
+
stdoutFn(question + " ");
|
|
2160
2171
|
const inputStream = opts?.stdin ?? process.stdin;
|
|
2161
2172
|
return new Promise((resolve14) => {
|
|
2162
2173
|
const rl = readline3.createInterface({
|
|
@@ -2187,7 +2198,7 @@ async function promptYesNo(question, defaultYes, opts) {
|
|
|
2187
2198
|
}
|
|
2188
2199
|
async function promptEmail(question, defaultValue, opts) {
|
|
2189
2200
|
const stdoutFn = opts?.stdout ?? ((s) => process.stdout.write(s));
|
|
2190
|
-
stdoutFn(question + "
|
|
2201
|
+
stdoutFn(question + " ");
|
|
2191
2202
|
const inputStream = opts?.stdin ?? process.stdin;
|
|
2192
2203
|
return new Promise((resolve14) => {
|
|
2193
2204
|
const rl = readline3.createInterface({
|
|
@@ -2279,6 +2290,17 @@ function resolveIdentity(projectRoot, opts = {}) {
|
|
|
2279
2290
|
// src/commands/init.ts
|
|
2280
2291
|
var HOOK_ADDITION = {
|
|
2281
2292
|
hooks: {
|
|
2293
|
+
PreToolUse: [
|
|
2294
|
+
{
|
|
2295
|
+
matcher: "Edit|Write",
|
|
2296
|
+
hooks: [
|
|
2297
|
+
{
|
|
2298
|
+
type: "command",
|
|
2299
|
+
command: "${CLAUDE_PROJECT_DIR}/.claude/hooks/pre-edit-gate.sh"
|
|
2300
|
+
}
|
|
2301
|
+
]
|
|
2302
|
+
}
|
|
2303
|
+
],
|
|
2282
2304
|
PostToolUse: [
|
|
2283
2305
|
{
|
|
2284
2306
|
matcher: "Edit|Write",
|
|
@@ -2315,6 +2337,17 @@ function writeAtomic(filePath, content) {
|
|
|
2315
2337
|
fs13.writeFileSync(tmpPath, content, "utf8");
|
|
2316
2338
|
fs13.renameSync(tmpPath, filePath);
|
|
2317
2339
|
}
|
|
2340
|
+
function readPackageVersion(packageJsonPath) {
|
|
2341
|
+
try {
|
|
2342
|
+
const raw = fs13.readFileSync(packageJsonPath, "utf8");
|
|
2343
|
+
const pkg = JSON.parse(raw);
|
|
2344
|
+
if (typeof pkg.version === "string" && pkg.version.length > 0) {
|
|
2345
|
+
return pkg.version;
|
|
2346
|
+
}
|
|
2347
|
+
} catch {
|
|
2348
|
+
}
|
|
2349
|
+
return null;
|
|
2350
|
+
}
|
|
2318
2351
|
async function initHandler(opts = {}) {
|
|
2319
2352
|
const cwd = opts.cwd ?? process.cwd();
|
|
2320
2353
|
const force = opts.force ?? false;
|
|
@@ -2325,6 +2358,7 @@ async function initHandler(opts = {}) {
|
|
|
2325
2358
|
const runWikiBuild = opts.runWikiBuild ?? wikiBuildHandler;
|
|
2326
2359
|
const promptYesNoFn = opts.promptYesNo ?? promptYesNo;
|
|
2327
2360
|
const promptEmailFn = opts.promptEmail ?? promptEmail;
|
|
2361
|
+
const spawnSyncFn = opts.spawnSyncFn ?? spawnSync3;
|
|
2328
2362
|
if (!fs13.existsSync(cwd)) {
|
|
2329
2363
|
stderr(`[cleargate init] ERROR: target directory does not exist: ${cwd}
|
|
2330
2364
|
`);
|
|
@@ -2386,7 +2420,14 @@ async function initHandler(opts = {}) {
|
|
|
2386
2420
|
}
|
|
2387
2421
|
}
|
|
2388
2422
|
}
|
|
2389
|
-
|
|
2423
|
+
let pinVersion;
|
|
2424
|
+
if (opts.pin) {
|
|
2425
|
+
pinVersion = opts.pin;
|
|
2426
|
+
} else {
|
|
2427
|
+
const payloadParent = path13.resolve(payloadDir, "..", "..");
|
|
2428
|
+
pinVersion = readPackageVersion(path13.join(payloadParent, "package.json")) ?? readPackageVersion(path13.join(path13.dirname(fileURLToPath5(import.meta.url)), "..", "package.json")) ?? "latest";
|
|
2429
|
+
}
|
|
2430
|
+
const copyReport = copyPayload(payloadDir, cwd, { force, pinVersion });
|
|
2390
2431
|
for (const action of copyReport.actions) {
|
|
2391
2432
|
const verb = action.action === "created" ? "Created" : action.action === "overwritten" ? "Overwritten" : "Skipped (exists)";
|
|
2392
2433
|
stdout(`[cleargate init] ${verb} ${action.relPath}
|
|
@@ -2467,6 +2508,47 @@ async function initHandler(opts = {}) {
|
|
|
2467
2508
|
`);
|
|
2468
2509
|
}
|
|
2469
2510
|
}
|
|
2511
|
+
{
|
|
2512
|
+
const distCliPath = path13.join(cwd, "cleargate-cli", "dist", "cli.js");
|
|
2513
|
+
let branch = null;
|
|
2514
|
+
let branchLabel = "";
|
|
2515
|
+
if (fs13.existsSync(distCliPath)) {
|
|
2516
|
+
branch = { cmd: "node", args: [distCliPath, "--version"] };
|
|
2517
|
+
branchLabel = `local dist (${distCliPath})`;
|
|
2518
|
+
} else {
|
|
2519
|
+
const whichResult = spawnSyncFn("command", ["-v", "cleargate"], {
|
|
2520
|
+
shell: true,
|
|
2521
|
+
encoding: "utf8",
|
|
2522
|
+
timeout: 3e3
|
|
2523
|
+
});
|
|
2524
|
+
if (whichResult.status === 0) {
|
|
2525
|
+
branch = { cmd: "cleargate", args: ["--version"] };
|
|
2526
|
+
branchLabel = "PATH (global install)";
|
|
2527
|
+
} else {
|
|
2528
|
+
branch = { cmd: "npx", args: ["-y", `cleargate@${pinVersion}`, "--version"] };
|
|
2529
|
+
branchLabel = `npx cleargate@${pinVersion} (cold-start ~600ms first call)`;
|
|
2530
|
+
}
|
|
2531
|
+
}
|
|
2532
|
+
if (branch !== null) {
|
|
2533
|
+
const probeResult = spawnSyncFn(branch.cmd, branch.args, {
|
|
2534
|
+
encoding: "utf8",
|
|
2535
|
+
timeout: 15e3
|
|
2536
|
+
});
|
|
2537
|
+
if (probeResult.status === 0) {
|
|
2538
|
+
stdout(`[cleargate init] \u{1F7E2} cleargate CLI resolved via ${branchLabel}
|
|
2539
|
+
`);
|
|
2540
|
+
} else {
|
|
2541
|
+
stdout(
|
|
2542
|
+
`[cleargate init] \u{1F534} cleargate CLI: not resolvable \u2014 hooks will no-op.
|
|
2543
|
+
[cleargate init] Attempted: ${branchLabel}
|
|
2544
|
+
[cleargate init] Fix: npm i -g cleargate@${pinVersion} or npx cleargate@${pinVersion} doctor
|
|
2545
|
+
`
|
|
2546
|
+
);
|
|
2547
|
+
exit(1);
|
|
2548
|
+
return;
|
|
2549
|
+
}
|
|
2550
|
+
}
|
|
2551
|
+
}
|
|
2470
2552
|
const existingParticipant = readParticipant(cwd);
|
|
2471
2553
|
if (existingParticipant === null) {
|
|
2472
2554
|
const identityOpts = opts.identityOpts ?? {};
|
|
@@ -2484,8 +2566,10 @@ async function initHandler(opts = {}) {
|
|
|
2484
2566
|
stdout(`[cleargate init] Participant identity: ${finalEmail} (inferred)
|
|
2485
2567
|
`);
|
|
2486
2568
|
} else {
|
|
2487
|
-
const
|
|
2488
|
-
const
|
|
2569
|
+
const isNoreply = gitEmail !== null && /@users\.noreply\.github\.com$/i.test(gitEmail);
|
|
2570
|
+
const defaultEmail = gitEmail !== null && !isNoreply ? gitEmail : "user@localhost";
|
|
2571
|
+
stdout("\n");
|
|
2572
|
+
const question = `Participant email (press Enter for default) [${defaultEmail}]:`;
|
|
2489
2573
|
const answer = await promptEmailFn(question, defaultEmail);
|
|
2490
2574
|
await writeParticipant(cwd, answer, "prompted", now);
|
|
2491
2575
|
stdout(`[cleargate init] Participant identity: ${answer} (prompted)
|
|
@@ -2502,7 +2586,7 @@ async function initHandler(opts = {}) {
|
|
|
2502
2586
|
// src/commands/wiki-ingest.ts
|
|
2503
2587
|
import * as fs14 from "fs";
|
|
2504
2588
|
import * as path14 from "path";
|
|
2505
|
-
import { spawnSync as
|
|
2589
|
+
import { spawnSync as spawnSync4 } from "child_process";
|
|
2506
2590
|
var EXCLUDED_SUFFIXES2 = [
|
|
2507
2591
|
".cleargate/knowledge/",
|
|
2508
2592
|
".cleargate/templates/",
|
|
@@ -2654,7 +2738,7 @@ function checkContentUnchanged(absRawPath, sha, relRawPath, gitRunner) {
|
|
|
2654
2738
|
}
|
|
2655
2739
|
}
|
|
2656
2740
|
function defaultGitRunner(cmd, args) {
|
|
2657
|
-
const result =
|
|
2741
|
+
const result = spawnSync4(cmd, args, { encoding: "utf8" });
|
|
2658
2742
|
if (result.status !== 0) return "\0__NONZERO__";
|
|
2659
2743
|
return result.stdout ?? "";
|
|
2660
2744
|
}
|
|
@@ -2903,7 +2987,7 @@ function loadWikiPages(wikiRoot) {
|
|
|
2903
2987
|
// src/wiki/lint-checks.ts
|
|
2904
2988
|
import * as fs16 from "fs";
|
|
2905
2989
|
import * as path16 from "path";
|
|
2906
|
-
import { spawnSync as
|
|
2990
|
+
import { spawnSync as spawnSync5 } from "child_process";
|
|
2907
2991
|
import yaml3 from "js-yaml";
|
|
2908
2992
|
|
|
2909
2993
|
// src/lib/work-item-type.ts
|
|
@@ -2998,7 +3082,7 @@ function checkStaleCommit(page, repoRoot, gitRunner) {
|
|
|
2998
3082
|
if (gitRunner) {
|
|
2999
3083
|
currentSha = gitRunner("git", ["log", "-1", "--format=%H", "--", rawPath]).trim();
|
|
3000
3084
|
} else {
|
|
3001
|
-
const result =
|
|
3085
|
+
const result = spawnSync5("git", ["log", "-1", "--format=%H", "--", rawPath], {
|
|
3002
3086
|
encoding: "utf8",
|
|
3003
3087
|
cwd: repoRoot
|
|
3004
3088
|
});
|
|
@@ -3739,6 +3823,7 @@ function applyStatusFix(rawText, newStatus) {
|
|
|
3739
3823
|
// src/commands/doctor.ts
|
|
3740
3824
|
import * as fs20 from "fs";
|
|
3741
3825
|
import * as path21 from "path";
|
|
3826
|
+
import { spawnSync as spawnSync6 } from "child_process";
|
|
3742
3827
|
|
|
3743
3828
|
// src/lib/pricing.ts
|
|
3744
3829
|
var PRICING_TABLE = {
|
|
@@ -3787,6 +3872,7 @@ function selectMode(flags) {
|
|
|
3787
3872
|
if (flags.checkScaffold) modes.push("check-scaffold");
|
|
3788
3873
|
if (flags.sessionStart) modes.push("session-start");
|
|
3789
3874
|
if (flags.pricing) modes.push("pricing");
|
|
3875
|
+
if (flags.canEdit) modes.push("can-edit");
|
|
3790
3876
|
if (modes.length > 1) {
|
|
3791
3877
|
throw new Error(
|
|
3792
3878
|
`cleargate doctor: mutually exclusive flags set: ${modes.join(", ")}. Use only one mode flag at a time.`
|
|
@@ -3811,7 +3897,18 @@ function parseHookLogLine(line) {
|
|
|
3811
3897
|
file: m[5].trim()
|
|
3812
3898
|
};
|
|
3813
3899
|
}
|
|
3814
|
-
function runHookHealth(stdout, cwd, now) {
|
|
3900
|
+
function runHookHealth(stdout, cwd, now, outcome) {
|
|
3901
|
+
const cleargateDir = path21.join(cwd, ".cleargate");
|
|
3902
|
+
if (!fs20.existsSync(cleargateDir)) {
|
|
3903
|
+
stdout("cleargate misconfigured: no .cleargate/ found. Run: cleargate init");
|
|
3904
|
+
if (outcome) outcome.configError = true;
|
|
3905
|
+
return;
|
|
3906
|
+
}
|
|
3907
|
+
const manifestPath = path21.join(cwd, "cleargate-planning", "MANIFEST.json");
|
|
3908
|
+
if (!fs20.existsSync(manifestPath)) {
|
|
3909
|
+
stdout(`cleargate misconfigured: cleargate-planning/MANIFEST.json not found. Run: cleargate init`);
|
|
3910
|
+
if (outcome) outcome.configError = true;
|
|
3911
|
+
}
|
|
3815
3912
|
const settingsPath = path21.join(cwd, ".claude", "settings.json");
|
|
3816
3913
|
if (!fs20.existsSync(settingsPath)) {
|
|
3817
3914
|
stdout("[doctor] No .claude/settings.json found \u2014 hook config unavailable.");
|
|
@@ -3948,7 +4045,57 @@ function parseCachedGateResult2(raw) {
|
|
|
3948
4045
|
failing_criteria: parsed.failing_criteria ?? []
|
|
3949
4046
|
};
|
|
3950
4047
|
}
|
|
3951
|
-
|
|
4048
|
+
function emitResolverStatusLine(cwd, stdout) {
|
|
4049
|
+
const distCliPath = path21.join(cwd, "cleargate-cli", "dist", "cli.js");
|
|
4050
|
+
if (fs20.existsSync(distCliPath)) {
|
|
4051
|
+
stdout(`cleargate CLI: local dist \u2014 ${distCliPath}`);
|
|
4052
|
+
return;
|
|
4053
|
+
}
|
|
4054
|
+
const whichResult = spawnSync6("command", ["-v", "cleargate"], {
|
|
4055
|
+
shell: true,
|
|
4056
|
+
encoding: "utf8",
|
|
4057
|
+
timeout: 3e3
|
|
4058
|
+
});
|
|
4059
|
+
if (whichResult.status === 0) {
|
|
4060
|
+
stdout("cleargate CLI: PATH (global install) \u2014 cleargate");
|
|
4061
|
+
return;
|
|
4062
|
+
}
|
|
4063
|
+
let pinVersion = "unknown";
|
|
4064
|
+
const hookPath = path21.join(cwd, ".claude", "hooks", "stamp-and-gate.sh");
|
|
4065
|
+
if (fs20.existsSync(hookPath)) {
|
|
4066
|
+
try {
|
|
4067
|
+
const hookContent = fs20.readFileSync(hookPath, "utf-8");
|
|
4068
|
+
const pinMatch = hookContent.match(/^#\s*cleargate-pin:\s*(\S+)\s*$/m);
|
|
4069
|
+
if (pinMatch?.[1]) {
|
|
4070
|
+
pinVersion = pinMatch[1];
|
|
4071
|
+
} else {
|
|
4072
|
+
const npxMatch = hookContent.match(/@cleargate\/cli@([^\s"']+)/);
|
|
4073
|
+
if (npxMatch?.[1]) pinVersion = npxMatch[1];
|
|
4074
|
+
}
|
|
4075
|
+
} catch {
|
|
4076
|
+
}
|
|
4077
|
+
}
|
|
4078
|
+
if (pinVersion === "unknown") {
|
|
4079
|
+
stdout("cleargate CLI: \u{1F534} not resolvable \u2014 hooks will no-op. Fix: npm i -g cleargate or npx cleargate doctor");
|
|
4080
|
+
} else {
|
|
4081
|
+
stdout(`cleargate CLI: npx @cleargate/cli@${pinVersion} (cold-start ~600ms first call)`);
|
|
4082
|
+
}
|
|
4083
|
+
}
|
|
4084
|
+
var PLANNING_FIRST_REMINDER = `Triage first, draft second:
|
|
4085
|
+
Before any Edit/Write that creates user-facing code, you must:
|
|
4086
|
+
(1) classify the request (Epic / Story / CR / Bug),
|
|
4087
|
+
(2) draft a work item under .cleargate/delivery/pending-sync/ from .cleargate/templates/,
|
|
4088
|
+
(3) halt at Gate 1 (Proposal approval) for human sign-off.
|
|
4089
|
+
Bypass this only if the user has explicitly waived planning in this conversation.`;
|
|
4090
|
+
async function runSessionStart(cwd, stdout, outcome) {
|
|
4091
|
+
const resolverLines = [];
|
|
4092
|
+
emitResolverStatusLine(cwd, (line) => {
|
|
4093
|
+
stdout(line);
|
|
4094
|
+
resolverLines.push(line);
|
|
4095
|
+
});
|
|
4096
|
+
if (outcome && resolverLines.some((l) => l.includes("\u{1F534}"))) {
|
|
4097
|
+
outcome.configError = true;
|
|
4098
|
+
}
|
|
3952
4099
|
const pendingSyncDir = path21.join(cwd, ".cleargate", "delivery", "pending-sync");
|
|
3953
4100
|
let files;
|
|
3954
4101
|
try {
|
|
@@ -3957,6 +4104,7 @@ async function runSessionStart(cwd, stdout) {
|
|
|
3957
4104
|
return;
|
|
3958
4105
|
}
|
|
3959
4106
|
const blocked = [];
|
|
4107
|
+
let hasApprovedStory = false;
|
|
3960
4108
|
for (const filePath of files) {
|
|
3961
4109
|
let raw;
|
|
3962
4110
|
try {
|
|
@@ -3971,6 +4119,9 @@ async function runSessionStart(cwd, stdout) {
|
|
|
3971
4119
|
} catch {
|
|
3972
4120
|
continue;
|
|
3973
4121
|
}
|
|
4122
|
+
if (fm["approved"] === true) {
|
|
4123
|
+
hasApprovedStory = true;
|
|
4124
|
+
}
|
|
3974
4125
|
const gate2 = parseCachedGateResult2(fm["cached_gate_result"]);
|
|
3975
4126
|
if (!gate2 || gate2.pass !== false) continue;
|
|
3976
4127
|
const idKeys = ["story_id", "epic_id", "proposal_id", "cr_id", "bug_id", "sprint_id"];
|
|
@@ -3988,9 +4139,19 @@ async function runSessionStart(cwd, stdout) {
|
|
|
3988
4139
|
const firstCriterionId = gate2.failing_criteria.length > 0 ? gate2.failing_criteria[0]?.id ?? "" : "";
|
|
3989
4140
|
blocked.push({ id: itemId, firstCriterionId });
|
|
3990
4141
|
}
|
|
4142
|
+
const activesentinel = path21.join(cwd, ".cleargate", "sprint-runs", ".active");
|
|
4143
|
+
const sprintActive = fs20.existsSync(activesentinel);
|
|
4144
|
+
const shouldRemind = !hasApprovedStory && !sprintActive;
|
|
4145
|
+
if (shouldRemind) {
|
|
4146
|
+
stdout(PLANNING_FIRST_REMINDER);
|
|
4147
|
+
if (blocked.length > 0) {
|
|
4148
|
+
stdout("");
|
|
4149
|
+
}
|
|
4150
|
+
}
|
|
3991
4151
|
if (blocked.length === 0) {
|
|
3992
4152
|
return;
|
|
3993
4153
|
}
|
|
4154
|
+
if (outcome) outcome.blocker = true;
|
|
3994
4155
|
const overflow = blocked.length > SESSION_START_MAX_ITEMS ? blocked.length - SESSION_START_MAX_ITEMS : 0;
|
|
3995
4156
|
const visible = blocked.slice(0, SESSION_START_MAX_ITEMS);
|
|
3996
4157
|
const lines = [`${blocked.length} items blocked:`];
|
|
@@ -4007,10 +4168,11 @@ async function runSessionStart(cwd, stdout) {
|
|
|
4007
4168
|
}
|
|
4008
4169
|
stdout(output);
|
|
4009
4170
|
}
|
|
4010
|
-
async function runPricing(filePath, cwd, stdout, stderr, exit) {
|
|
4171
|
+
async function runPricing(filePath, cwd, stdout, stderr, exit, outcome) {
|
|
4011
4172
|
if (!filePath) {
|
|
4012
4173
|
stderr("cleargate doctor --pricing: missing <file> argument");
|
|
4013
|
-
|
|
4174
|
+
if (outcome) outcome.configError = true;
|
|
4175
|
+
exit(2);
|
|
4014
4176
|
return;
|
|
4015
4177
|
}
|
|
4016
4178
|
const absPath = path21.isAbsolute(filePath) ? filePath : path21.resolve(cwd, filePath);
|
|
@@ -4019,12 +4181,14 @@ async function runPricing(filePath, cwd, stdout, stderr, exit) {
|
|
|
4019
4181
|
raw = fs20.readFileSync(absPath, "utf-8");
|
|
4020
4182
|
} catch {
|
|
4021
4183
|
stderr(`cleargate doctor --pricing: cannot read file: ${absPath}`);
|
|
4022
|
-
|
|
4184
|
+
if (outcome) outcome.configError = true;
|
|
4185
|
+
exit(2);
|
|
4023
4186
|
return;
|
|
4024
4187
|
}
|
|
4025
4188
|
if (!raw.trimStart().startsWith("---")) {
|
|
4026
4189
|
stderr(`cleargate doctor --pricing: file has no frontmatter: ${absPath}`);
|
|
4027
|
-
|
|
4190
|
+
if (outcome) outcome.configError = true;
|
|
4191
|
+
exit(2);
|
|
4028
4192
|
return;
|
|
4029
4193
|
}
|
|
4030
4194
|
let fm;
|
|
@@ -4032,12 +4196,14 @@ async function runPricing(filePath, cwd, stdout, stderr, exit) {
|
|
|
4032
4196
|
fm = parseFrontmatter(raw).fm;
|
|
4033
4197
|
} catch {
|
|
4034
4198
|
stderr(`cleargate doctor --pricing: cannot parse frontmatter in: ${absPath}`);
|
|
4035
|
-
|
|
4199
|
+
if (outcome) outcome.configError = true;
|
|
4200
|
+
exit(2);
|
|
4036
4201
|
return;
|
|
4037
4202
|
}
|
|
4038
4203
|
const draftTokensRaw = fm["draft_tokens"];
|
|
4039
4204
|
if (!draftTokensRaw) {
|
|
4040
4205
|
stdout("draft_tokens unpopulated \u2014 run cleargate stamp-tokens first");
|
|
4206
|
+
if (outcome) outcome.blocker = true;
|
|
4041
4207
|
exit(1);
|
|
4042
4208
|
return;
|
|
4043
4209
|
}
|
|
@@ -4049,16 +4215,19 @@ async function runPricing(filePath, cwd, stdout, stderr, exit) {
|
|
|
4049
4215
|
draftTokens = JSON.parse(draftTokensRaw);
|
|
4050
4216
|
} catch {
|
|
4051
4217
|
stdout("draft_tokens unpopulated \u2014 run cleargate stamp-tokens first");
|
|
4218
|
+
if (outcome) outcome.blocker = true;
|
|
4052
4219
|
exit(1);
|
|
4053
4220
|
return;
|
|
4054
4221
|
}
|
|
4055
4222
|
} else {
|
|
4056
4223
|
stdout("draft_tokens unpopulated \u2014 run cleargate stamp-tokens first");
|
|
4224
|
+
if (outcome) outcome.blocker = true;
|
|
4057
4225
|
exit(1);
|
|
4058
4226
|
return;
|
|
4059
4227
|
}
|
|
4060
4228
|
if (draftTokens.input === null && draftTokens.output === null && draftTokens.cache_read === null && draftTokens.cache_creation === null) {
|
|
4061
4229
|
stdout("draft_tokens unpopulated \u2014 run cleargate stamp-tokens first");
|
|
4230
|
+
if (outcome) outcome.blocker = true;
|
|
4062
4231
|
exit(1);
|
|
4063
4232
|
return;
|
|
4064
4233
|
}
|
|
@@ -4076,18 +4245,95 @@ async function runPricing(filePath, cwd, stdout, stderr, exit) {
|
|
|
4076
4245
|
`${fileName}: ${model} \u2014 input:${input} output:${output} cache_read:${cacheRead} cache_creation:${cacheCreation} \u2248 $${usd.toFixed(4)}`
|
|
4077
4246
|
);
|
|
4078
4247
|
}
|
|
4248
|
+
function globMatch(pattern, filePath) {
|
|
4249
|
+
const normalPattern = pattern.replace(/\\/g, "/");
|
|
4250
|
+
const normalFile = filePath.replace(/\\/g, "/");
|
|
4251
|
+
const regexStr = normalPattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, "\0").replace(/\*/g, "[^/]*").replace(//g, ".*");
|
|
4252
|
+
const re = new RegExp(`^${regexStr}$`);
|
|
4253
|
+
return re.test(normalFile);
|
|
4254
|
+
}
|
|
4255
|
+
async function runCanEdit(filePath, cwd, stdout, exit, outcome) {
|
|
4256
|
+
const activeSentinel = path21.join(cwd, ".cleargate", "sprint-runs", ".active");
|
|
4257
|
+
if (fs20.existsSync(activeSentinel)) {
|
|
4258
|
+
stdout("allowed: sprint active");
|
|
4259
|
+
return;
|
|
4260
|
+
}
|
|
4261
|
+
const pendingSyncDir = path21.join(cwd, ".cleargate", "delivery", "pending-sync");
|
|
4262
|
+
let files;
|
|
4263
|
+
try {
|
|
4264
|
+
files = fs20.readdirSync(pendingSyncDir).filter((f) => f.endsWith(".md")).map((f) => path21.join(pendingSyncDir, f));
|
|
4265
|
+
} catch {
|
|
4266
|
+
stdout("blocked: no_approved_stories");
|
|
4267
|
+
if (outcome) outcome.blocker = true;
|
|
4268
|
+
exit(1);
|
|
4269
|
+
return;
|
|
4270
|
+
}
|
|
4271
|
+
let hasApprovedStory = false;
|
|
4272
|
+
let coveredByStory = false;
|
|
4273
|
+
for (const storyPath of files) {
|
|
4274
|
+
let raw;
|
|
4275
|
+
try {
|
|
4276
|
+
raw = fs20.readFileSync(storyPath, "utf-8");
|
|
4277
|
+
} catch {
|
|
4278
|
+
continue;
|
|
4279
|
+
}
|
|
4280
|
+
if (!raw.trimStart().startsWith("---")) continue;
|
|
4281
|
+
let fm;
|
|
4282
|
+
try {
|
|
4283
|
+
fm = parseFrontmatter(raw).fm;
|
|
4284
|
+
} catch {
|
|
4285
|
+
continue;
|
|
4286
|
+
}
|
|
4287
|
+
if (fm["approved"] !== true) continue;
|
|
4288
|
+
hasApprovedStory = true;
|
|
4289
|
+
const implFilesRaw = fm["implementation_files"];
|
|
4290
|
+
if (implFilesRaw === void 0 || implFilesRaw === null) {
|
|
4291
|
+
coveredByStory = true;
|
|
4292
|
+
break;
|
|
4293
|
+
}
|
|
4294
|
+
if (Array.isArray(implFilesRaw)) {
|
|
4295
|
+
for (const pattern of implFilesRaw) {
|
|
4296
|
+
if (typeof pattern !== "string") continue;
|
|
4297
|
+
if (globMatch(pattern, filePath)) {
|
|
4298
|
+
coveredByStory = true;
|
|
4299
|
+
break;
|
|
4300
|
+
}
|
|
4301
|
+
}
|
|
4302
|
+
}
|
|
4303
|
+
if (coveredByStory) break;
|
|
4304
|
+
}
|
|
4305
|
+
if (!hasApprovedStory) {
|
|
4306
|
+
stdout("blocked: no_approved_stories");
|
|
4307
|
+
if (outcome) outcome.blocker = true;
|
|
4308
|
+
exit(1);
|
|
4309
|
+
return;
|
|
4310
|
+
}
|
|
4311
|
+
if (!coveredByStory) {
|
|
4312
|
+
stdout("blocked: file_not_in_implementation_files");
|
|
4313
|
+
if (outcome) outcome.blocker = true;
|
|
4314
|
+
exit(1);
|
|
4315
|
+
return;
|
|
4316
|
+
}
|
|
4317
|
+
stdout("allowed");
|
|
4318
|
+
}
|
|
4079
4319
|
async function doctorHandler(flags, cli) {
|
|
4080
4320
|
const cwd = cli?.cwd ?? process.cwd();
|
|
4081
4321
|
const now = cli?.now ? cli.now() : /* @__PURE__ */ new Date();
|
|
4082
4322
|
const stdout = cli?.stdout ?? ((s) => process.stdout.write(s + "\n"));
|
|
4083
4323
|
const stderr = cli?.stderr ?? ((s) => process.stderr.write(s + "\n"));
|
|
4084
4324
|
const exit = cli?.exit ?? ((code) => process.exit(code));
|
|
4325
|
+
const outcome = { configError: false, blocker: false };
|
|
4326
|
+
let exitedEarly = false;
|
|
4327
|
+
const wrappedExit = (code) => {
|
|
4328
|
+
exitedEarly = true;
|
|
4329
|
+
return exit(code);
|
|
4330
|
+
};
|
|
4085
4331
|
let mode;
|
|
4086
4332
|
try {
|
|
4087
4333
|
mode = selectMode(flags);
|
|
4088
4334
|
} catch (err) {
|
|
4089
4335
|
stderr(err.message);
|
|
4090
|
-
exit(
|
|
4336
|
+
exit(2);
|
|
4091
4337
|
return;
|
|
4092
4338
|
}
|
|
4093
4339
|
switch (mode) {
|
|
@@ -4095,26 +4341,38 @@ async function doctorHandler(flags, cli) {
|
|
|
4095
4341
|
await runCheckScaffold(flags, cli ?? {}, cwd, now, stdout, stderr);
|
|
4096
4342
|
break;
|
|
4097
4343
|
case "hook-health":
|
|
4098
|
-
runHookHealth(stdout, cwd, now);
|
|
4344
|
+
runHookHealth(stdout, cwd, now, outcome);
|
|
4099
4345
|
break;
|
|
4100
4346
|
case "session-start":
|
|
4101
|
-
await runSessionStart(cwd, stdout);
|
|
4347
|
+
await runSessionStart(cwd, stdout, outcome);
|
|
4102
4348
|
break;
|
|
4103
4349
|
case "pricing":
|
|
4104
|
-
await runPricing(flags.pricingFile ?? "", cwd, stdout, stderr,
|
|
4350
|
+
await runPricing(flags.pricingFile ?? "", cwd, stdout, stderr, wrappedExit, outcome);
|
|
4351
|
+
break;
|
|
4352
|
+
case "can-edit":
|
|
4353
|
+
await runCanEdit(flags.canEditFile ?? "", cwd, stdout, wrappedExit, outcome);
|
|
4105
4354
|
break;
|
|
4106
4355
|
default: {
|
|
4107
4356
|
const exhaustiveCheck = mode;
|
|
4108
4357
|
stderr(`cleargate doctor: unknown mode '${String(exhaustiveCheck)}'`);
|
|
4109
|
-
exit(
|
|
4358
|
+
exit(2);
|
|
4359
|
+
return;
|
|
4110
4360
|
}
|
|
4111
4361
|
}
|
|
4362
|
+
if (exitedEarly) return;
|
|
4363
|
+
if (outcome.configError) {
|
|
4364
|
+
exit(2);
|
|
4365
|
+
} else if (outcome.blocker) {
|
|
4366
|
+
exit(1);
|
|
4367
|
+
} else {
|
|
4368
|
+
exit(0);
|
|
4369
|
+
}
|
|
4112
4370
|
}
|
|
4113
4371
|
|
|
4114
4372
|
// src/commands/gate.ts
|
|
4115
4373
|
import * as fs24 from "fs";
|
|
4116
4374
|
import * as path24 from "path";
|
|
4117
|
-
import { spawnSync as
|
|
4375
|
+
import { spawnSync as spawnSync7 } from "child_process";
|
|
4118
4376
|
|
|
4119
4377
|
// src/commands/execution-mode.ts
|
|
4120
4378
|
import * as fs21 from "fs";
|
|
@@ -4221,6 +4479,14 @@ function parsePredicate(src) {
|
|
|
4221
4479
|
const value = parseValue(rawVal);
|
|
4222
4480
|
return { kind: "frontmatter", ref, field, op, value };
|
|
4223
4481
|
}
|
|
4482
|
+
const markerNotMatch = s.match(/^body does not contain marker ['"]([A-Z]+)['"]$/);
|
|
4483
|
+
if (markerNotMatch) {
|
|
4484
|
+
const marker = markerNotMatch[1];
|
|
4485
|
+
if (marker !== "TBD" && marker !== "TODO" && marker !== "FIXME") {
|
|
4486
|
+
throw new Error(`unsupported predicate shape: ${src}`);
|
|
4487
|
+
}
|
|
4488
|
+
return { kind: "marker-absence", marker };
|
|
4489
|
+
}
|
|
4224
4490
|
const bodyNotMatch = s.match(/^body does not contain ['"](.+)['"]$/);
|
|
4225
4491
|
if (bodyNotMatch) {
|
|
4226
4492
|
return { kind: "body-contains", needle: bodyNotMatch[1], negated: true };
|
|
@@ -4276,6 +4542,8 @@ function evaluate(predicate, doc, opts) {
|
|
|
4276
4542
|
return evalFrontmatter(parsed, doc, projectRoot);
|
|
4277
4543
|
case "body-contains":
|
|
4278
4544
|
return evalBodyContains(parsed, doc);
|
|
4545
|
+
case "marker-absence":
|
|
4546
|
+
return evalMarkerAbsence(parsed, doc);
|
|
4279
4547
|
case "section":
|
|
4280
4548
|
return evalSection(parsed, doc);
|
|
4281
4549
|
case "file-exists":
|
|
@@ -4298,6 +4566,26 @@ function evalFrontmatter(parsed, doc, projectRoot) {
|
|
|
4298
4566
|
detail: `frontmatter key '${parsed.ref}' is missing or null in ${doc.absPath}`
|
|
4299
4567
|
};
|
|
4300
4568
|
}
|
|
4569
|
+
const refStr = String(refVal);
|
|
4570
|
+
const looksLikeProse = refStr.length > 200 || /[ —–:()\n]/.test(refStr);
|
|
4571
|
+
if (looksLikeProse) {
|
|
4572
|
+
const waiver = doc.fm["proposal_gate_waiver"];
|
|
4573
|
+
const hasExplicitWaiver = waiver !== null && waiver !== void 0 && waiver !== false && String(waiver).trim() !== "" && String(waiver).trim() !== "false";
|
|
4574
|
+
const approvedBy = doc.fm["approved_by"];
|
|
4575
|
+
const approvedAt = doc.fm["approved_at"];
|
|
4576
|
+
const hasApprovalFields = approvedBy !== null && approvedBy !== void 0 && String(approvedBy).trim() !== "" && approvedAt !== null && approvedAt !== void 0 && String(approvedAt).trim() !== "";
|
|
4577
|
+
const hasWaiver = hasExplicitWaiver || hasApprovalFields;
|
|
4578
|
+
if (hasWaiver) {
|
|
4579
|
+
return {
|
|
4580
|
+
pass: true,
|
|
4581
|
+
detail: `context_source is prose; proposal-gate waiver per frontmatter approved_by/approved_at`
|
|
4582
|
+
};
|
|
4583
|
+
}
|
|
4584
|
+
return {
|
|
4585
|
+
pass: false,
|
|
4586
|
+
detail: `context_source is prose but no proposal_gate_waiver (approved_by + approved_at) found in frontmatter`
|
|
4587
|
+
};
|
|
4588
|
+
}
|
|
4301
4589
|
const linkedPath = resolveLinkedPath(String(refVal), doc.absPath, projectRoot);
|
|
4302
4590
|
if (!linkedPath) {
|
|
4303
4591
|
return {
|
|
@@ -4422,6 +4710,38 @@ function evalBodyContains(parsed, doc) {
|
|
|
4422
4710
|
return { pass: false, detail: `'${needle}' not found in body` };
|
|
4423
4711
|
}
|
|
4424
4712
|
}
|
|
4713
|
+
function evalMarkerAbsence(parsed, doc) {
|
|
4714
|
+
const { marker } = parsed;
|
|
4715
|
+
const lines = doc.body.split("\n");
|
|
4716
|
+
const templateSelfRefRe = /^\s*-\s*\[[x ]\]\s*0\s*"TBDs?"\s*exist/i;
|
|
4717
|
+
const markerRe = new RegExp(
|
|
4718
|
+
`(?:^|(?<=\\())${marker}(?=:)|\\(${marker}\\)|\\[${marker}\\]|(?<=//\\s*)${marker}(?!\\w)|(?<=#\\s*)${marker}(?!\\w)`,
|
|
4719
|
+
// # MARKER (comment)
|
|
4720
|
+
"g"
|
|
4721
|
+
);
|
|
4722
|
+
const bareLineRe = new RegExp(`^${marker}$`);
|
|
4723
|
+
const violations = [];
|
|
4724
|
+
for (let i = 0; i < lines.length; i++) {
|
|
4725
|
+
const line = lines[i];
|
|
4726
|
+
if (templateSelfRefRe.test(line)) continue;
|
|
4727
|
+
const trimmed = line.trim();
|
|
4728
|
+
if (bareLineRe.test(trimmed)) {
|
|
4729
|
+
violations.push(i + 1);
|
|
4730
|
+
continue;
|
|
4731
|
+
}
|
|
4732
|
+
markerRe.lastIndex = 0;
|
|
4733
|
+
if (markerRe.test(line)) {
|
|
4734
|
+
violations.push(i + 1);
|
|
4735
|
+
}
|
|
4736
|
+
}
|
|
4737
|
+
if (violations.length > 0) {
|
|
4738
|
+
return {
|
|
4739
|
+
pass: false,
|
|
4740
|
+
detail: `${violations.length} marker occurrence${violations.length === 1 ? "" : "s"} of '${marker}' at line${violations.length === 1 ? "" : "s"} ${violations.join(", ")}`
|
|
4741
|
+
};
|
|
4742
|
+
}
|
|
4743
|
+
return { pass: true, detail: `no '${marker}' markers found in body` };
|
|
4744
|
+
}
|
|
4425
4745
|
function evalSection(parsed, doc) {
|
|
4426
4746
|
const body = doc.body;
|
|
4427
4747
|
const rawParts = body.split(/^(?=## )/m);
|
|
@@ -4789,7 +5109,7 @@ function gateQaHandler(opts, cli) {
|
|
|
4789
5109
|
const stdoutFn = cli?.stdout ?? ((s) => process.stdout.write(s + "\n"));
|
|
4790
5110
|
const stderrFn = cli?.stderr ?? ((s) => process.stderr.write(s + "\n"));
|
|
4791
5111
|
const exitFn = cli?.exit ?? ((code2) => process.exit(code2));
|
|
4792
|
-
const spawnFn = cli?.spawnFn ??
|
|
5112
|
+
const spawnFn = cli?.spawnFn ?? spawnSync7;
|
|
4793
5113
|
const sprintId = cli?.sprintId ?? "SPRINT-UNKNOWN";
|
|
4794
5114
|
const mode = readSprintExecutionMode(sprintId, {
|
|
4795
5115
|
sprintFilePath: cli?.sprintFilePath,
|
|
@@ -4815,7 +5135,7 @@ function gateArchHandler(opts, cli) {
|
|
|
4815
5135
|
const stdoutFn = cli?.stdout ?? ((s) => process.stdout.write(s + "\n"));
|
|
4816
5136
|
const stderrFn = cli?.stderr ?? ((s) => process.stderr.write(s + "\n"));
|
|
4817
5137
|
const exitFn = cli?.exit ?? ((code2) => process.exit(code2));
|
|
4818
|
-
const spawnFn = cli?.spawnFn ??
|
|
5138
|
+
const spawnFn = cli?.spawnFn ?? spawnSync7;
|
|
4819
5139
|
const sprintId = cli?.sprintId ?? "SPRINT-UNKNOWN";
|
|
4820
5140
|
const mode = readSprintExecutionMode(sprintId, {
|
|
4821
5141
|
sprintFilePath: cli?.sprintFilePath,
|
|
@@ -4839,13 +5159,13 @@ function gateArchHandler(opts, cli) {
|
|
|
4839
5159
|
}
|
|
4840
5160
|
|
|
4841
5161
|
// src/commands/gate-run.ts
|
|
4842
|
-
import { spawnSync as
|
|
5162
|
+
import { spawnSync as spawnSync8 } from "child_process";
|
|
4843
5163
|
var KNOWN_GATES = ["precommit", "test", "typecheck", "lint"];
|
|
4844
5164
|
function gateRunHandler(name, opts, cli) {
|
|
4845
5165
|
const stdoutFn = cli?.stdout ?? ((s) => process.stdout.write(s + "\n"));
|
|
4846
5166
|
const stderrFn = cli?.stderr ?? ((s) => process.stderr.write(s + "\n"));
|
|
4847
5167
|
const exitFn = cli?.exit ?? ((code) => process.exit(code));
|
|
4848
|
-
const spawnFn = cli?.spawnFn ??
|
|
5168
|
+
const spawnFn = cli?.spawnFn ?? spawnSync8;
|
|
4849
5169
|
const cwd = cli?.cwd ?? process.cwd();
|
|
4850
5170
|
const configLoaderFn = cli?.configLoader ?? loadWikiConfig;
|
|
4851
5171
|
if (!KNOWN_GATES.includes(name)) {
|
|
@@ -4877,7 +5197,7 @@ function gateRunHandler(name, opts, cli) {
|
|
|
4877
5197
|
// src/commands/sprint.ts
|
|
4878
5198
|
import * as fs25 from "fs";
|
|
4879
5199
|
import * as path25 from "path";
|
|
4880
|
-
import { spawnSync as
|
|
5200
|
+
import { spawnSync as spawnSync9 } from "child_process";
|
|
4881
5201
|
import yaml7 from "js-yaml";
|
|
4882
5202
|
var TERMINAL_STATUSES2 = /* @__PURE__ */ new Set(["Completed", "Done", "Abandoned", "Closed", "Resolved"]);
|
|
4883
5203
|
function resolveRunScript(opts) {
|
|
@@ -4892,7 +5212,7 @@ function sprintInitHandler(opts, cli) {
|
|
|
4892
5212
|
const stdoutFn = cli?.stdout ?? ((s) => process.stdout.write(s + "\n"));
|
|
4893
5213
|
const stderrFn = cli?.stderr ?? ((s) => process.stderr.write(s + "\n"));
|
|
4894
5214
|
const exitFn = cli?.exit ?? defaultExit;
|
|
4895
|
-
const spawnFn = cli?.spawnFn ??
|
|
5215
|
+
const spawnFn = cli?.spawnFn ?? spawnSync9;
|
|
4896
5216
|
const mode = readSprintExecutionMode(opts.sprintId, {
|
|
4897
5217
|
sprintFilePath: cli?.sprintFilePath,
|
|
4898
5218
|
cwd: cli?.cwd
|
|
@@ -4914,7 +5234,7 @@ function sprintCloseHandler(opts, cli) {
|
|
|
4914
5234
|
const stdoutFn = cli?.stdout ?? ((s) => process.stdout.write(s + "\n"));
|
|
4915
5235
|
const stderrFn = cli?.stderr ?? ((s) => process.stderr.write(s + "\n"));
|
|
4916
5236
|
const exitFn = cli?.exit ?? defaultExit;
|
|
4917
|
-
const spawnFn = cli?.spawnFn ??
|
|
5237
|
+
const spawnFn = cli?.spawnFn ?? spawnSync9;
|
|
4918
5238
|
const mode = readSprintExecutionMode(opts.sprintId, {
|
|
4919
5239
|
sprintFilePath: cli?.sprintFilePath,
|
|
4920
5240
|
cwd: cli?.cwd
|
|
@@ -5019,7 +5339,7 @@ async function sprintArchiveHandler(opts, cli) {
|
|
|
5019
5339
|
const stdoutFn = cli?.stdout ?? ((s) => process.stdout.write(s + "\n"));
|
|
5020
5340
|
const stderrFn = cli?.stderr ?? ((s) => process.stderr.write(s + "\n"));
|
|
5021
5341
|
const exitFn = cli?.exit ?? defaultExit;
|
|
5022
|
-
const spawnFn = cli?.spawnFn ??
|
|
5342
|
+
const spawnFn = cli?.spawnFn ?? spawnSync9;
|
|
5023
5343
|
const cwd = cli?.cwd ?? process.cwd();
|
|
5024
5344
|
const wikiBuildFn = cli?.wikiBuildFn ?? (async (wCwd, wStdout) => {
|
|
5025
5345
|
const fakeExit = (code) => {
|
|
@@ -5243,7 +5563,7 @@ async function sprintArchiveHandler(opts, cli) {
|
|
|
5243
5563
|
// src/commands/story.ts
|
|
5244
5564
|
import * as fs26 from "fs";
|
|
5245
5565
|
import * as path26 from "path";
|
|
5246
|
-
import { spawnSync as
|
|
5566
|
+
import { spawnSync as spawnSync10 } from "child_process";
|
|
5247
5567
|
function defaultExit2(code) {
|
|
5248
5568
|
return process.exit(code);
|
|
5249
5569
|
}
|
|
@@ -5269,7 +5589,7 @@ function storyStartHandler(opts, cli) {
|
|
|
5269
5589
|
const stdoutFn = cli?.stdout ?? ((s) => process.stdout.write(s + "\n"));
|
|
5270
5590
|
const stderrFn = cli?.stderr ?? ((s) => process.stderr.write(s + "\n"));
|
|
5271
5591
|
const exitFn = cli?.exit ?? defaultExit2;
|
|
5272
|
-
const spawnFn = cli?.spawnFn ??
|
|
5592
|
+
const spawnFn = cli?.spawnFn ?? spawnSync10;
|
|
5273
5593
|
const cwd = cli?.cwd ?? process.cwd();
|
|
5274
5594
|
const sprintId = cli?.sprintId ?? resolveSprintIdFromSentinel(cwd) ?? "SPRINT-UNKNOWN";
|
|
5275
5595
|
const mode = readSprintExecutionMode(sprintId, {
|
|
@@ -5347,7 +5667,7 @@ function storyCompleteHandler(opts, cli) {
|
|
|
5347
5667
|
const stdoutFn = cli?.stdout ?? ((s) => process.stdout.write(s + "\n"));
|
|
5348
5668
|
const stderrFn = cli?.stderr ?? ((s) => process.stderr.write(s + "\n"));
|
|
5349
5669
|
const exitFn = cli?.exit ?? defaultExit2;
|
|
5350
|
-
const spawnFn = cli?.spawnFn ??
|
|
5670
|
+
const spawnFn = cli?.spawnFn ?? spawnSync10;
|
|
5351
5671
|
const cwd = cli?.cwd ?? process.cwd();
|
|
5352
5672
|
const sprintId = cli?.sprintId ?? resolveSprintIdFromSentinel(cwd) ?? "SPRINT-UNKNOWN";
|
|
5353
5673
|
const mode = readSprintExecutionMode(sprintId, {
|
|
@@ -5457,7 +5777,7 @@ function storyCompleteHandler(opts, cli) {
|
|
|
5457
5777
|
|
|
5458
5778
|
// src/commands/state.ts
|
|
5459
5779
|
import * as path27 from "path";
|
|
5460
|
-
import { spawnSync as
|
|
5780
|
+
import { spawnSync as spawnSync11 } from "child_process";
|
|
5461
5781
|
function defaultExit3(code) {
|
|
5462
5782
|
return process.exit(code);
|
|
5463
5783
|
}
|
|
@@ -5470,7 +5790,7 @@ function stateUpdateHandler(opts, cli) {
|
|
|
5470
5790
|
const stdoutFn = cli?.stdout ?? ((s) => process.stdout.write(s + "\n"));
|
|
5471
5791
|
const stderrFn = cli?.stderr ?? ((s) => process.stderr.write(s + "\n"));
|
|
5472
5792
|
const exitFn = cli?.exit ?? defaultExit3;
|
|
5473
|
-
const spawnFn = cli?.spawnFn ??
|
|
5793
|
+
const spawnFn = cli?.spawnFn ?? spawnSync11;
|
|
5474
5794
|
const cwd = cli?.cwd;
|
|
5475
5795
|
const sprintId = cli?.sprintId ?? resolveSprintIdFromSentinel(cwd) ?? "SPRINT-UNKNOWN";
|
|
5476
5796
|
const mode = readSprintExecutionMode(sprintId, {
|
|
@@ -5497,7 +5817,7 @@ function stateValidateHandler(opts, cli) {
|
|
|
5497
5817
|
const stdoutFn = cli?.stdout ?? ((s) => process.stdout.write(s + "\n"));
|
|
5498
5818
|
const stderrFn = cli?.stderr ?? ((s) => process.stderr.write(s + "\n"));
|
|
5499
5819
|
const exitFn = cli?.exit ?? defaultExit3;
|
|
5500
|
-
const spawnFn = cli?.spawnFn ??
|
|
5820
|
+
const spawnFn = cli?.spawnFn ?? spawnSync11;
|
|
5501
5821
|
const mode = readSprintExecutionMode(opts.sprintId, {
|
|
5502
5822
|
sprintFilePath: cli?.sprintFilePath,
|
|
5503
5823
|
cwd: cli?.cwd
|
|
@@ -8471,6 +8791,108 @@ async function adminLoginHandler(opts = {}) {
|
|
|
8471
8791
|
stdout(`Credentials saved to ${authFilePath} (chmod 600).`);
|
|
8472
8792
|
}
|
|
8473
8793
|
|
|
8794
|
+
// src/commands/hotfix.ts
|
|
8795
|
+
import * as fs34 from "fs";
|
|
8796
|
+
import * as path44 from "path";
|
|
8797
|
+
function defaultExit4(code) {
|
|
8798
|
+
return process.exit(code);
|
|
8799
|
+
}
|
|
8800
|
+
var SLUG_RE = /^[a-z0-9-]+$/;
|
|
8801
|
+
var HOTFIX_FILE_RE = /^HOTFIX-(\d+)_.*\.md$/;
|
|
8802
|
+
function maxHotfixId(pendingDir) {
|
|
8803
|
+
let max = 0;
|
|
8804
|
+
let entries;
|
|
8805
|
+
try {
|
|
8806
|
+
entries = fs34.readdirSync(pendingDir);
|
|
8807
|
+
} catch {
|
|
8808
|
+
return 0;
|
|
8809
|
+
}
|
|
8810
|
+
for (const entry of entries) {
|
|
8811
|
+
const m = HOTFIX_FILE_RE.exec(entry);
|
|
8812
|
+
if (m) {
|
|
8813
|
+
const n = parseInt(m[1], 10);
|
|
8814
|
+
if (n > max) max = n;
|
|
8815
|
+
}
|
|
8816
|
+
}
|
|
8817
|
+
return max;
|
|
8818
|
+
}
|
|
8819
|
+
function countActiveHotfixes(repoRoot) {
|
|
8820
|
+
const pendingDir = path44.join(repoRoot, ".cleargate", "delivery", "pending-sync");
|
|
8821
|
+
const archiveDir = path44.join(repoRoot, ".cleargate", "delivery", "archive");
|
|
8822
|
+
const sevenDaysAgo = Date.now() - 7 * 24 * 60 * 60 * 1e3;
|
|
8823
|
+
let count = 0;
|
|
8824
|
+
let pendingEntries = [];
|
|
8825
|
+
try {
|
|
8826
|
+
pendingEntries = fs34.readdirSync(pendingDir);
|
|
8827
|
+
} catch {
|
|
8828
|
+
}
|
|
8829
|
+
for (const entry of pendingEntries) {
|
|
8830
|
+
if (entry.startsWith("HOTFIX-") && entry.endsWith(".md")) count++;
|
|
8831
|
+
}
|
|
8832
|
+
let archiveEntries = [];
|
|
8833
|
+
try {
|
|
8834
|
+
archiveEntries = fs34.readdirSync(archiveDir);
|
|
8835
|
+
} catch {
|
|
8836
|
+
}
|
|
8837
|
+
for (const entry of archiveEntries) {
|
|
8838
|
+
if (entry.startsWith("HOTFIX-") && entry.endsWith(".md")) {
|
|
8839
|
+
try {
|
|
8840
|
+
const stat = fs34.statSync(path44.join(archiveDir, entry));
|
|
8841
|
+
if (stat.mtimeMs >= sevenDaysAgo) count++;
|
|
8842
|
+
} catch {
|
|
8843
|
+
}
|
|
8844
|
+
}
|
|
8845
|
+
}
|
|
8846
|
+
return count;
|
|
8847
|
+
}
|
|
8848
|
+
function resolveTemplatePath(repoRoot) {
|
|
8849
|
+
return path44.join(repoRoot, ".cleargate", "templates", "hotfix.md");
|
|
8850
|
+
}
|
|
8851
|
+
function hotfixNewHandler(opts, cli) {
|
|
8852
|
+
const stdoutFn = cli?.stdout ?? ((s) => process.stdout.write(s + "\n"));
|
|
8853
|
+
const stderrFn = cli?.stderr ?? ((s) => process.stderr.write(s + "\n"));
|
|
8854
|
+
const exitFn = cli?.exit ?? defaultExit4;
|
|
8855
|
+
const repoRoot = cli?.cwd ?? process.cwd();
|
|
8856
|
+
const now = cli?.now ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
8857
|
+
if (!SLUG_RE.test(opts.slug)) {
|
|
8858
|
+
stderrFn(`[cleargate hotfix new] slug must match ^[a-z0-9-]+$ (got: "${opts.slug}")`);
|
|
8859
|
+
return exitFn(1);
|
|
8860
|
+
}
|
|
8861
|
+
const activeCount = countActiveHotfixes(repoRoot);
|
|
8862
|
+
if (activeCount >= 3) {
|
|
8863
|
+
stderrFn(
|
|
8864
|
+
`Hotfix cap: \u22643 per rolling 7-day window. Currently ${activeCount} active. Bundle into a sprint or downgrade one to a CR.`
|
|
8865
|
+
);
|
|
8866
|
+
return exitFn(1);
|
|
8867
|
+
}
|
|
8868
|
+
const pendingDir = path44.join(repoRoot, ".cleargate", "delivery", "pending-sync");
|
|
8869
|
+
const maxId = maxHotfixId(pendingDir);
|
|
8870
|
+
const nextId = maxId + 1;
|
|
8871
|
+
const idStr = `HOTFIX-${String(nextId).padStart(3, "0")}`;
|
|
8872
|
+
const templatePath = resolveTemplatePath(repoRoot);
|
|
8873
|
+
let templateContent;
|
|
8874
|
+
try {
|
|
8875
|
+
templateContent = fs34.readFileSync(templatePath, "utf8");
|
|
8876
|
+
} catch {
|
|
8877
|
+
stderrFn(`[cleargate hotfix new] template not found: ${templatePath}`);
|
|
8878
|
+
return exitFn(2);
|
|
8879
|
+
}
|
|
8880
|
+
const content = templateContent.replace(/\{ID\}/g, idStr).replace(/\{SLUG\}/g, opts.slug).replace(/\{ISO\}/g, now);
|
|
8881
|
+
const fileSlug = opts.slug.replace(/-/g, "_");
|
|
8882
|
+
const fileName = `${idStr}_${fileSlug}.md`;
|
|
8883
|
+
const outPath = path44.join(pendingDir, fileName);
|
|
8884
|
+
try {
|
|
8885
|
+
fs34.mkdirSync(pendingDir, { recursive: true });
|
|
8886
|
+
fs34.writeFileSync(outPath, content, "utf8");
|
|
8887
|
+
} catch (err) {
|
|
8888
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
8889
|
+
stderrFn(`[cleargate hotfix new] write failed: ${msg}`);
|
|
8890
|
+
return exitFn(1);
|
|
8891
|
+
}
|
|
8892
|
+
stdoutFn(`[cleargate hotfix new] created: ${outPath}`);
|
|
8893
|
+
return exitFn(0);
|
|
8894
|
+
}
|
|
8895
|
+
|
|
8474
8896
|
// src/cli.ts
|
|
8475
8897
|
var program = new Command();
|
|
8476
8898
|
program.name("cleargate").description("ClearGate CLI \u2014 connects AI agent teams to the ClearGate MCP server").version(package_default.version, "-V, --version").option("--profile <name>", "configuration profile to use", "default").option("--mcp-url <url>", "MCP server URL (overrides config file and env)").showHelpAfterError("(use `cleargate --help`)");
|
|
@@ -8487,8 +8909,8 @@ program.command("join <invite-url>").description("join a ClearGate workspace usi
|
|
|
8487
8909
|
...cmdOpts.code !== void 0 ? { code: cmdOpts.code } : {}
|
|
8488
8910
|
});
|
|
8489
8911
|
});
|
|
8490
|
-
program.command("init").description("initialise a repo with ClearGate scaffold (CLAUDE.md block, hook config, agents, templates)").option("--force", "overwrite existing files that differ from the bundled payload").option("--yes", "non-interactive: accept all defaults without prompting").action(async (opts) => {
|
|
8491
|
-
await initHandler({ force: opts.force ?? false, yes: opts.yes ?? false });
|
|
8912
|
+
program.command("init").description("initialise a repo with ClearGate scaffold (CLAUDE.md block, hook config, agents, templates)").option("--force", "overwrite existing files that differ from the bundled payload").option("--yes", "non-interactive: accept all defaults without prompting").option("--pin <ver>", "CR-009: pin hook resolver to a specific cleargate CLI version (default: package version)").action(async (opts) => {
|
|
8913
|
+
await initHandler({ force: opts.force ?? false, yes: opts.yes ?? false, pin: opts.pin });
|
|
8492
8914
|
});
|
|
8493
8915
|
program.command("whoami").description("print the currently authenticated agent identity").action(async () => {
|
|
8494
8916
|
const { whoamiHandler } = await import("./whoami-CX7CXJD5.js");
|
|
@@ -8610,14 +9032,20 @@ admin.command("bootstrap-root <handle>").description("seed the first root admin
|
|
|
8610
9032
|
const { bootstrapRootHandler } = await import("./bootstrap-root-FGWDICDT.js");
|
|
8611
9033
|
await bootstrapRootHandler({ handle, databaseUrl: opts.databaseUrl, force: opts.force ?? false });
|
|
8612
9034
|
});
|
|
8613
|
-
program.command("doctor").description("diagnose scaffold drift, hook health, blocked items, and token cost").option("--check-scaffold", "check scaffold files for drift against install snapshot").option("--session-start-mode", "hidden: enables daily throttle (used by session-start hook)", false).option("--session-start", "emit blocked pending-sync items summary (used by SessionStart hook)").option("--pricing <file>", "compute USD cost estimate from a work item's draft_tokens").option("-v, --verbose", "show per-file drift detail").addHelpText("after", [
|
|
9035
|
+
program.command("doctor").description("diagnose scaffold drift, hook health, blocked items, and token cost").option("--check-scaffold", "check scaffold files for drift against install snapshot").option("--session-start-mode", "hidden: enables daily throttle (used by session-start hook)", false).option("--session-start", "emit blocked pending-sync items summary (used by SessionStart hook)").option("--pricing <file>", "compute USD cost estimate from a work item's draft_tokens").option("--can-edit <file>", "CR-008: exit 0 if editing file is allowed, exit 1 if planning required").option("--cwd <dir>", "working directory for the doctor check (default: process.cwd())").option("-v, --verbose", "show per-file drift detail").addHelpText("after", [
|
|
8614
9036
|
"",
|
|
8615
9037
|
"Modes (mutually exclusive):",
|
|
8616
9038
|
" --check-scaffold Compute drift for all tracked scaffold files.",
|
|
8617
9039
|
" Writes .cleargate/.drift-state.json.",
|
|
8618
9040
|
" --session-start List blocked pending-sync items (\u226410, \u2264100 tokens).",
|
|
8619
9041
|
" --pricing <file> Compute USD estimate from a work item's draft_tokens.",
|
|
8620
|
-
"
|
|
9042
|
+
" --can-edit <file> Check if editing a file requires a planning work item.",
|
|
9043
|
+
" (default) Print a minimal hook-config health report.",
|
|
9044
|
+
"",
|
|
9045
|
+
"Exit codes:",
|
|
9046
|
+
" 0 Clean \u2014 no blockers, no config errors.",
|
|
9047
|
+
" 1 Blocked items or advisory issues \u2014 see stdout.",
|
|
9048
|
+
" 2 ClearGate misconfigured or partially installed \u2014 see stdout for remediation."
|
|
8621
9049
|
].join("\n")).action(async (opts) => {
|
|
8622
9050
|
await doctorHandler({
|
|
8623
9051
|
checkScaffold: opts.checkScaffold,
|
|
@@ -8625,8 +9053,10 @@ program.command("doctor").description("diagnose scaffold drift, hook health, blo
|
|
|
8625
9053
|
sessionStart: opts.sessionStart,
|
|
8626
9054
|
pricing: !!opts.pricing,
|
|
8627
9055
|
pricingFile: opts.pricing,
|
|
9056
|
+
canEdit: !!opts.canEdit,
|
|
9057
|
+
canEditFile: opts.canEdit,
|
|
8628
9058
|
verbose: opts.verbose
|
|
8629
|
-
});
|
|
9059
|
+
}, opts.cwd ? { cwd: opts.cwd } : void 0);
|
|
8630
9060
|
});
|
|
8631
9061
|
program.command("upgrade").description("three-way merge scaffold files with upstream changes").option("--dry-run", "print plan without making any changes").option("--yes", 'auto-accept "take theirs" for all merge-3way files (non-interactive)').option("--only <tier>", "restrict to a specific scaffold tier (protocol/template/agent/hook/skill/cli-config)").addHelpText("after", [
|
|
8632
9062
|
"",
|
|
@@ -8648,7 +9078,7 @@ program.command("uninstall").description("remove ClearGate scaffold from a proje
|
|
|
8648
9078
|
"",
|
|
8649
9079
|
"Always removed (no prompt): .claude/agents/*.md, ClearGate hooks,",
|
|
8650
9080
|
" .claude/skills/flashcard/, CLAUDE.md CLEARGATE block,",
|
|
8651
|
-
"
|
|
9081
|
+
" `cleargate` from package.json, .install-manifest.json, .drift-state.json.",
|
|
8652
9082
|
"",
|
|
8653
9083
|
"Non-git targets: uncommitted-changes check is skipped silently."
|
|
8654
9084
|
].join("\n")).action(async (opts) => {
|
|
@@ -8701,5 +9131,9 @@ program.command("sync-log").description("filter and print sync-log entries").opt
|
|
|
8701
9131
|
limit: opts.limit !== void 0 ? parseInt(opts.limit, 10) : 50
|
|
8702
9132
|
});
|
|
8703
9133
|
});
|
|
9134
|
+
var hotfix = program.command("hotfix").description("hotfix lane commands (off-sprint trivial fix scaffolding)");
|
|
9135
|
+
hotfix.command("new <slug>").description("scaffold a new HOTFIX-NNN_<slug>.md in pending-sync/").action((slug) => {
|
|
9136
|
+
hotfixNewHandler({ slug });
|
|
9137
|
+
});
|
|
8704
9138
|
void program.parseAsync(process.argv);
|
|
8705
9139
|
//# sourceMappingURL=cli.js.map
|