cleargate 0.5.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/MANIFEST.json +30 -16
- package/dist/cli.cjs +485 -51
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +480 -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.1",
|
|
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,46 @@ 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{1F7E1} cleargate CLI: not resolvable in this environment.
|
|
2543
|
+
[cleargate init] Attempted: ${branchLabel}
|
|
2544
|
+
[cleargate init] This is a warning, not a fatal error. Hooks will no-op until resolved.
|
|
2545
|
+
[cleargate init] Fix: npm i -g cleargate@${pinVersion} or npx cleargate@${pinVersion} doctor
|
|
2546
|
+
`
|
|
2547
|
+
);
|
|
2548
|
+
}
|
|
2549
|
+
}
|
|
2550
|
+
}
|
|
2470
2551
|
const existingParticipant = readParticipant(cwd);
|
|
2471
2552
|
if (existingParticipant === null) {
|
|
2472
2553
|
const identityOpts = opts.identityOpts ?? {};
|
|
@@ -2484,8 +2565,10 @@ async function initHandler(opts = {}) {
|
|
|
2484
2565
|
stdout(`[cleargate init] Participant identity: ${finalEmail} (inferred)
|
|
2485
2566
|
`);
|
|
2486
2567
|
} else {
|
|
2487
|
-
const
|
|
2488
|
-
const
|
|
2568
|
+
const isNoreply = gitEmail !== null && /@users\.noreply\.github\.com$/i.test(gitEmail);
|
|
2569
|
+
const defaultEmail = gitEmail !== null && !isNoreply ? gitEmail : "user@localhost";
|
|
2570
|
+
stdout("\n");
|
|
2571
|
+
const question = `Participant email (press Enter for default) [${defaultEmail}]:`;
|
|
2489
2572
|
const answer = await promptEmailFn(question, defaultEmail);
|
|
2490
2573
|
await writeParticipant(cwd, answer, "prompted", now);
|
|
2491
2574
|
stdout(`[cleargate init] Participant identity: ${answer} (prompted)
|
|
@@ -2502,7 +2585,7 @@ async function initHandler(opts = {}) {
|
|
|
2502
2585
|
// src/commands/wiki-ingest.ts
|
|
2503
2586
|
import * as fs14 from "fs";
|
|
2504
2587
|
import * as path14 from "path";
|
|
2505
|
-
import { spawnSync as
|
|
2588
|
+
import { spawnSync as spawnSync4 } from "child_process";
|
|
2506
2589
|
var EXCLUDED_SUFFIXES2 = [
|
|
2507
2590
|
".cleargate/knowledge/",
|
|
2508
2591
|
".cleargate/templates/",
|
|
@@ -2654,7 +2737,7 @@ function checkContentUnchanged(absRawPath, sha, relRawPath, gitRunner) {
|
|
|
2654
2737
|
}
|
|
2655
2738
|
}
|
|
2656
2739
|
function defaultGitRunner(cmd, args) {
|
|
2657
|
-
const result =
|
|
2740
|
+
const result = spawnSync4(cmd, args, { encoding: "utf8" });
|
|
2658
2741
|
if (result.status !== 0) return "\0__NONZERO__";
|
|
2659
2742
|
return result.stdout ?? "";
|
|
2660
2743
|
}
|
|
@@ -2903,7 +2986,7 @@ function loadWikiPages(wikiRoot) {
|
|
|
2903
2986
|
// src/wiki/lint-checks.ts
|
|
2904
2987
|
import * as fs16 from "fs";
|
|
2905
2988
|
import * as path16 from "path";
|
|
2906
|
-
import { spawnSync as
|
|
2989
|
+
import { spawnSync as spawnSync5 } from "child_process";
|
|
2907
2990
|
import yaml3 from "js-yaml";
|
|
2908
2991
|
|
|
2909
2992
|
// src/lib/work-item-type.ts
|
|
@@ -2998,7 +3081,7 @@ function checkStaleCommit(page, repoRoot, gitRunner) {
|
|
|
2998
3081
|
if (gitRunner) {
|
|
2999
3082
|
currentSha = gitRunner("git", ["log", "-1", "--format=%H", "--", rawPath]).trim();
|
|
3000
3083
|
} else {
|
|
3001
|
-
const result =
|
|
3084
|
+
const result = spawnSync5("git", ["log", "-1", "--format=%H", "--", rawPath], {
|
|
3002
3085
|
encoding: "utf8",
|
|
3003
3086
|
cwd: repoRoot
|
|
3004
3087
|
});
|
|
@@ -3739,6 +3822,7 @@ function applyStatusFix(rawText, newStatus) {
|
|
|
3739
3822
|
// src/commands/doctor.ts
|
|
3740
3823
|
import * as fs20 from "fs";
|
|
3741
3824
|
import * as path21 from "path";
|
|
3825
|
+
import { spawnSync as spawnSync6 } from "child_process";
|
|
3742
3826
|
|
|
3743
3827
|
// src/lib/pricing.ts
|
|
3744
3828
|
var PRICING_TABLE = {
|
|
@@ -3787,6 +3871,7 @@ function selectMode(flags) {
|
|
|
3787
3871
|
if (flags.checkScaffold) modes.push("check-scaffold");
|
|
3788
3872
|
if (flags.sessionStart) modes.push("session-start");
|
|
3789
3873
|
if (flags.pricing) modes.push("pricing");
|
|
3874
|
+
if (flags.canEdit) modes.push("can-edit");
|
|
3790
3875
|
if (modes.length > 1) {
|
|
3791
3876
|
throw new Error(
|
|
3792
3877
|
`cleargate doctor: mutually exclusive flags set: ${modes.join(", ")}. Use only one mode flag at a time.`
|
|
@@ -3811,7 +3896,18 @@ function parseHookLogLine(line) {
|
|
|
3811
3896
|
file: m[5].trim()
|
|
3812
3897
|
};
|
|
3813
3898
|
}
|
|
3814
|
-
function runHookHealth(stdout, cwd, now) {
|
|
3899
|
+
function runHookHealth(stdout, cwd, now, outcome) {
|
|
3900
|
+
const cleargateDir = path21.join(cwd, ".cleargate");
|
|
3901
|
+
if (!fs20.existsSync(cleargateDir)) {
|
|
3902
|
+
stdout("cleargate misconfigured: no .cleargate/ found. Run: cleargate init");
|
|
3903
|
+
if (outcome) outcome.configError = true;
|
|
3904
|
+
return;
|
|
3905
|
+
}
|
|
3906
|
+
const manifestPath = path21.join(cwd, "cleargate-planning", "MANIFEST.json");
|
|
3907
|
+
if (!fs20.existsSync(manifestPath)) {
|
|
3908
|
+
stdout(`cleargate misconfigured: cleargate-planning/MANIFEST.json not found. Run: cleargate init`);
|
|
3909
|
+
if (outcome) outcome.configError = true;
|
|
3910
|
+
}
|
|
3815
3911
|
const settingsPath = path21.join(cwd, ".claude", "settings.json");
|
|
3816
3912
|
if (!fs20.existsSync(settingsPath)) {
|
|
3817
3913
|
stdout("[doctor] No .claude/settings.json found \u2014 hook config unavailable.");
|
|
@@ -3948,7 +4044,57 @@ function parseCachedGateResult2(raw) {
|
|
|
3948
4044
|
failing_criteria: parsed.failing_criteria ?? []
|
|
3949
4045
|
};
|
|
3950
4046
|
}
|
|
3951
|
-
|
|
4047
|
+
function emitResolverStatusLine(cwd, stdout) {
|
|
4048
|
+
const distCliPath = path21.join(cwd, "cleargate-cli", "dist", "cli.js");
|
|
4049
|
+
if (fs20.existsSync(distCliPath)) {
|
|
4050
|
+
stdout(`cleargate CLI: local dist \u2014 ${distCliPath}`);
|
|
4051
|
+
return;
|
|
4052
|
+
}
|
|
4053
|
+
const whichResult = spawnSync6("command", ["-v", "cleargate"], {
|
|
4054
|
+
shell: true,
|
|
4055
|
+
encoding: "utf8",
|
|
4056
|
+
timeout: 3e3
|
|
4057
|
+
});
|
|
4058
|
+
if (whichResult.status === 0) {
|
|
4059
|
+
stdout("cleargate CLI: PATH (global install) \u2014 cleargate");
|
|
4060
|
+
return;
|
|
4061
|
+
}
|
|
4062
|
+
let pinVersion = "unknown";
|
|
4063
|
+
const hookPath = path21.join(cwd, ".claude", "hooks", "stamp-and-gate.sh");
|
|
4064
|
+
if (fs20.existsSync(hookPath)) {
|
|
4065
|
+
try {
|
|
4066
|
+
const hookContent = fs20.readFileSync(hookPath, "utf-8");
|
|
4067
|
+
const pinMatch = hookContent.match(/^#\s*cleargate-pin:\s*(\S+)\s*$/m);
|
|
4068
|
+
if (pinMatch?.[1]) {
|
|
4069
|
+
pinVersion = pinMatch[1];
|
|
4070
|
+
} else {
|
|
4071
|
+
const npxMatch = hookContent.match(/@cleargate\/cli@([^\s"']+)/);
|
|
4072
|
+
if (npxMatch?.[1]) pinVersion = npxMatch[1];
|
|
4073
|
+
}
|
|
4074
|
+
} catch {
|
|
4075
|
+
}
|
|
4076
|
+
}
|
|
4077
|
+
if (pinVersion === "unknown") {
|
|
4078
|
+
stdout("cleargate CLI: \u{1F534} not resolvable \u2014 hooks will no-op. Fix: npm i -g cleargate or npx cleargate doctor");
|
|
4079
|
+
} else {
|
|
4080
|
+
stdout(`cleargate CLI: npx @cleargate/cli@${pinVersion} (cold-start ~600ms first call)`);
|
|
4081
|
+
}
|
|
4082
|
+
}
|
|
4083
|
+
var PLANNING_FIRST_REMINDER = `Triage first, draft second:
|
|
4084
|
+
Before any Edit/Write that creates user-facing code, you must:
|
|
4085
|
+
(1) classify the request (Epic / Story / CR / Bug),
|
|
4086
|
+
(2) draft a work item under .cleargate/delivery/pending-sync/ from .cleargate/templates/,
|
|
4087
|
+
(3) halt at Gate 1 (Proposal approval) for human sign-off.
|
|
4088
|
+
Bypass this only if the user has explicitly waived planning in this conversation.`;
|
|
4089
|
+
async function runSessionStart(cwd, stdout, outcome) {
|
|
4090
|
+
const resolverLines = [];
|
|
4091
|
+
emitResolverStatusLine(cwd, (line) => {
|
|
4092
|
+
stdout(line);
|
|
4093
|
+
resolverLines.push(line);
|
|
4094
|
+
});
|
|
4095
|
+
if (outcome && resolverLines.some((l) => l.includes("\u{1F534}"))) {
|
|
4096
|
+
outcome.configError = true;
|
|
4097
|
+
}
|
|
3952
4098
|
const pendingSyncDir = path21.join(cwd, ".cleargate", "delivery", "pending-sync");
|
|
3953
4099
|
let files;
|
|
3954
4100
|
try {
|
|
@@ -3957,6 +4103,7 @@ async function runSessionStart(cwd, stdout) {
|
|
|
3957
4103
|
return;
|
|
3958
4104
|
}
|
|
3959
4105
|
const blocked = [];
|
|
4106
|
+
let hasApprovedStory = false;
|
|
3960
4107
|
for (const filePath of files) {
|
|
3961
4108
|
let raw;
|
|
3962
4109
|
try {
|
|
@@ -3971,6 +4118,9 @@ async function runSessionStart(cwd, stdout) {
|
|
|
3971
4118
|
} catch {
|
|
3972
4119
|
continue;
|
|
3973
4120
|
}
|
|
4121
|
+
if (fm["approved"] === true) {
|
|
4122
|
+
hasApprovedStory = true;
|
|
4123
|
+
}
|
|
3974
4124
|
const gate2 = parseCachedGateResult2(fm["cached_gate_result"]);
|
|
3975
4125
|
if (!gate2 || gate2.pass !== false) continue;
|
|
3976
4126
|
const idKeys = ["story_id", "epic_id", "proposal_id", "cr_id", "bug_id", "sprint_id"];
|
|
@@ -3988,9 +4138,19 @@ async function runSessionStart(cwd, stdout) {
|
|
|
3988
4138
|
const firstCriterionId = gate2.failing_criteria.length > 0 ? gate2.failing_criteria[0]?.id ?? "" : "";
|
|
3989
4139
|
blocked.push({ id: itemId, firstCriterionId });
|
|
3990
4140
|
}
|
|
4141
|
+
const activesentinel = path21.join(cwd, ".cleargate", "sprint-runs", ".active");
|
|
4142
|
+
const sprintActive = fs20.existsSync(activesentinel);
|
|
4143
|
+
const shouldRemind = !hasApprovedStory && !sprintActive;
|
|
4144
|
+
if (shouldRemind) {
|
|
4145
|
+
stdout(PLANNING_FIRST_REMINDER);
|
|
4146
|
+
if (blocked.length > 0) {
|
|
4147
|
+
stdout("");
|
|
4148
|
+
}
|
|
4149
|
+
}
|
|
3991
4150
|
if (blocked.length === 0) {
|
|
3992
4151
|
return;
|
|
3993
4152
|
}
|
|
4153
|
+
if (outcome) outcome.blocker = true;
|
|
3994
4154
|
const overflow = blocked.length > SESSION_START_MAX_ITEMS ? blocked.length - SESSION_START_MAX_ITEMS : 0;
|
|
3995
4155
|
const visible = blocked.slice(0, SESSION_START_MAX_ITEMS);
|
|
3996
4156
|
const lines = [`${blocked.length} items blocked:`];
|
|
@@ -4007,10 +4167,11 @@ async function runSessionStart(cwd, stdout) {
|
|
|
4007
4167
|
}
|
|
4008
4168
|
stdout(output);
|
|
4009
4169
|
}
|
|
4010
|
-
async function runPricing(filePath, cwd, stdout, stderr, exit) {
|
|
4170
|
+
async function runPricing(filePath, cwd, stdout, stderr, exit, outcome) {
|
|
4011
4171
|
if (!filePath) {
|
|
4012
4172
|
stderr("cleargate doctor --pricing: missing <file> argument");
|
|
4013
|
-
|
|
4173
|
+
if (outcome) outcome.configError = true;
|
|
4174
|
+
exit(2);
|
|
4014
4175
|
return;
|
|
4015
4176
|
}
|
|
4016
4177
|
const absPath = path21.isAbsolute(filePath) ? filePath : path21.resolve(cwd, filePath);
|
|
@@ -4019,12 +4180,14 @@ async function runPricing(filePath, cwd, stdout, stderr, exit) {
|
|
|
4019
4180
|
raw = fs20.readFileSync(absPath, "utf-8");
|
|
4020
4181
|
} catch {
|
|
4021
4182
|
stderr(`cleargate doctor --pricing: cannot read file: ${absPath}`);
|
|
4022
|
-
|
|
4183
|
+
if (outcome) outcome.configError = true;
|
|
4184
|
+
exit(2);
|
|
4023
4185
|
return;
|
|
4024
4186
|
}
|
|
4025
4187
|
if (!raw.trimStart().startsWith("---")) {
|
|
4026
4188
|
stderr(`cleargate doctor --pricing: file has no frontmatter: ${absPath}`);
|
|
4027
|
-
|
|
4189
|
+
if (outcome) outcome.configError = true;
|
|
4190
|
+
exit(2);
|
|
4028
4191
|
return;
|
|
4029
4192
|
}
|
|
4030
4193
|
let fm;
|
|
@@ -4032,12 +4195,14 @@ async function runPricing(filePath, cwd, stdout, stderr, exit) {
|
|
|
4032
4195
|
fm = parseFrontmatter(raw).fm;
|
|
4033
4196
|
} catch {
|
|
4034
4197
|
stderr(`cleargate doctor --pricing: cannot parse frontmatter in: ${absPath}`);
|
|
4035
|
-
|
|
4198
|
+
if (outcome) outcome.configError = true;
|
|
4199
|
+
exit(2);
|
|
4036
4200
|
return;
|
|
4037
4201
|
}
|
|
4038
4202
|
const draftTokensRaw = fm["draft_tokens"];
|
|
4039
4203
|
if (!draftTokensRaw) {
|
|
4040
4204
|
stdout("draft_tokens unpopulated \u2014 run cleargate stamp-tokens first");
|
|
4205
|
+
if (outcome) outcome.blocker = true;
|
|
4041
4206
|
exit(1);
|
|
4042
4207
|
return;
|
|
4043
4208
|
}
|
|
@@ -4049,16 +4214,19 @@ async function runPricing(filePath, cwd, stdout, stderr, exit) {
|
|
|
4049
4214
|
draftTokens = JSON.parse(draftTokensRaw);
|
|
4050
4215
|
} catch {
|
|
4051
4216
|
stdout("draft_tokens unpopulated \u2014 run cleargate stamp-tokens first");
|
|
4217
|
+
if (outcome) outcome.blocker = true;
|
|
4052
4218
|
exit(1);
|
|
4053
4219
|
return;
|
|
4054
4220
|
}
|
|
4055
4221
|
} else {
|
|
4056
4222
|
stdout("draft_tokens unpopulated \u2014 run cleargate stamp-tokens first");
|
|
4223
|
+
if (outcome) outcome.blocker = true;
|
|
4057
4224
|
exit(1);
|
|
4058
4225
|
return;
|
|
4059
4226
|
}
|
|
4060
4227
|
if (draftTokens.input === null && draftTokens.output === null && draftTokens.cache_read === null && draftTokens.cache_creation === null) {
|
|
4061
4228
|
stdout("draft_tokens unpopulated \u2014 run cleargate stamp-tokens first");
|
|
4229
|
+
if (outcome) outcome.blocker = true;
|
|
4062
4230
|
exit(1);
|
|
4063
4231
|
return;
|
|
4064
4232
|
}
|
|
@@ -4076,18 +4244,95 @@ async function runPricing(filePath, cwd, stdout, stderr, exit) {
|
|
|
4076
4244
|
`${fileName}: ${model} \u2014 input:${input} output:${output} cache_read:${cacheRead} cache_creation:${cacheCreation} \u2248 $${usd.toFixed(4)}`
|
|
4077
4245
|
);
|
|
4078
4246
|
}
|
|
4247
|
+
function globMatch(pattern, filePath) {
|
|
4248
|
+
const normalPattern = pattern.replace(/\\/g, "/");
|
|
4249
|
+
const normalFile = filePath.replace(/\\/g, "/");
|
|
4250
|
+
const regexStr = normalPattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, "\0").replace(/\*/g, "[^/]*").replace(//g, ".*");
|
|
4251
|
+
const re = new RegExp(`^${regexStr}$`);
|
|
4252
|
+
return re.test(normalFile);
|
|
4253
|
+
}
|
|
4254
|
+
async function runCanEdit(filePath, cwd, stdout, exit, outcome) {
|
|
4255
|
+
const activeSentinel = path21.join(cwd, ".cleargate", "sprint-runs", ".active");
|
|
4256
|
+
if (fs20.existsSync(activeSentinel)) {
|
|
4257
|
+
stdout("allowed: sprint active");
|
|
4258
|
+
return;
|
|
4259
|
+
}
|
|
4260
|
+
const pendingSyncDir = path21.join(cwd, ".cleargate", "delivery", "pending-sync");
|
|
4261
|
+
let files;
|
|
4262
|
+
try {
|
|
4263
|
+
files = fs20.readdirSync(pendingSyncDir).filter((f) => f.endsWith(".md")).map((f) => path21.join(pendingSyncDir, f));
|
|
4264
|
+
} catch {
|
|
4265
|
+
stdout("blocked: no_approved_stories");
|
|
4266
|
+
if (outcome) outcome.blocker = true;
|
|
4267
|
+
exit(1);
|
|
4268
|
+
return;
|
|
4269
|
+
}
|
|
4270
|
+
let hasApprovedStory = false;
|
|
4271
|
+
let coveredByStory = false;
|
|
4272
|
+
for (const storyPath of files) {
|
|
4273
|
+
let raw;
|
|
4274
|
+
try {
|
|
4275
|
+
raw = fs20.readFileSync(storyPath, "utf-8");
|
|
4276
|
+
} catch {
|
|
4277
|
+
continue;
|
|
4278
|
+
}
|
|
4279
|
+
if (!raw.trimStart().startsWith("---")) continue;
|
|
4280
|
+
let fm;
|
|
4281
|
+
try {
|
|
4282
|
+
fm = parseFrontmatter(raw).fm;
|
|
4283
|
+
} catch {
|
|
4284
|
+
continue;
|
|
4285
|
+
}
|
|
4286
|
+
if (fm["approved"] !== true) continue;
|
|
4287
|
+
hasApprovedStory = true;
|
|
4288
|
+
const implFilesRaw = fm["implementation_files"];
|
|
4289
|
+
if (implFilesRaw === void 0 || implFilesRaw === null) {
|
|
4290
|
+
coveredByStory = true;
|
|
4291
|
+
break;
|
|
4292
|
+
}
|
|
4293
|
+
if (Array.isArray(implFilesRaw)) {
|
|
4294
|
+
for (const pattern of implFilesRaw) {
|
|
4295
|
+
if (typeof pattern !== "string") continue;
|
|
4296
|
+
if (globMatch(pattern, filePath)) {
|
|
4297
|
+
coveredByStory = true;
|
|
4298
|
+
break;
|
|
4299
|
+
}
|
|
4300
|
+
}
|
|
4301
|
+
}
|
|
4302
|
+
if (coveredByStory) break;
|
|
4303
|
+
}
|
|
4304
|
+
if (!hasApprovedStory) {
|
|
4305
|
+
stdout("blocked: no_approved_stories");
|
|
4306
|
+
if (outcome) outcome.blocker = true;
|
|
4307
|
+
exit(1);
|
|
4308
|
+
return;
|
|
4309
|
+
}
|
|
4310
|
+
if (!coveredByStory) {
|
|
4311
|
+
stdout("blocked: file_not_in_implementation_files");
|
|
4312
|
+
if (outcome) outcome.blocker = true;
|
|
4313
|
+
exit(1);
|
|
4314
|
+
return;
|
|
4315
|
+
}
|
|
4316
|
+
stdout("allowed");
|
|
4317
|
+
}
|
|
4079
4318
|
async function doctorHandler(flags, cli) {
|
|
4080
4319
|
const cwd = cli?.cwd ?? process.cwd();
|
|
4081
4320
|
const now = cli?.now ? cli.now() : /* @__PURE__ */ new Date();
|
|
4082
4321
|
const stdout = cli?.stdout ?? ((s) => process.stdout.write(s + "\n"));
|
|
4083
4322
|
const stderr = cli?.stderr ?? ((s) => process.stderr.write(s + "\n"));
|
|
4084
4323
|
const exit = cli?.exit ?? ((code) => process.exit(code));
|
|
4324
|
+
const outcome = { configError: false, blocker: false };
|
|
4325
|
+
let exitedEarly = false;
|
|
4326
|
+
const wrappedExit = (code) => {
|
|
4327
|
+
exitedEarly = true;
|
|
4328
|
+
return exit(code);
|
|
4329
|
+
};
|
|
4085
4330
|
let mode;
|
|
4086
4331
|
try {
|
|
4087
4332
|
mode = selectMode(flags);
|
|
4088
4333
|
} catch (err) {
|
|
4089
4334
|
stderr(err.message);
|
|
4090
|
-
exit(
|
|
4335
|
+
exit(2);
|
|
4091
4336
|
return;
|
|
4092
4337
|
}
|
|
4093
4338
|
switch (mode) {
|
|
@@ -4095,26 +4340,38 @@ async function doctorHandler(flags, cli) {
|
|
|
4095
4340
|
await runCheckScaffold(flags, cli ?? {}, cwd, now, stdout, stderr);
|
|
4096
4341
|
break;
|
|
4097
4342
|
case "hook-health":
|
|
4098
|
-
runHookHealth(stdout, cwd, now);
|
|
4343
|
+
runHookHealth(stdout, cwd, now, outcome);
|
|
4099
4344
|
break;
|
|
4100
4345
|
case "session-start":
|
|
4101
|
-
await runSessionStart(cwd, stdout);
|
|
4346
|
+
await runSessionStart(cwd, stdout, outcome);
|
|
4102
4347
|
break;
|
|
4103
4348
|
case "pricing":
|
|
4104
|
-
await runPricing(flags.pricingFile ?? "", cwd, stdout, stderr,
|
|
4349
|
+
await runPricing(flags.pricingFile ?? "", cwd, stdout, stderr, wrappedExit, outcome);
|
|
4350
|
+
break;
|
|
4351
|
+
case "can-edit":
|
|
4352
|
+
await runCanEdit(flags.canEditFile ?? "", cwd, stdout, wrappedExit, outcome);
|
|
4105
4353
|
break;
|
|
4106
4354
|
default: {
|
|
4107
4355
|
const exhaustiveCheck = mode;
|
|
4108
4356
|
stderr(`cleargate doctor: unknown mode '${String(exhaustiveCheck)}'`);
|
|
4109
|
-
exit(
|
|
4357
|
+
exit(2);
|
|
4358
|
+
return;
|
|
4110
4359
|
}
|
|
4111
4360
|
}
|
|
4361
|
+
if (exitedEarly) return;
|
|
4362
|
+
if (outcome.configError) {
|
|
4363
|
+
exit(2);
|
|
4364
|
+
} else if (outcome.blocker) {
|
|
4365
|
+
exit(1);
|
|
4366
|
+
} else {
|
|
4367
|
+
exit(0);
|
|
4368
|
+
}
|
|
4112
4369
|
}
|
|
4113
4370
|
|
|
4114
4371
|
// src/commands/gate.ts
|
|
4115
4372
|
import * as fs24 from "fs";
|
|
4116
4373
|
import * as path24 from "path";
|
|
4117
|
-
import { spawnSync as
|
|
4374
|
+
import { spawnSync as spawnSync7 } from "child_process";
|
|
4118
4375
|
|
|
4119
4376
|
// src/commands/execution-mode.ts
|
|
4120
4377
|
import * as fs21 from "fs";
|
|
@@ -4221,6 +4478,14 @@ function parsePredicate(src) {
|
|
|
4221
4478
|
const value = parseValue(rawVal);
|
|
4222
4479
|
return { kind: "frontmatter", ref, field, op, value };
|
|
4223
4480
|
}
|
|
4481
|
+
const markerNotMatch = s.match(/^body does not contain marker ['"]([A-Z]+)['"]$/);
|
|
4482
|
+
if (markerNotMatch) {
|
|
4483
|
+
const marker = markerNotMatch[1];
|
|
4484
|
+
if (marker !== "TBD" && marker !== "TODO" && marker !== "FIXME") {
|
|
4485
|
+
throw new Error(`unsupported predicate shape: ${src}`);
|
|
4486
|
+
}
|
|
4487
|
+
return { kind: "marker-absence", marker };
|
|
4488
|
+
}
|
|
4224
4489
|
const bodyNotMatch = s.match(/^body does not contain ['"](.+)['"]$/);
|
|
4225
4490
|
if (bodyNotMatch) {
|
|
4226
4491
|
return { kind: "body-contains", needle: bodyNotMatch[1], negated: true };
|
|
@@ -4276,6 +4541,8 @@ function evaluate(predicate, doc, opts) {
|
|
|
4276
4541
|
return evalFrontmatter(parsed, doc, projectRoot);
|
|
4277
4542
|
case "body-contains":
|
|
4278
4543
|
return evalBodyContains(parsed, doc);
|
|
4544
|
+
case "marker-absence":
|
|
4545
|
+
return evalMarkerAbsence(parsed, doc);
|
|
4279
4546
|
case "section":
|
|
4280
4547
|
return evalSection(parsed, doc);
|
|
4281
4548
|
case "file-exists":
|
|
@@ -4298,6 +4565,26 @@ function evalFrontmatter(parsed, doc, projectRoot) {
|
|
|
4298
4565
|
detail: `frontmatter key '${parsed.ref}' is missing or null in ${doc.absPath}`
|
|
4299
4566
|
};
|
|
4300
4567
|
}
|
|
4568
|
+
const refStr = String(refVal);
|
|
4569
|
+
const looksLikeProse = refStr.length > 200 || /[ —–:()\n]/.test(refStr);
|
|
4570
|
+
if (looksLikeProse) {
|
|
4571
|
+
const waiver = doc.fm["proposal_gate_waiver"];
|
|
4572
|
+
const hasExplicitWaiver = waiver !== null && waiver !== void 0 && waiver !== false && String(waiver).trim() !== "" && String(waiver).trim() !== "false";
|
|
4573
|
+
const approvedBy = doc.fm["approved_by"];
|
|
4574
|
+
const approvedAt = doc.fm["approved_at"];
|
|
4575
|
+
const hasApprovalFields = approvedBy !== null && approvedBy !== void 0 && String(approvedBy).trim() !== "" && approvedAt !== null && approvedAt !== void 0 && String(approvedAt).trim() !== "";
|
|
4576
|
+
const hasWaiver = hasExplicitWaiver || hasApprovalFields;
|
|
4577
|
+
if (hasWaiver) {
|
|
4578
|
+
return {
|
|
4579
|
+
pass: true,
|
|
4580
|
+
detail: `context_source is prose; proposal-gate waiver per frontmatter approved_by/approved_at`
|
|
4581
|
+
};
|
|
4582
|
+
}
|
|
4583
|
+
return {
|
|
4584
|
+
pass: false,
|
|
4585
|
+
detail: `context_source is prose but no proposal_gate_waiver (approved_by + approved_at) found in frontmatter`
|
|
4586
|
+
};
|
|
4587
|
+
}
|
|
4301
4588
|
const linkedPath = resolveLinkedPath(String(refVal), doc.absPath, projectRoot);
|
|
4302
4589
|
if (!linkedPath) {
|
|
4303
4590
|
return {
|
|
@@ -4422,6 +4709,38 @@ function evalBodyContains(parsed, doc) {
|
|
|
4422
4709
|
return { pass: false, detail: `'${needle}' not found in body` };
|
|
4423
4710
|
}
|
|
4424
4711
|
}
|
|
4712
|
+
function evalMarkerAbsence(parsed, doc) {
|
|
4713
|
+
const { marker } = parsed;
|
|
4714
|
+
const lines = doc.body.split("\n");
|
|
4715
|
+
const templateSelfRefRe = /^\s*-\s*\[[x ]\]\s*0\s*"TBDs?"\s*exist/i;
|
|
4716
|
+
const markerRe = new RegExp(
|
|
4717
|
+
`(?:^|(?<=\\())${marker}(?=:)|\\(${marker}\\)|\\[${marker}\\]|(?<=//\\s*)${marker}(?!\\w)|(?<=#\\s*)${marker}(?!\\w)`,
|
|
4718
|
+
// # MARKER (comment)
|
|
4719
|
+
"g"
|
|
4720
|
+
);
|
|
4721
|
+
const bareLineRe = new RegExp(`^${marker}$`);
|
|
4722
|
+
const violations = [];
|
|
4723
|
+
for (let i = 0; i < lines.length; i++) {
|
|
4724
|
+
const line = lines[i];
|
|
4725
|
+
if (templateSelfRefRe.test(line)) continue;
|
|
4726
|
+
const trimmed = line.trim();
|
|
4727
|
+
if (bareLineRe.test(trimmed)) {
|
|
4728
|
+
violations.push(i + 1);
|
|
4729
|
+
continue;
|
|
4730
|
+
}
|
|
4731
|
+
markerRe.lastIndex = 0;
|
|
4732
|
+
if (markerRe.test(line)) {
|
|
4733
|
+
violations.push(i + 1);
|
|
4734
|
+
}
|
|
4735
|
+
}
|
|
4736
|
+
if (violations.length > 0) {
|
|
4737
|
+
return {
|
|
4738
|
+
pass: false,
|
|
4739
|
+
detail: `${violations.length} marker occurrence${violations.length === 1 ? "" : "s"} of '${marker}' at line${violations.length === 1 ? "" : "s"} ${violations.join(", ")}`
|
|
4740
|
+
};
|
|
4741
|
+
}
|
|
4742
|
+
return { pass: true, detail: `no '${marker}' markers found in body` };
|
|
4743
|
+
}
|
|
4425
4744
|
function evalSection(parsed, doc) {
|
|
4426
4745
|
const body = doc.body;
|
|
4427
4746
|
const rawParts = body.split(/^(?=## )/m);
|
|
@@ -4789,7 +5108,7 @@ function gateQaHandler(opts, cli) {
|
|
|
4789
5108
|
const stdoutFn = cli?.stdout ?? ((s) => process.stdout.write(s + "\n"));
|
|
4790
5109
|
const stderrFn = cli?.stderr ?? ((s) => process.stderr.write(s + "\n"));
|
|
4791
5110
|
const exitFn = cli?.exit ?? ((code2) => process.exit(code2));
|
|
4792
|
-
const spawnFn = cli?.spawnFn ??
|
|
5111
|
+
const spawnFn = cli?.spawnFn ?? spawnSync7;
|
|
4793
5112
|
const sprintId = cli?.sprintId ?? "SPRINT-UNKNOWN";
|
|
4794
5113
|
const mode = readSprintExecutionMode(sprintId, {
|
|
4795
5114
|
sprintFilePath: cli?.sprintFilePath,
|
|
@@ -4815,7 +5134,7 @@ function gateArchHandler(opts, cli) {
|
|
|
4815
5134
|
const stdoutFn = cli?.stdout ?? ((s) => process.stdout.write(s + "\n"));
|
|
4816
5135
|
const stderrFn = cli?.stderr ?? ((s) => process.stderr.write(s + "\n"));
|
|
4817
5136
|
const exitFn = cli?.exit ?? ((code2) => process.exit(code2));
|
|
4818
|
-
const spawnFn = cli?.spawnFn ??
|
|
5137
|
+
const spawnFn = cli?.spawnFn ?? spawnSync7;
|
|
4819
5138
|
const sprintId = cli?.sprintId ?? "SPRINT-UNKNOWN";
|
|
4820
5139
|
const mode = readSprintExecutionMode(sprintId, {
|
|
4821
5140
|
sprintFilePath: cli?.sprintFilePath,
|
|
@@ -4839,13 +5158,13 @@ function gateArchHandler(opts, cli) {
|
|
|
4839
5158
|
}
|
|
4840
5159
|
|
|
4841
5160
|
// src/commands/gate-run.ts
|
|
4842
|
-
import { spawnSync as
|
|
5161
|
+
import { spawnSync as spawnSync8 } from "child_process";
|
|
4843
5162
|
var KNOWN_GATES = ["precommit", "test", "typecheck", "lint"];
|
|
4844
5163
|
function gateRunHandler(name, opts, cli) {
|
|
4845
5164
|
const stdoutFn = cli?.stdout ?? ((s) => process.stdout.write(s + "\n"));
|
|
4846
5165
|
const stderrFn = cli?.stderr ?? ((s) => process.stderr.write(s + "\n"));
|
|
4847
5166
|
const exitFn = cli?.exit ?? ((code) => process.exit(code));
|
|
4848
|
-
const spawnFn = cli?.spawnFn ??
|
|
5167
|
+
const spawnFn = cli?.spawnFn ?? spawnSync8;
|
|
4849
5168
|
const cwd = cli?.cwd ?? process.cwd();
|
|
4850
5169
|
const configLoaderFn = cli?.configLoader ?? loadWikiConfig;
|
|
4851
5170
|
if (!KNOWN_GATES.includes(name)) {
|
|
@@ -4877,7 +5196,7 @@ function gateRunHandler(name, opts, cli) {
|
|
|
4877
5196
|
// src/commands/sprint.ts
|
|
4878
5197
|
import * as fs25 from "fs";
|
|
4879
5198
|
import * as path25 from "path";
|
|
4880
|
-
import { spawnSync as
|
|
5199
|
+
import { spawnSync as spawnSync9 } from "child_process";
|
|
4881
5200
|
import yaml7 from "js-yaml";
|
|
4882
5201
|
var TERMINAL_STATUSES2 = /* @__PURE__ */ new Set(["Completed", "Done", "Abandoned", "Closed", "Resolved"]);
|
|
4883
5202
|
function resolveRunScript(opts) {
|
|
@@ -4892,7 +5211,7 @@ function sprintInitHandler(opts, cli) {
|
|
|
4892
5211
|
const stdoutFn = cli?.stdout ?? ((s) => process.stdout.write(s + "\n"));
|
|
4893
5212
|
const stderrFn = cli?.stderr ?? ((s) => process.stderr.write(s + "\n"));
|
|
4894
5213
|
const exitFn = cli?.exit ?? defaultExit;
|
|
4895
|
-
const spawnFn = cli?.spawnFn ??
|
|
5214
|
+
const spawnFn = cli?.spawnFn ?? spawnSync9;
|
|
4896
5215
|
const mode = readSprintExecutionMode(opts.sprintId, {
|
|
4897
5216
|
sprintFilePath: cli?.sprintFilePath,
|
|
4898
5217
|
cwd: cli?.cwd
|
|
@@ -4914,7 +5233,7 @@ function sprintCloseHandler(opts, cli) {
|
|
|
4914
5233
|
const stdoutFn = cli?.stdout ?? ((s) => process.stdout.write(s + "\n"));
|
|
4915
5234
|
const stderrFn = cli?.stderr ?? ((s) => process.stderr.write(s + "\n"));
|
|
4916
5235
|
const exitFn = cli?.exit ?? defaultExit;
|
|
4917
|
-
const spawnFn = cli?.spawnFn ??
|
|
5236
|
+
const spawnFn = cli?.spawnFn ?? spawnSync9;
|
|
4918
5237
|
const mode = readSprintExecutionMode(opts.sprintId, {
|
|
4919
5238
|
sprintFilePath: cli?.sprintFilePath,
|
|
4920
5239
|
cwd: cli?.cwd
|
|
@@ -5019,7 +5338,7 @@ async function sprintArchiveHandler(opts, cli) {
|
|
|
5019
5338
|
const stdoutFn = cli?.stdout ?? ((s) => process.stdout.write(s + "\n"));
|
|
5020
5339
|
const stderrFn = cli?.stderr ?? ((s) => process.stderr.write(s + "\n"));
|
|
5021
5340
|
const exitFn = cli?.exit ?? defaultExit;
|
|
5022
|
-
const spawnFn = cli?.spawnFn ??
|
|
5341
|
+
const spawnFn = cli?.spawnFn ?? spawnSync9;
|
|
5023
5342
|
const cwd = cli?.cwd ?? process.cwd();
|
|
5024
5343
|
const wikiBuildFn = cli?.wikiBuildFn ?? (async (wCwd, wStdout) => {
|
|
5025
5344
|
const fakeExit = (code) => {
|
|
@@ -5243,7 +5562,7 @@ async function sprintArchiveHandler(opts, cli) {
|
|
|
5243
5562
|
// src/commands/story.ts
|
|
5244
5563
|
import * as fs26 from "fs";
|
|
5245
5564
|
import * as path26 from "path";
|
|
5246
|
-
import { spawnSync as
|
|
5565
|
+
import { spawnSync as spawnSync10 } from "child_process";
|
|
5247
5566
|
function defaultExit2(code) {
|
|
5248
5567
|
return process.exit(code);
|
|
5249
5568
|
}
|
|
@@ -5269,7 +5588,7 @@ function storyStartHandler(opts, cli) {
|
|
|
5269
5588
|
const stdoutFn = cli?.stdout ?? ((s) => process.stdout.write(s + "\n"));
|
|
5270
5589
|
const stderrFn = cli?.stderr ?? ((s) => process.stderr.write(s + "\n"));
|
|
5271
5590
|
const exitFn = cli?.exit ?? defaultExit2;
|
|
5272
|
-
const spawnFn = cli?.spawnFn ??
|
|
5591
|
+
const spawnFn = cli?.spawnFn ?? spawnSync10;
|
|
5273
5592
|
const cwd = cli?.cwd ?? process.cwd();
|
|
5274
5593
|
const sprintId = cli?.sprintId ?? resolveSprintIdFromSentinel(cwd) ?? "SPRINT-UNKNOWN";
|
|
5275
5594
|
const mode = readSprintExecutionMode(sprintId, {
|
|
@@ -5347,7 +5666,7 @@ function storyCompleteHandler(opts, cli) {
|
|
|
5347
5666
|
const stdoutFn = cli?.stdout ?? ((s) => process.stdout.write(s + "\n"));
|
|
5348
5667
|
const stderrFn = cli?.stderr ?? ((s) => process.stderr.write(s + "\n"));
|
|
5349
5668
|
const exitFn = cli?.exit ?? defaultExit2;
|
|
5350
|
-
const spawnFn = cli?.spawnFn ??
|
|
5669
|
+
const spawnFn = cli?.spawnFn ?? spawnSync10;
|
|
5351
5670
|
const cwd = cli?.cwd ?? process.cwd();
|
|
5352
5671
|
const sprintId = cli?.sprintId ?? resolveSprintIdFromSentinel(cwd) ?? "SPRINT-UNKNOWN";
|
|
5353
5672
|
const mode = readSprintExecutionMode(sprintId, {
|
|
@@ -5457,7 +5776,7 @@ function storyCompleteHandler(opts, cli) {
|
|
|
5457
5776
|
|
|
5458
5777
|
// src/commands/state.ts
|
|
5459
5778
|
import * as path27 from "path";
|
|
5460
|
-
import { spawnSync as
|
|
5779
|
+
import { spawnSync as spawnSync11 } from "child_process";
|
|
5461
5780
|
function defaultExit3(code) {
|
|
5462
5781
|
return process.exit(code);
|
|
5463
5782
|
}
|
|
@@ -5470,7 +5789,7 @@ function stateUpdateHandler(opts, cli) {
|
|
|
5470
5789
|
const stdoutFn = cli?.stdout ?? ((s) => process.stdout.write(s + "\n"));
|
|
5471
5790
|
const stderrFn = cli?.stderr ?? ((s) => process.stderr.write(s + "\n"));
|
|
5472
5791
|
const exitFn = cli?.exit ?? defaultExit3;
|
|
5473
|
-
const spawnFn = cli?.spawnFn ??
|
|
5792
|
+
const spawnFn = cli?.spawnFn ?? spawnSync11;
|
|
5474
5793
|
const cwd = cli?.cwd;
|
|
5475
5794
|
const sprintId = cli?.sprintId ?? resolveSprintIdFromSentinel(cwd) ?? "SPRINT-UNKNOWN";
|
|
5476
5795
|
const mode = readSprintExecutionMode(sprintId, {
|
|
@@ -5497,7 +5816,7 @@ function stateValidateHandler(opts, cli) {
|
|
|
5497
5816
|
const stdoutFn = cli?.stdout ?? ((s) => process.stdout.write(s + "\n"));
|
|
5498
5817
|
const stderrFn = cli?.stderr ?? ((s) => process.stderr.write(s + "\n"));
|
|
5499
5818
|
const exitFn = cli?.exit ?? defaultExit3;
|
|
5500
|
-
const spawnFn = cli?.spawnFn ??
|
|
5819
|
+
const spawnFn = cli?.spawnFn ?? spawnSync11;
|
|
5501
5820
|
const mode = readSprintExecutionMode(opts.sprintId, {
|
|
5502
5821
|
sprintFilePath: cli?.sprintFilePath,
|
|
5503
5822
|
cwd: cli?.cwd
|
|
@@ -8471,6 +8790,108 @@ async function adminLoginHandler(opts = {}) {
|
|
|
8471
8790
|
stdout(`Credentials saved to ${authFilePath} (chmod 600).`);
|
|
8472
8791
|
}
|
|
8473
8792
|
|
|
8793
|
+
// src/commands/hotfix.ts
|
|
8794
|
+
import * as fs34 from "fs";
|
|
8795
|
+
import * as path44 from "path";
|
|
8796
|
+
function defaultExit4(code) {
|
|
8797
|
+
return process.exit(code);
|
|
8798
|
+
}
|
|
8799
|
+
var SLUG_RE = /^[a-z0-9-]+$/;
|
|
8800
|
+
var HOTFIX_FILE_RE = /^HOTFIX-(\d+)_.*\.md$/;
|
|
8801
|
+
function maxHotfixId(pendingDir) {
|
|
8802
|
+
let max = 0;
|
|
8803
|
+
let entries;
|
|
8804
|
+
try {
|
|
8805
|
+
entries = fs34.readdirSync(pendingDir);
|
|
8806
|
+
} catch {
|
|
8807
|
+
return 0;
|
|
8808
|
+
}
|
|
8809
|
+
for (const entry of entries) {
|
|
8810
|
+
const m = HOTFIX_FILE_RE.exec(entry);
|
|
8811
|
+
if (m) {
|
|
8812
|
+
const n = parseInt(m[1], 10);
|
|
8813
|
+
if (n > max) max = n;
|
|
8814
|
+
}
|
|
8815
|
+
}
|
|
8816
|
+
return max;
|
|
8817
|
+
}
|
|
8818
|
+
function countActiveHotfixes(repoRoot) {
|
|
8819
|
+
const pendingDir = path44.join(repoRoot, ".cleargate", "delivery", "pending-sync");
|
|
8820
|
+
const archiveDir = path44.join(repoRoot, ".cleargate", "delivery", "archive");
|
|
8821
|
+
const sevenDaysAgo = Date.now() - 7 * 24 * 60 * 60 * 1e3;
|
|
8822
|
+
let count = 0;
|
|
8823
|
+
let pendingEntries = [];
|
|
8824
|
+
try {
|
|
8825
|
+
pendingEntries = fs34.readdirSync(pendingDir);
|
|
8826
|
+
} catch {
|
|
8827
|
+
}
|
|
8828
|
+
for (const entry of pendingEntries) {
|
|
8829
|
+
if (entry.startsWith("HOTFIX-") && entry.endsWith(".md")) count++;
|
|
8830
|
+
}
|
|
8831
|
+
let archiveEntries = [];
|
|
8832
|
+
try {
|
|
8833
|
+
archiveEntries = fs34.readdirSync(archiveDir);
|
|
8834
|
+
} catch {
|
|
8835
|
+
}
|
|
8836
|
+
for (const entry of archiveEntries) {
|
|
8837
|
+
if (entry.startsWith("HOTFIX-") && entry.endsWith(".md")) {
|
|
8838
|
+
try {
|
|
8839
|
+
const stat = fs34.statSync(path44.join(archiveDir, entry));
|
|
8840
|
+
if (stat.mtimeMs >= sevenDaysAgo) count++;
|
|
8841
|
+
} catch {
|
|
8842
|
+
}
|
|
8843
|
+
}
|
|
8844
|
+
}
|
|
8845
|
+
return count;
|
|
8846
|
+
}
|
|
8847
|
+
function resolveTemplatePath(repoRoot) {
|
|
8848
|
+
return path44.join(repoRoot, ".cleargate", "templates", "hotfix.md");
|
|
8849
|
+
}
|
|
8850
|
+
function hotfixNewHandler(opts, cli) {
|
|
8851
|
+
const stdoutFn = cli?.stdout ?? ((s) => process.stdout.write(s + "\n"));
|
|
8852
|
+
const stderrFn = cli?.stderr ?? ((s) => process.stderr.write(s + "\n"));
|
|
8853
|
+
const exitFn = cli?.exit ?? defaultExit4;
|
|
8854
|
+
const repoRoot = cli?.cwd ?? process.cwd();
|
|
8855
|
+
const now = cli?.now ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
8856
|
+
if (!SLUG_RE.test(opts.slug)) {
|
|
8857
|
+
stderrFn(`[cleargate hotfix new] slug must match ^[a-z0-9-]+$ (got: "${opts.slug}")`);
|
|
8858
|
+
return exitFn(1);
|
|
8859
|
+
}
|
|
8860
|
+
const activeCount = countActiveHotfixes(repoRoot);
|
|
8861
|
+
if (activeCount >= 3) {
|
|
8862
|
+
stderrFn(
|
|
8863
|
+
`Hotfix cap: \u22643 per rolling 7-day window. Currently ${activeCount} active. Bundle into a sprint or downgrade one to a CR.`
|
|
8864
|
+
);
|
|
8865
|
+
return exitFn(1);
|
|
8866
|
+
}
|
|
8867
|
+
const pendingDir = path44.join(repoRoot, ".cleargate", "delivery", "pending-sync");
|
|
8868
|
+
const maxId = maxHotfixId(pendingDir);
|
|
8869
|
+
const nextId = maxId + 1;
|
|
8870
|
+
const idStr = `HOTFIX-${String(nextId).padStart(3, "0")}`;
|
|
8871
|
+
const templatePath = resolveTemplatePath(repoRoot);
|
|
8872
|
+
let templateContent;
|
|
8873
|
+
try {
|
|
8874
|
+
templateContent = fs34.readFileSync(templatePath, "utf8");
|
|
8875
|
+
} catch {
|
|
8876
|
+
stderrFn(`[cleargate hotfix new] template not found: ${templatePath}`);
|
|
8877
|
+
return exitFn(2);
|
|
8878
|
+
}
|
|
8879
|
+
const content = templateContent.replace(/\{ID\}/g, idStr).replace(/\{SLUG\}/g, opts.slug).replace(/\{ISO\}/g, now);
|
|
8880
|
+
const fileSlug = opts.slug.replace(/-/g, "_");
|
|
8881
|
+
const fileName = `${idStr}_${fileSlug}.md`;
|
|
8882
|
+
const outPath = path44.join(pendingDir, fileName);
|
|
8883
|
+
try {
|
|
8884
|
+
fs34.mkdirSync(pendingDir, { recursive: true });
|
|
8885
|
+
fs34.writeFileSync(outPath, content, "utf8");
|
|
8886
|
+
} catch (err) {
|
|
8887
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
8888
|
+
stderrFn(`[cleargate hotfix new] write failed: ${msg}`);
|
|
8889
|
+
return exitFn(1);
|
|
8890
|
+
}
|
|
8891
|
+
stdoutFn(`[cleargate hotfix new] created: ${outPath}`);
|
|
8892
|
+
return exitFn(0);
|
|
8893
|
+
}
|
|
8894
|
+
|
|
8474
8895
|
// src/cli.ts
|
|
8475
8896
|
var program = new Command();
|
|
8476
8897
|
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 +8908,8 @@ program.command("join <invite-url>").description("join a ClearGate workspace usi
|
|
|
8487
8908
|
...cmdOpts.code !== void 0 ? { code: cmdOpts.code } : {}
|
|
8488
8909
|
});
|
|
8489
8910
|
});
|
|
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 });
|
|
8911
|
+
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) => {
|
|
8912
|
+
await initHandler({ force: opts.force ?? false, yes: opts.yes ?? false, pin: opts.pin });
|
|
8492
8913
|
});
|
|
8493
8914
|
program.command("whoami").description("print the currently authenticated agent identity").action(async () => {
|
|
8494
8915
|
const { whoamiHandler } = await import("./whoami-CX7CXJD5.js");
|
|
@@ -8610,14 +9031,20 @@ admin.command("bootstrap-root <handle>").description("seed the first root admin
|
|
|
8610
9031
|
const { bootstrapRootHandler } = await import("./bootstrap-root-FGWDICDT.js");
|
|
8611
9032
|
await bootstrapRootHandler({ handle, databaseUrl: opts.databaseUrl, force: opts.force ?? false });
|
|
8612
9033
|
});
|
|
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", [
|
|
9034
|
+
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
9035
|
"",
|
|
8615
9036
|
"Modes (mutually exclusive):",
|
|
8616
9037
|
" --check-scaffold Compute drift for all tracked scaffold files.",
|
|
8617
9038
|
" Writes .cleargate/.drift-state.json.",
|
|
8618
9039
|
" --session-start List blocked pending-sync items (\u226410, \u2264100 tokens).",
|
|
8619
9040
|
" --pricing <file> Compute USD estimate from a work item's draft_tokens.",
|
|
8620
|
-
"
|
|
9041
|
+
" --can-edit <file> Check if editing a file requires a planning work item.",
|
|
9042
|
+
" (default) Print a minimal hook-config health report.",
|
|
9043
|
+
"",
|
|
9044
|
+
"Exit codes:",
|
|
9045
|
+
" 0 Clean \u2014 no blockers, no config errors.",
|
|
9046
|
+
" 1 Blocked items or advisory issues \u2014 see stdout.",
|
|
9047
|
+
" 2 ClearGate misconfigured or partially installed \u2014 see stdout for remediation."
|
|
8621
9048
|
].join("\n")).action(async (opts) => {
|
|
8622
9049
|
await doctorHandler({
|
|
8623
9050
|
checkScaffold: opts.checkScaffold,
|
|
@@ -8625,8 +9052,10 @@ program.command("doctor").description("diagnose scaffold drift, hook health, blo
|
|
|
8625
9052
|
sessionStart: opts.sessionStart,
|
|
8626
9053
|
pricing: !!opts.pricing,
|
|
8627
9054
|
pricingFile: opts.pricing,
|
|
9055
|
+
canEdit: !!opts.canEdit,
|
|
9056
|
+
canEditFile: opts.canEdit,
|
|
8628
9057
|
verbose: opts.verbose
|
|
8629
|
-
});
|
|
9058
|
+
}, opts.cwd ? { cwd: opts.cwd } : void 0);
|
|
8630
9059
|
});
|
|
8631
9060
|
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
9061
|
"",
|
|
@@ -8648,7 +9077,7 @@ program.command("uninstall").description("remove ClearGate scaffold from a proje
|
|
|
8648
9077
|
"",
|
|
8649
9078
|
"Always removed (no prompt): .claude/agents/*.md, ClearGate hooks,",
|
|
8650
9079
|
" .claude/skills/flashcard/, CLAUDE.md CLEARGATE block,",
|
|
8651
|
-
"
|
|
9080
|
+
" `cleargate` from package.json, .install-manifest.json, .drift-state.json.",
|
|
8652
9081
|
"",
|
|
8653
9082
|
"Non-git targets: uncommitted-changes check is skipped silently."
|
|
8654
9083
|
].join("\n")).action(async (opts) => {
|
|
@@ -8701,5 +9130,9 @@ program.command("sync-log").description("filter and print sync-log entries").opt
|
|
|
8701
9130
|
limit: opts.limit !== void 0 ? parseInt(opts.limit, 10) : 50
|
|
8702
9131
|
});
|
|
8703
9132
|
});
|
|
9133
|
+
var hotfix = program.command("hotfix").description("hotfix lane commands (off-sprint trivial fix scaffolding)");
|
|
9134
|
+
hotfix.command("new <slug>").description("scaffold a new HOTFIX-NNN_<slug>.md in pending-sync/").action((slug) => {
|
|
9135
|
+
hotfixNewHandler({ slug });
|
|
9136
|
+
});
|
|
8704
9137
|
void program.parseAsync(process.argv);
|
|
8705
9138
|
//# sourceMappingURL=cli.js.map
|