cleargate 0.10.0 → 0.11.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/CHANGELOG.md +20 -0
- package/README.md +11 -1
- package/dist/MANIFEST.json +40 -26
- package/dist/chunk-HZPJ5QX4.js +459 -0
- package/dist/chunk-HZPJ5QX4.js.map +1 -0
- package/dist/cli.cjs +419 -202
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +387 -513
- package/dist/cli.js.map +1 -1
- package/dist/lib/lifecycle-reconcile.cjs +497 -0
- package/dist/lib/lifecycle-reconcile.cjs.map +1 -0
- package/dist/lib/lifecycle-reconcile.d.cts +136 -0
- package/dist/lib/lifecycle-reconcile.d.ts +136 -0
- package/dist/lib/lifecycle-reconcile.js +20 -0
- package/dist/lib/lifecycle-reconcile.js.map +1 -0
- package/dist/templates/cleargate-planning/.claude/agents/architect.md +55 -2
- package/dist/templates/cleargate-planning/.claude/agents/developer.md +22 -0
- package/dist/templates/cleargate-planning/.claude/agents/devops.md +249 -0
- package/dist/templates/cleargate-planning/.claude/agents/qa.md +41 -0
- package/dist/templates/cleargate-planning/.claude/agents/reporter.md +44 -8
- package/dist/templates/cleargate-planning/.claude/hooks/pre-commit-surface-gate.sh +21 -0
- package/dist/templates/cleargate-planning/.claude/hooks/stamp-and-gate.sh +12 -1
- package/dist/templates/cleargate-planning/.claude/hooks/token-ledger.sh +21 -1
- package/dist/templates/cleargate-planning/.claude/skills/sprint-execution/SKILL.md +200 -29
- package/dist/templates/cleargate-planning/.cleargate/knowledge/mid-sprint-triage-rubric.md +160 -0
- package/dist/templates/cleargate-planning/.cleargate/knowledge/readiness-gates.md +41 -9
- package/dist/templates/cleargate-planning/.cleargate/scripts/close_sprint.mjs +98 -16
- package/dist/templates/cleargate-planning/.cleargate/scripts/gate-checks.json +3 -3
- package/dist/templates/cleargate-planning/.cleargate/scripts/init_sprint.mjs +86 -10
- package/dist/templates/cleargate-planning/.cleargate/scripts/run_script.sh +173 -87
- package/dist/templates/cleargate-planning/.cleargate/scripts/suggest_improvements.mjs +150 -22
- package/dist/templates/cleargate-planning/.cleargate/scripts/test/test_flashcard_gate.sh +20 -20
- package/dist/templates/cleargate-planning/.cleargate/scripts/validate_state.mjs +32 -8
- package/dist/templates/cleargate-planning/.cleargate/scripts/write_dispatch.sh +12 -1
- package/dist/templates/cleargate-planning/.cleargate/templates/Bug.md +3 -0
- package/dist/templates/cleargate-planning/.cleargate/templates/CR.md +3 -0
- package/dist/templates/cleargate-planning/.cleargate/templates/epic.md +3 -0
- package/dist/templates/cleargate-planning/.cleargate/templates/hotfix.md +3 -0
- package/dist/templates/cleargate-planning/.cleargate/templates/initiative.md +1 -1
- package/dist/templates/cleargate-planning/.cleargate/templates/sprint_context.md +8 -0
- package/dist/templates/cleargate-planning/.cleargate/templates/story.md +3 -0
- package/dist/templates/cleargate-planning/CLAUDE.md +3 -1
- package/dist/templates/cleargate-planning/MANIFEST.json +40 -26
- package/package.json +8 -5
- package/templates/cleargate-planning/.claude/agents/architect.md +55 -2
- package/templates/cleargate-planning/.claude/agents/developer.md +22 -0
- package/templates/cleargate-planning/.claude/agents/devops.md +249 -0
- package/templates/cleargate-planning/.claude/agents/qa.md +41 -0
- package/templates/cleargate-planning/.claude/agents/reporter.md +44 -8
- package/templates/cleargate-planning/.claude/hooks/pre-commit-surface-gate.sh +21 -0
- package/templates/cleargate-planning/.claude/hooks/stamp-and-gate.sh +12 -1
- package/templates/cleargate-planning/.claude/hooks/token-ledger.sh +21 -1
- package/templates/cleargate-planning/.claude/skills/sprint-execution/SKILL.md +200 -29
- package/templates/cleargate-planning/.cleargate/knowledge/mid-sprint-triage-rubric.md +160 -0
- package/templates/cleargate-planning/.cleargate/knowledge/readiness-gates.md +41 -9
- package/templates/cleargate-planning/.cleargate/scripts/close_sprint.mjs +98 -16
- package/templates/cleargate-planning/.cleargate/scripts/gate-checks.json +3 -3
- package/templates/cleargate-planning/.cleargate/scripts/init_sprint.mjs +86 -10
- package/templates/cleargate-planning/.cleargate/scripts/run_script.sh +173 -87
- package/templates/cleargate-planning/.cleargate/scripts/suggest_improvements.mjs +150 -22
- package/templates/cleargate-planning/.cleargate/scripts/test/test_flashcard_gate.sh +20 -20
- package/templates/cleargate-planning/.cleargate/scripts/validate_state.mjs +32 -8
- package/templates/cleargate-planning/.cleargate/scripts/write_dispatch.sh +12 -1
- package/templates/cleargate-planning/.cleargate/templates/Bug.md +3 -0
- package/templates/cleargate-planning/.cleargate/templates/CR.md +3 -0
- package/templates/cleargate-planning/.cleargate/templates/epic.md +3 -0
- package/templates/cleargate-planning/.cleargate/templates/hotfix.md +3 -0
- package/templates/cleargate-planning/.cleargate/templates/initiative.md +1 -1
- package/templates/cleargate-planning/.cleargate/templates/sprint_context.md +8 -0
- package/templates/cleargate-planning/.cleargate/templates/story.md +3 -0
- package/templates/cleargate-planning/CLAUDE.md +3 -1
- package/templates/cleargate-planning/MANIFEST.json +40 -26
package/dist/cli.js
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
checkVerbMismatch,
|
|
4
|
+
parseFrontmatter,
|
|
5
|
+
reconcileDecomposition,
|
|
6
|
+
reconcileLifecycle
|
|
7
|
+
} from "./chunk-HZPJ5QX4.js";
|
|
2
8
|
import {
|
|
3
9
|
AcquireError,
|
|
4
10
|
acquireAccessToken,
|
|
@@ -15,10 +21,10 @@ import { Command } from "commander";
|
|
|
15
21
|
// package.json
|
|
16
22
|
var package_default = {
|
|
17
23
|
name: "cleargate",
|
|
18
|
-
version: "0.
|
|
24
|
+
version: "0.11.0",
|
|
19
25
|
private: false,
|
|
20
26
|
type: "module",
|
|
21
|
-
description: "Planning framework for Claude Code agents \u2014 sprint/epic/story protocol,
|
|
27
|
+
description: "Planning framework for Claude Code agents \u2014 sprint/epic/story protocol, five-role agent team (architect/developer/qa/devops/reporter), Karpathy-style awareness wiki.",
|
|
22
28
|
license: "MIT",
|
|
23
29
|
bin: {
|
|
24
30
|
cleargate: "dist/cli.js"
|
|
@@ -62,9 +68,12 @@ var package_default = {
|
|
|
62
68
|
build: "tsup",
|
|
63
69
|
dev: "tsup --watch",
|
|
64
70
|
typecheck: "tsc --noEmit",
|
|
65
|
-
|
|
66
|
-
test: "
|
|
67
|
-
"test:
|
|
71
|
+
test: "tsx --test --test-reporter=spec 'test/**/*.node.test.ts'",
|
|
72
|
+
"test:file": "tsx --test --test-reporter=spec",
|
|
73
|
+
"test:vitest": "npm run build && vitest run",
|
|
74
|
+
"test:vitest:watch": "vitest",
|
|
75
|
+
"test:node": "tsx --test --test-reporter=spec 'test/**/*.node.test.ts'",
|
|
76
|
+
"test:node:file": "tsx --test --test-reporter=spec"
|
|
68
77
|
},
|
|
69
78
|
dependencies: {
|
|
70
79
|
"@napi-rs/keyring": "^1.2.0",
|
|
@@ -1069,53 +1078,14 @@ function getCodebaseVersion(opts) {
|
|
|
1069
1078
|
// src/lib/stamp-frontmatter.ts
|
|
1070
1079
|
import * as fs3 from "fs/promises";
|
|
1071
1080
|
|
|
1072
|
-
// src/wiki/parse-frontmatter.ts
|
|
1073
|
-
import yaml from "js-yaml";
|
|
1074
|
-
function parseFrontmatter(raw) {
|
|
1075
|
-
const lines = raw.split("\n");
|
|
1076
|
-
if (lines[0] !== "---") {
|
|
1077
|
-
throw new Error("parseFrontmatter: input does not start with ---");
|
|
1078
|
-
}
|
|
1079
|
-
let closeIdx = -1;
|
|
1080
|
-
for (let i = 1; i < lines.length; i++) {
|
|
1081
|
-
if (lines[i] === "---") {
|
|
1082
|
-
closeIdx = i;
|
|
1083
|
-
break;
|
|
1084
|
-
}
|
|
1085
|
-
}
|
|
1086
|
-
if (closeIdx === -1) {
|
|
1087
|
-
throw new Error("parseFrontmatter: missing closing ---");
|
|
1088
|
-
}
|
|
1089
|
-
const yamlText = lines.slice(1, closeIdx).join("\n");
|
|
1090
|
-
const bodyLines = lines.slice(closeIdx + 1);
|
|
1091
|
-
if (bodyLines[0] === "") bodyLines.shift();
|
|
1092
|
-
const body = bodyLines.join("\n");
|
|
1093
|
-
if (yamlText.trim() === "") {
|
|
1094
|
-
return { fm: {}, body };
|
|
1095
|
-
}
|
|
1096
|
-
let parsed;
|
|
1097
|
-
try {
|
|
1098
|
-
parsed = yaml.load(yamlText, { schema: yaml.CORE_SCHEMA });
|
|
1099
|
-
} catch (err) {
|
|
1100
|
-
throw new Error(`parseFrontmatter: invalid YAML: ${err.message}`);
|
|
1101
|
-
}
|
|
1102
|
-
if (parsed === null || parsed === void 0) {
|
|
1103
|
-
return { fm: {}, body };
|
|
1104
|
-
}
|
|
1105
|
-
if (typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
1106
|
-
throw new Error("parseFrontmatter: frontmatter is not a YAML mapping");
|
|
1107
|
-
}
|
|
1108
|
-
return { fm: parsed, body };
|
|
1109
|
-
}
|
|
1110
|
-
|
|
1111
1081
|
// src/lib/frontmatter-yaml.ts
|
|
1112
|
-
import
|
|
1082
|
+
import yaml from "js-yaml";
|
|
1113
1083
|
function serializeFrontmatter(fm) {
|
|
1114
1084
|
if (Object.keys(fm).length === 0) {
|
|
1115
1085
|
return "---\n---";
|
|
1116
1086
|
}
|
|
1117
|
-
const yamlBody =
|
|
1118
|
-
schema:
|
|
1087
|
+
const yamlBody = yaml.dump(fm, {
|
|
1088
|
+
schema: yaml.CORE_SCHEMA,
|
|
1119
1089
|
lineWidth: -1,
|
|
1120
1090
|
noRefs: true,
|
|
1121
1091
|
noCompatMode: true,
|
|
@@ -1297,7 +1267,20 @@ var HOOK_FILES_WITH_PIN = /* @__PURE__ */ new Set([
|
|
|
1297
1267
|
".claude/hooks/stamp-and-gate.sh",
|
|
1298
1268
|
".claude/hooks/session-start.sh"
|
|
1299
1269
|
]);
|
|
1300
|
-
var SKIP_FILES = /* @__PURE__ */ new Set(["CLAUDE.md"]);
|
|
1270
|
+
var SKIP_FILES = /* @__PURE__ */ new Set(["CLAUDE.md", "MANIFEST.json"]);
|
|
1271
|
+
var FIRST_INSTALL_ONLY = [
|
|
1272
|
+
".gitignore",
|
|
1273
|
+
".cleargate/FLASHCARD.md",
|
|
1274
|
+
/^\.cleargate\/scripts\//
|
|
1275
|
+
];
|
|
1276
|
+
function isFirstInstallOnly(relPath) {
|
|
1277
|
+
for (const pattern of FIRST_INSTALL_ONLY) {
|
|
1278
|
+
if (typeof pattern === "string" ? pattern === relPath : pattern.test(relPath)) {
|
|
1279
|
+
return true;
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
return false;
|
|
1283
|
+
}
|
|
1301
1284
|
function listFilesRecursive(dir) {
|
|
1302
1285
|
const results = [];
|
|
1303
1286
|
function walk(current, rel) {
|
|
@@ -1351,6 +1334,14 @@ function copyPayload(payloadDir, targetCwd, opts) {
|
|
|
1351
1334
|
report.actions.push({ action: "skipped", relPath });
|
|
1352
1335
|
continue;
|
|
1353
1336
|
}
|
|
1337
|
+
if (isFirstInstallOnly(relPath)) {
|
|
1338
|
+
if (needsExec && process.platform !== "win32") {
|
|
1339
|
+
fs5.chmodSync(dstPath, 493);
|
|
1340
|
+
}
|
|
1341
|
+
report.skipped++;
|
|
1342
|
+
report.actions.push({ action: "skipped", relPath });
|
|
1343
|
+
continue;
|
|
1344
|
+
}
|
|
1354
1345
|
fs5.writeFileSync(dstPath, srcBuffer);
|
|
1355
1346
|
if (needsExec && process.platform !== "win32") {
|
|
1356
1347
|
fs5.chmodSync(dstPath, 493);
|
|
@@ -1483,7 +1474,8 @@ var PREFIX_MAP = [
|
|
|
1483
1474
|
{ prefix: "SPRINT-", type: "sprint", bucket: "sprints" },
|
|
1484
1475
|
{ prefix: "PROPOSAL-", type: "proposal", bucket: "proposals" },
|
|
1485
1476
|
{ prefix: "CR-", type: "cr", bucket: "crs" },
|
|
1486
|
-
{ prefix: "BUG-", type: "bug", bucket: "bugs" }
|
|
1477
|
+
{ prefix: "BUG-", type: "bug", bucket: "bugs" },
|
|
1478
|
+
{ prefix: "INITIATIVE-", type: "initiative", bucket: "initiatives" }
|
|
1487
1479
|
];
|
|
1488
1480
|
function deriveBucket(filename) {
|
|
1489
1481
|
const base = filename.includes("/") ? filename.split("/").pop() : filename;
|
|
@@ -3496,7 +3488,7 @@ function loadWikiPages(wikiRoot) {
|
|
|
3496
3488
|
import * as fs19 from "fs";
|
|
3497
3489
|
import * as path19 from "path";
|
|
3498
3490
|
import { spawnSync as spawnSync5 } from "child_process";
|
|
3499
|
-
import
|
|
3491
|
+
import yaml2 from "js-yaml";
|
|
3500
3492
|
|
|
3501
3493
|
// src/lib/work-item-type.ts
|
|
3502
3494
|
var FM_KEY_MAP = [
|
|
@@ -3504,14 +3496,18 @@ var FM_KEY_MAP = [
|
|
|
3504
3496
|
{ key: "epic_id", type: "epic" },
|
|
3505
3497
|
{ key: "proposal_id", type: "proposal" },
|
|
3506
3498
|
{ key: "cr_id", type: "cr" },
|
|
3507
|
-
{ key: "bug_id", type: "bug" }
|
|
3499
|
+
{ key: "bug_id", type: "bug" },
|
|
3500
|
+
{ key: "initiative_id", type: "initiative" },
|
|
3501
|
+
{ key: "sprint_id", type: "sprint" }
|
|
3508
3502
|
];
|
|
3509
3503
|
var PREFIX_MAP2 = [
|
|
3510
3504
|
{ prefix: "STORY-", type: "story" },
|
|
3511
3505
|
{ prefix: "EPIC-", type: "epic" },
|
|
3512
3506
|
{ prefix: "PROPOSAL-", type: "proposal" },
|
|
3513
3507
|
{ prefix: "CR-", type: "cr" },
|
|
3514
|
-
{ prefix: "BUG-", type: "bug" }
|
|
3508
|
+
{ prefix: "BUG-", type: "bug" },
|
|
3509
|
+
{ prefix: "INITIATIVE-", type: "initiative" },
|
|
3510
|
+
{ prefix: "SPRINT-", type: "sprint" }
|
|
3515
3511
|
];
|
|
3516
3512
|
function detectWorkItemTypeFromFm(fm) {
|
|
3517
3513
|
for (const { key, type } of FM_KEY_MAP) {
|
|
@@ -3536,7 +3532,9 @@ var WORK_ITEM_TRANSITIONS = {
|
|
|
3536
3532
|
epic: ["ready-for-decomposition", "ready-for-coding"],
|
|
3537
3533
|
story: ["ready-for-execution"],
|
|
3538
3534
|
cr: ["ready-to-apply"],
|
|
3539
|
-
bug: ["ready-for-fix"]
|
|
3535
|
+
bug: ["ready-for-fix"],
|
|
3536
|
+
initiative: ["ready-for-decomposition"],
|
|
3537
|
+
sprint: ["ready-for-execution"]
|
|
3540
3538
|
};
|
|
3541
3539
|
|
|
3542
3540
|
// src/wiki/lint-checks.ts
|
|
@@ -3743,7 +3741,7 @@ function parseCachedGateResult(raw) {
|
|
|
3743
3741
|
if (typeof raw === "string") {
|
|
3744
3742
|
if (!raw.startsWith("{")) return null;
|
|
3745
3743
|
try {
|
|
3746
|
-
const parsed =
|
|
3744
|
+
const parsed = yaml2.load(raw);
|
|
3747
3745
|
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return null;
|
|
3748
3746
|
const p = parsed;
|
|
3749
3747
|
return { pass: p["pass"], failing_criteria: p["failing_criteria"], last_gate_check: p["last_gate_check"] };
|
|
@@ -3831,7 +3829,7 @@ function discoverPlainTextMentions(pages, repoRoot) {
|
|
|
3831
3829
|
if (p.page.id) byId.set(p.page.id, true);
|
|
3832
3830
|
}
|
|
3833
3831
|
const suggestions = [];
|
|
3834
|
-
const
|
|
3832
|
+
const ID_PATTERN = /\b((?:EPIC|STORY|SPRINT|PROPOSAL|CR|BUG)-[\w-]+)\b/g;
|
|
3835
3833
|
const LINK_PATTERN = /\[\[[\w-]+\]\]/g;
|
|
3836
3834
|
for (const page of pages) {
|
|
3837
3835
|
const relPage = path19.relative(wikiRoot, page.absPath).replace(/\\/g, "/");
|
|
@@ -3840,7 +3838,7 @@ function discoverPlainTextMentions(pages, repoRoot) {
|
|
|
3840
3838
|
const inner = m[0].slice(2, -2);
|
|
3841
3839
|
wrappedRefs.add(inner);
|
|
3842
3840
|
}
|
|
3843
|
-
for (const m of page.body.matchAll(
|
|
3841
|
+
for (const m of page.body.matchAll(ID_PATTERN)) {
|
|
3844
3842
|
const mentionedId = m[1];
|
|
3845
3843
|
if (!byId.has(mentionedId)) continue;
|
|
3846
3844
|
if (wrappedRefs.has(mentionedId)) continue;
|
|
@@ -3874,7 +3872,7 @@ function checkIndexBudget(repoRoot, indexTokenCeiling) {
|
|
|
3874
3872
|
// src/lib/wiki-config.ts
|
|
3875
3873
|
import * as fs20 from "fs";
|
|
3876
3874
|
import * as path20 from "path";
|
|
3877
|
-
import
|
|
3875
|
+
import yaml3 from "js-yaml";
|
|
3878
3876
|
var DEFAULT_INDEX_TOKEN_CEILING = 8e3;
|
|
3879
3877
|
var DEFAULT_BUCKET_PAGINATION_CEILING = 50;
|
|
3880
3878
|
function loadWikiConfig(repoRoot) {
|
|
@@ -3896,7 +3894,7 @@ function loadWikiConfig(repoRoot) {
|
|
|
3896
3894
|
}
|
|
3897
3895
|
let parsed;
|
|
3898
3896
|
try {
|
|
3899
|
-
parsed =
|
|
3897
|
+
parsed = yaml3.load(raw, { schema: yaml3.CORE_SCHEMA });
|
|
3900
3898
|
} catch (err) {
|
|
3901
3899
|
throw new Error(`Malformed YAML in ${configPath}: ${String(err)}`);
|
|
3902
3900
|
}
|
|
@@ -5138,7 +5136,7 @@ async function doctorHandler(flags, cli) {
|
|
|
5138
5136
|
|
|
5139
5137
|
// src/commands/gate.ts
|
|
5140
5138
|
import * as fs29 from "fs";
|
|
5141
|
-
import * as
|
|
5139
|
+
import * as path30 from "path";
|
|
5142
5140
|
import { spawnSync as spawnSync8 } from "child_process";
|
|
5143
5141
|
|
|
5144
5142
|
// src/commands/execution-mode.ts
|
|
@@ -5226,12 +5224,19 @@ function printInertAndExit(stdoutFn, exitFn) {
|
|
|
5226
5224
|
return exitFn(0);
|
|
5227
5225
|
}
|
|
5228
5226
|
|
|
5227
|
+
// src/lib/script-paths.ts
|
|
5228
|
+
import * as path28 from "path";
|
|
5229
|
+
function resolveCleargateScript(opts, scriptName) {
|
|
5230
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
5231
|
+
return path28.join(cwd, ".cleargate", "scripts", scriptName);
|
|
5232
|
+
}
|
|
5233
|
+
|
|
5229
5234
|
// src/commands/gate.ts
|
|
5230
|
-
import
|
|
5235
|
+
import yaml5 from "js-yaml";
|
|
5231
5236
|
|
|
5232
5237
|
// src/lib/readiness-predicates.ts
|
|
5233
5238
|
import * as fs27 from "fs";
|
|
5234
|
-
import * as
|
|
5239
|
+
import * as path29 from "path";
|
|
5235
5240
|
function parsePredicate(src) {
|
|
5236
5241
|
const s = src.trim();
|
|
5237
5242
|
const fmMatch = s.match(
|
|
@@ -5263,7 +5268,7 @@ function parsePredicate(src) {
|
|
|
5263
5268
|
return { kind: "body-contains", needle: bodyMatch[1], negated: false };
|
|
5264
5269
|
}
|
|
5265
5270
|
const sectionMatch = s.match(
|
|
5266
|
-
/^section\((\d+)\) has (≥|>=|==|>)(\d+) (checked-checkbox|unchecked-checkbox|listed-item)$/
|
|
5271
|
+
/^section\((\d+)\) has (≥|>=|==|>)(\d+) (checked-checkbox|unchecked-checkbox|listed-item|declared-item)$/
|
|
5267
5272
|
);
|
|
5268
5273
|
if (sectionMatch) {
|
|
5269
5274
|
const index = parseInt(sectionMatch[1], 10);
|
|
@@ -5291,6 +5296,9 @@ function parsePredicate(src) {
|
|
|
5291
5296
|
const value = statusMatch[2].trim().replace(/^['"]|['"]$/g, "");
|
|
5292
5297
|
return { kind: "status-of", id, value };
|
|
5293
5298
|
}
|
|
5299
|
+
if (s === "existing-surfaces-verified") {
|
|
5300
|
+
return { kind: "existing-surfaces-verified" };
|
|
5301
|
+
}
|
|
5294
5302
|
throw new Error(`unsupported predicate shape: ${src}`);
|
|
5295
5303
|
}
|
|
5296
5304
|
function parseValue(raw) {
|
|
@@ -5319,6 +5327,8 @@ function evaluate(predicate, doc, opts) {
|
|
|
5319
5327
|
return evalLinkTargetExists(parsed, opts);
|
|
5320
5328
|
case "status-of":
|
|
5321
5329
|
return evalStatusOf(parsed, opts, projectRoot);
|
|
5330
|
+
case "existing-surfaces-verified":
|
|
5331
|
+
return evalExistingSurfacesVerified(doc, projectRoot);
|
|
5322
5332
|
}
|
|
5323
5333
|
}
|
|
5324
5334
|
function evalFrontmatter(parsed, doc, projectRoot) {
|
|
@@ -5395,8 +5405,14 @@ function compareValues(actual, op, expected) {
|
|
|
5395
5405
|
}
|
|
5396
5406
|
function resolveLinkedPath(ref, docAbsPath, projectRoot) {
|
|
5397
5407
|
const candidates = [
|
|
5398
|
-
|
|
5399
|
-
|
|
5408
|
+
path29.resolve(path29.dirname(docAbsPath), ref),
|
|
5409
|
+
// 1. relative to citer
|
|
5410
|
+
path29.resolve(projectRoot, ref),
|
|
5411
|
+
// 2. project root
|
|
5412
|
+
path29.resolve(projectRoot, ".cleargate", "delivery", "pending-sync", ref),
|
|
5413
|
+
// 3. live
|
|
5414
|
+
path29.resolve(projectRoot, ".cleargate", "delivery", "archive", ref)
|
|
5415
|
+
// 4. archived
|
|
5400
5416
|
];
|
|
5401
5417
|
for (const candidate of candidates) {
|
|
5402
5418
|
if (!candidate.startsWith(projectRoot)) continue;
|
|
@@ -5533,6 +5549,9 @@ function evalSection(parsed, doc) {
|
|
|
5533
5549
|
case "listed-item":
|
|
5534
5550
|
actualCount = (sectionContent.match(/^\s*- /gm) || []).length;
|
|
5535
5551
|
break;
|
|
5552
|
+
case "declared-item":
|
|
5553
|
+
actualCount = countDeclaredItems(sectionContent);
|
|
5554
|
+
break;
|
|
5536
5555
|
}
|
|
5537
5556
|
const pass = applyCountOp(actualCount, parsed.count.op, parsed.count.n);
|
|
5538
5557
|
const opStr = parsed.count.op === ">=" ? "\u2265" : parsed.count.op;
|
|
@@ -5549,9 +5568,39 @@ function applyCountOp(actual, op, n) {
|
|
|
5549
5568
|
return actual > n;
|
|
5550
5569
|
}
|
|
5551
5570
|
}
|
|
5571
|
+
function countDeclaredItems(sectionContent) {
|
|
5572
|
+
const lines = sectionContent.split("\n");
|
|
5573
|
+
let count = 0;
|
|
5574
|
+
let inTable = false;
|
|
5575
|
+
for (const line of lines) {
|
|
5576
|
+
if (/^\s*- /.test(line)) {
|
|
5577
|
+
count++;
|
|
5578
|
+
inTable = false;
|
|
5579
|
+
continue;
|
|
5580
|
+
}
|
|
5581
|
+
if (/^\|.+\|/.test(line)) {
|
|
5582
|
+
if (/^\|[\s\-:]+\|[\s\-:|]*$/.test(line.replace(/\s/g, ""))) {
|
|
5583
|
+
inTable = true;
|
|
5584
|
+
continue;
|
|
5585
|
+
}
|
|
5586
|
+
if (inTable) {
|
|
5587
|
+
count++;
|
|
5588
|
+
}
|
|
5589
|
+
continue;
|
|
5590
|
+
}
|
|
5591
|
+
if (inTable && !/^\|/.test(line)) {
|
|
5592
|
+
inTable = false;
|
|
5593
|
+
}
|
|
5594
|
+
if (/^(\*{1,2}|_{1,2})?[A-Z][^|*\n]*(\*{1,2}|_{1,2})?:/.test(line.trim())) {
|
|
5595
|
+
count++;
|
|
5596
|
+
continue;
|
|
5597
|
+
}
|
|
5598
|
+
}
|
|
5599
|
+
return count;
|
|
5600
|
+
}
|
|
5552
5601
|
function evalFileExists(parsed, projectRoot) {
|
|
5553
|
-
const resolved =
|
|
5554
|
-
if (!resolved.startsWith(projectRoot +
|
|
5602
|
+
const resolved = path29.resolve(projectRoot, parsed.path);
|
|
5603
|
+
if (!resolved.startsWith(projectRoot + path29.sep) && resolved !== projectRoot) {
|
|
5555
5604
|
return {
|
|
5556
5605
|
pass: false,
|
|
5557
5606
|
detail: `path '${parsed.path}' resolves outside project root (sandbox violation)`
|
|
@@ -5565,7 +5614,7 @@ function evalFileExists(parsed, projectRoot) {
|
|
|
5565
5614
|
}
|
|
5566
5615
|
function evalLinkTargetExists(parsed, opts) {
|
|
5567
5616
|
const projectRoot = opts?.projectRoot ?? process.cwd();
|
|
5568
|
-
const wikiIndexPath = opts?.wikiIndexPath ??
|
|
5617
|
+
const wikiIndexPath = opts?.wikiIndexPath ?? path29.join(projectRoot, ".cleargate", "wiki", "index.md");
|
|
5569
5618
|
if (!wikiIndexPath.startsWith(projectRoot)) {
|
|
5570
5619
|
return { pass: false, detail: "wikiIndexPath resolves outside project root" };
|
|
5571
5620
|
}
|
|
@@ -5582,7 +5631,7 @@ function evalLinkTargetExists(parsed, opts) {
|
|
|
5582
5631
|
};
|
|
5583
5632
|
}
|
|
5584
5633
|
function evalStatusOf(parsed, opts, projectRoot) {
|
|
5585
|
-
const wikiIndexPath = opts?.wikiIndexPath ??
|
|
5634
|
+
const wikiIndexPath = opts?.wikiIndexPath ?? path29.join(projectRoot, ".cleargate", "wiki", "index.md");
|
|
5586
5635
|
if (!wikiIndexPath.startsWith(projectRoot)) {
|
|
5587
5636
|
return { pass: false, detail: "wikiIndexPath resolves outside project root" };
|
|
5588
5637
|
}
|
|
@@ -5599,7 +5648,7 @@ function evalStatusOf(parsed, opts, projectRoot) {
|
|
|
5599
5648
|
return { pass: false, detail: `[[${parsed.id}]] not found in wiki index` };
|
|
5600
5649
|
}
|
|
5601
5650
|
const rawPath = rowMatch[1].trim();
|
|
5602
|
-
const fullPath =
|
|
5651
|
+
const fullPath = path29.resolve(projectRoot, rawPath);
|
|
5603
5652
|
if (!fullPath.startsWith(projectRoot)) {
|
|
5604
5653
|
return { pass: false, detail: `wiki path for ${parsed.id} resolves outside project root` };
|
|
5605
5654
|
}
|
|
@@ -5614,10 +5663,64 @@ function evalStatusOf(parsed, opts, projectRoot) {
|
|
|
5614
5663
|
detail: pass ? `status-of([[${parsed.id}]]) == ${parsed.value}` : `status-of([[${parsed.id}]]) is '${status}', expected '${parsed.value}'`
|
|
5615
5664
|
};
|
|
5616
5665
|
}
|
|
5666
|
+
function evalExistingSurfacesVerified(doc, projectRoot) {
|
|
5667
|
+
const body = doc.body;
|
|
5668
|
+
const rawParts = body.split(/^(?=## )/m);
|
|
5669
|
+
let sectionContent;
|
|
5670
|
+
for (const part of rawParts) {
|
|
5671
|
+
if (part.startsWith("## Existing Surfaces")) {
|
|
5672
|
+
sectionContent = part;
|
|
5673
|
+
break;
|
|
5674
|
+
}
|
|
5675
|
+
}
|
|
5676
|
+
if (!sectionContent) {
|
|
5677
|
+
return {
|
|
5678
|
+
pass: true,
|
|
5679
|
+
detail: `not-applicable: ## Existing Surfaces section absent \u2014 reuse-audit-recorded already failing`
|
|
5680
|
+
};
|
|
5681
|
+
}
|
|
5682
|
+
const PATH_RE = /[a-zA-Z0-9_./-]+\.[a-zA-Z]{1,5}(?::[a-zA-Z_][a-zA-Z0-9_]*)?/g;
|
|
5683
|
+
const rawMatches = sectionContent.match(PATH_RE) ?? [];
|
|
5684
|
+
const paths = [...new Set(rawMatches.map((m) => m.replace(/:([a-zA-Z_][a-zA-Z0-9_]*)$/, "")))];
|
|
5685
|
+
if (paths.length === 0) {
|
|
5686
|
+
const SENTINEL_RE = /no overlap found|no existing surface|no prior implementation|audit returned empty/i;
|
|
5687
|
+
if (SENTINEL_RE.test(sectionContent)) {
|
|
5688
|
+
return {
|
|
5689
|
+
pass: true,
|
|
5690
|
+
detail: `## Existing Surfaces contains no path citations; sentinel phrase present \u2014 audit explicitly empty`
|
|
5691
|
+
};
|
|
5692
|
+
}
|
|
5693
|
+
return {
|
|
5694
|
+
pass: false,
|
|
5695
|
+
detail: `'## Existing Surfaces' has no path citations and no "no overlap found" sentinel`
|
|
5696
|
+
};
|
|
5697
|
+
}
|
|
5698
|
+
const missing = [];
|
|
5699
|
+
for (const p of paths) {
|
|
5700
|
+
const resolved = path29.resolve(projectRoot, p);
|
|
5701
|
+
if (!resolved.startsWith(projectRoot + path29.sep) && resolved !== projectRoot) {
|
|
5702
|
+
missing.push(p);
|
|
5703
|
+
continue;
|
|
5704
|
+
}
|
|
5705
|
+
if (!fs27.existsSync(resolved)) {
|
|
5706
|
+
missing.push(p);
|
|
5707
|
+
}
|
|
5708
|
+
}
|
|
5709
|
+
if (missing.length > 0) {
|
|
5710
|
+
return {
|
|
5711
|
+
pass: false,
|
|
5712
|
+
detail: `cited paths do not exist on disk: ${missing.join(", ")}`
|
|
5713
|
+
};
|
|
5714
|
+
}
|
|
5715
|
+
return {
|
|
5716
|
+
pass: true,
|
|
5717
|
+
detail: `all ${paths.length} cited path${paths.length === 1 ? "" : "s"} exist on disk`
|
|
5718
|
+
};
|
|
5719
|
+
}
|
|
5617
5720
|
|
|
5618
5721
|
// src/lib/frontmatter-cache.ts
|
|
5619
5722
|
import * as fs28 from "fs/promises";
|
|
5620
|
-
import
|
|
5723
|
+
import yaml4 from "js-yaml";
|
|
5621
5724
|
async function readCachedGate(absPath) {
|
|
5622
5725
|
let raw;
|
|
5623
5726
|
try {
|
|
@@ -5685,7 +5788,7 @@ function coerceCachedGate(val) {
|
|
|
5685
5788
|
}
|
|
5686
5789
|
if (typeof val === "string" && val.startsWith("{")) {
|
|
5687
5790
|
try {
|
|
5688
|
-
const parsed =
|
|
5791
|
+
const parsed = yaml4.load(val, { schema: yaml4.CORE_SCHEMA });
|
|
5689
5792
|
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return null;
|
|
5690
5793
|
const p = parsed;
|
|
5691
5794
|
return {
|
|
@@ -5708,7 +5811,7 @@ function loadGateBlocks(gatesDocPath) {
|
|
|
5708
5811
|
let match;
|
|
5709
5812
|
while ((match = fenceRe.exec(raw)) !== null) {
|
|
5710
5813
|
const yamlContent = match[1];
|
|
5711
|
-
const parsed =
|
|
5814
|
+
const parsed = yaml5.load(yamlContent);
|
|
5712
5815
|
const block = Array.isArray(parsed) ? parsed[0] : parsed;
|
|
5713
5816
|
if (block && typeof block === "object" && "work_item_type" in block && "transition" in block && "severity" in block && "criteria" in block) {
|
|
5714
5817
|
blocks.push(block);
|
|
@@ -5737,7 +5840,7 @@ async function gateCheckHandler(file, opts, cli) {
|
|
|
5737
5840
|
const exitFn = cli?.exit ?? ((code) => process.exit(code));
|
|
5738
5841
|
const cwd = cli?.cwd ?? process.cwd();
|
|
5739
5842
|
const nowFn = cli?.now ?? (() => /* @__PURE__ */ new Date());
|
|
5740
|
-
const absPath =
|
|
5843
|
+
const absPath = path30.isAbsolute(file) ? file : path30.resolve(cwd, file);
|
|
5741
5844
|
if (!fs29.existsSync(absPath)) {
|
|
5742
5845
|
stderrFn(`[cleargate gate] error: file not found: ${absPath}`);
|
|
5743
5846
|
return exitFn(1);
|
|
@@ -5763,7 +5866,7 @@ async function gateCheckHandler(file, opts, cli) {
|
|
|
5763
5866
|
return exitFn(1);
|
|
5764
5867
|
}
|
|
5765
5868
|
const projectRoot = cwd;
|
|
5766
|
-
const gatesDocPath = cli?.gatesDocPath ??
|
|
5869
|
+
const gatesDocPath = cli?.gatesDocPath ?? path30.join(projectRoot, ".cleargate", "knowledge", "readiness-gates.md");
|
|
5767
5870
|
if (!fs29.existsSync(gatesDocPath)) {
|
|
5768
5871
|
stderrFn(`[cleargate gate] error: readiness-gates.md not found at: ${gatesDocPath}`);
|
|
5769
5872
|
return exitFn(1);
|
|
@@ -5787,7 +5890,6 @@ async function gateCheckHandler(file, opts, cli) {
|
|
|
5787
5890
|
const wikiIndexPath = cli?.wikiIndexPath;
|
|
5788
5891
|
const parsedDoc = { fm, body, absPath };
|
|
5789
5892
|
const evalOpts = { projectRoot, ...wikiIndexPath ? { wikiIndexPath } : {} };
|
|
5790
|
-
const failingCriteria = [];
|
|
5791
5893
|
const allResults = [];
|
|
5792
5894
|
for (const criterion of gate2.criteria) {
|
|
5793
5895
|
let result;
|
|
@@ -5796,9 +5898,32 @@ async function gateCheckHandler(file, opts, cli) {
|
|
|
5796
5898
|
} catch (err) {
|
|
5797
5899
|
result = { pass: false, detail: `predicate error: ${String(err)}` };
|
|
5798
5900
|
}
|
|
5799
|
-
allResults.push({ id: criterion.id, ...result });
|
|
5800
|
-
|
|
5801
|
-
|
|
5901
|
+
allResults.push({ id: criterion.id, ...result, or_group: criterion.or_group });
|
|
5902
|
+
}
|
|
5903
|
+
const failingCriteria = [];
|
|
5904
|
+
const orGroups = /* @__PURE__ */ new Map();
|
|
5905
|
+
for (const r of allResults) {
|
|
5906
|
+
if (r.or_group) {
|
|
5907
|
+
const existing = orGroups.get(r.or_group) ?? [];
|
|
5908
|
+
existing.push(r);
|
|
5909
|
+
orGroups.set(r.or_group, existing);
|
|
5910
|
+
}
|
|
5911
|
+
}
|
|
5912
|
+
for (const r of allResults) {
|
|
5913
|
+
if (r.or_group) {
|
|
5914
|
+
const groupMembers = orGroups.get(r.or_group);
|
|
5915
|
+
const isFirstMember = groupMembers[0].id === r.id;
|
|
5916
|
+
if (isFirstMember) {
|
|
5917
|
+
const anyPasses = groupMembers.some((m) => m.pass);
|
|
5918
|
+
if (!anyPasses) {
|
|
5919
|
+
const details = groupMembers.map((m) => `${m.id}: ${m.detail}`).join("; ");
|
|
5920
|
+
failingCriteria.push({ id: r.or_group, detail: `OR-group failed \u2014 all alternatives failed: ${details}` });
|
|
5921
|
+
}
|
|
5922
|
+
}
|
|
5923
|
+
} else {
|
|
5924
|
+
if (!r.pass) {
|
|
5925
|
+
failingCriteria.push({ id: r.id, detail: r.detail });
|
|
5926
|
+
}
|
|
5802
5927
|
}
|
|
5803
5928
|
}
|
|
5804
5929
|
const overallPass = failingCriteria.length === 0;
|
|
@@ -5815,15 +5940,15 @@ async function gateCheckHandler(file, opts, cli) {
|
|
|
5815
5940
|
if (overallPass) {
|
|
5816
5941
|
stdoutFn(`\u2705 ${detectedType}.${transition} passed (${gate2.criteria.length} criteria)`);
|
|
5817
5942
|
} else {
|
|
5818
|
-
for (const
|
|
5819
|
-
if (
|
|
5820
|
-
|
|
5821
|
-
|
|
5822
|
-
}
|
|
5823
|
-
stdoutFn(`\u274C ${r.id}: ${r.detail}`);
|
|
5824
|
-
}
|
|
5943
|
+
for (const fc of failingCriteria) {
|
|
5944
|
+
if (isAdvisory) {
|
|
5945
|
+
stdoutFn(`\u26A0 ${fc.id}: ${fc.detail} (advisory)`);
|
|
5946
|
+
} else {
|
|
5947
|
+
stdoutFn(`\u274C ${fc.id}: ${fc.detail}`);
|
|
5825
5948
|
}
|
|
5826
|
-
|
|
5949
|
+
}
|
|
5950
|
+
if (opts.verbose) {
|
|
5951
|
+
for (const r of allResults) {
|
|
5827
5952
|
stdoutFn(` [${r.pass ? "pass" : "fail"}] ${r.id}: ${r.detail}`);
|
|
5828
5953
|
}
|
|
5829
5954
|
}
|
|
@@ -5837,7 +5962,7 @@ async function gateExplainHandler(file, cli) {
|
|
|
5837
5962
|
const stderrFn = cli?.stderr ?? ((s) => process.stderr.write(s + "\n"));
|
|
5838
5963
|
const exitFn = cli?.exit ?? ((code) => process.exit(code));
|
|
5839
5964
|
const cwd = cli?.cwd ?? process.cwd();
|
|
5840
|
-
const absPath =
|
|
5965
|
+
const absPath = path30.isAbsolute(file) ? file : path30.resolve(cwd, file);
|
|
5841
5966
|
if (!fs29.existsSync(absPath)) {
|
|
5842
5967
|
stderrFn(`[cleargate gate] error: file not found: ${absPath}`);
|
|
5843
5968
|
return exitFn(1);
|
|
@@ -5870,7 +5995,7 @@ async function gateExplainHandler(file, cli) {
|
|
|
5870
5995
|
function resolveRunScriptForGate(opts) {
|
|
5871
5996
|
if (opts.runScriptPath) return opts.runScriptPath;
|
|
5872
5997
|
const cwd = opts.cwd ?? process.cwd();
|
|
5873
|
-
return
|
|
5998
|
+
return path30.join(cwd, ".cleargate", "scripts", "run_script.sh");
|
|
5874
5999
|
}
|
|
5875
6000
|
function gateQaHandler(opts, cli) {
|
|
5876
6001
|
const stdoutFn = cli?.stdout ?? ((s) => process.stdout.write(s + "\n"));
|
|
@@ -5886,9 +6011,11 @@ function gateQaHandler(opts, cli) {
|
|
|
5886
6011
|
return printInertAndExit(stdoutFn, exitFn);
|
|
5887
6012
|
}
|
|
5888
6013
|
const runScript = resolveRunScriptForGate(cli ?? {});
|
|
6014
|
+
const qaCwd = cli?.cwd ?? process.cwd();
|
|
6015
|
+
const gateRunnerPath = resolveCleargateScript({ cwd: qaCwd }, "pre_gate_runner.sh");
|
|
5889
6016
|
const result = spawnFn(
|
|
5890
6017
|
"bash",
|
|
5891
|
-
[runScript, "
|
|
6018
|
+
[runScript, "bash", gateRunnerPath, "qa", opts.worktree, opts.branch],
|
|
5892
6019
|
{ stdio: "inherit" }
|
|
5893
6020
|
);
|
|
5894
6021
|
if (result.error) {
|
|
@@ -5912,9 +6039,11 @@ function gateArchHandler(opts, cli) {
|
|
|
5912
6039
|
return printInertAndExit(stdoutFn, exitFn);
|
|
5913
6040
|
}
|
|
5914
6041
|
const runScript = resolveRunScriptForGate(cli ?? {});
|
|
6042
|
+
const archCwd = cli?.cwd ?? process.cwd();
|
|
6043
|
+
const archGateRunnerPath = resolveCleargateScript({ cwd: archCwd }, "pre_gate_runner.sh");
|
|
5915
6044
|
const result = spawnFn(
|
|
5916
6045
|
"bash",
|
|
5917
|
-
[runScript, "
|
|
6046
|
+
[runScript, "bash", archGateRunnerPath, "arch", opts.worktree, opts.branch],
|
|
5918
6047
|
{ stdio: "inherit" }
|
|
5919
6048
|
);
|
|
5920
6049
|
if (result.error) {
|
|
@@ -5962,319 +6091,10 @@ function gateRunHandler(name, opts, cli) {
|
|
|
5962
6091
|
}
|
|
5963
6092
|
|
|
5964
6093
|
// src/commands/sprint.ts
|
|
5965
|
-
import * as fs31 from "fs";
|
|
5966
|
-
import * as path31 from "path";
|
|
5967
|
-
import { spawnSync as spawnSync11, execSync as execSync2 } from "child_process";
|
|
5968
|
-
import yaml7 from "js-yaml";
|
|
5969
|
-
|
|
5970
|
-
// src/lib/lifecycle-reconcile.ts
|
|
5971
6094
|
import * as fs30 from "fs";
|
|
5972
|
-
import * as
|
|
5973
|
-
import { spawnSync as spawnSync10 } from "child_process";
|
|
5974
|
-
|
|
5975
|
-
feat: {
|
|
5976
|
-
types: ["STORY", "EPIC", "CR"],
|
|
5977
|
-
expected: ["Done", "Completed"]
|
|
5978
|
-
},
|
|
5979
|
-
fix: {
|
|
5980
|
-
types: ["BUG", "HOTFIX"],
|
|
5981
|
-
expected: ["Verified", "Done", "Completed"]
|
|
5982
|
-
}
|
|
5983
|
-
};
|
|
5984
|
-
var ID_PATTERN = /\b(STORY-\d{3}-\d{2}|(CR|BUG|EPIC|HOTFIX)-\d{3}|(PROPOSAL|PROP)-\d{3})\b/g;
|
|
5985
|
-
function normalizeId(raw) {
|
|
5986
|
-
return raw.replace(/^PROP-(\d+)$/, "PROPOSAL-$1");
|
|
5987
|
-
}
|
|
5988
|
-
function idType(id) {
|
|
5989
|
-
if (/^STORY-\d{3}-\d{2}$/.test(id)) return "STORY";
|
|
5990
|
-
if (/^CR-\d{3}$/.test(id)) return "CR";
|
|
5991
|
-
if (/^BUG-\d{3}$/.test(id)) return "BUG";
|
|
5992
|
-
if (/^EPIC-\d{3}$/.test(id)) return "EPIC";
|
|
5993
|
-
if (/^PROPOSAL-\d{3}$/.test(id)) return "PROPOSAL";
|
|
5994
|
-
if (/^HOTFIX-\d{3}$/.test(id)) return "HOTFIX";
|
|
5995
|
-
return null;
|
|
5996
|
-
}
|
|
5997
|
-
function parseCommitMessage(msg) {
|
|
5998
|
-
const lines = msg.split("\n");
|
|
5999
|
-
const subject = lines[0] ?? "";
|
|
6000
|
-
let firstBodyLine = "";
|
|
6001
|
-
for (let i = 1; i < lines.length; i++) {
|
|
6002
|
-
if (lines[i]?.trim()) {
|
|
6003
|
-
firstBodyLine = lines[i];
|
|
6004
|
-
break;
|
|
6005
|
-
}
|
|
6006
|
-
}
|
|
6007
|
-
const verbMatch = /^(\w+)[(!]/.exec(subject) ?? /^(\w+):/.exec(subject);
|
|
6008
|
-
const verb = verbMatch ? verbMatch[1].toLowerCase() : "";
|
|
6009
|
-
const searchText = subject + (firstBodyLine ? "\n" + firstBodyLine : "");
|
|
6010
|
-
const results = [];
|
|
6011
|
-
const seen = /* @__PURE__ */ new Set();
|
|
6012
|
-
let m;
|
|
6013
|
-
ID_PATTERN.lastIndex = 0;
|
|
6014
|
-
while ((m = ID_PATTERN.exec(searchText)) !== null) {
|
|
6015
|
-
const rawId = m[0];
|
|
6016
|
-
const id = normalizeId(rawId);
|
|
6017
|
-
if (seen.has(id)) continue;
|
|
6018
|
-
seen.add(id);
|
|
6019
|
-
const type = idType(id);
|
|
6020
|
-
if (!type) continue;
|
|
6021
|
-
results.push({ verb, id, type });
|
|
6022
|
-
}
|
|
6023
|
-
return results;
|
|
6024
|
-
}
|
|
6025
|
-
function findArtifactFile(deliveryRoot, id) {
|
|
6026
|
-
const prefix = `${id}_`;
|
|
6027
|
-
const dirs = [
|
|
6028
|
-
{ rel: "pending-sync", inArchive: false },
|
|
6029
|
-
{ rel: "archive", inArchive: true }
|
|
6030
|
-
];
|
|
6031
|
-
for (const { rel, inArchive } of dirs) {
|
|
6032
|
-
const dir = path30.join(deliveryRoot, rel);
|
|
6033
|
-
let entries;
|
|
6034
|
-
try {
|
|
6035
|
-
entries = fs30.readdirSync(dir);
|
|
6036
|
-
} catch {
|
|
6037
|
-
continue;
|
|
6038
|
-
}
|
|
6039
|
-
const match = entries.find(
|
|
6040
|
-
(e) => (e.startsWith(prefix) || e === `${id}.md`) && e.endsWith(".md")
|
|
6041
|
-
);
|
|
6042
|
-
if (match) {
|
|
6043
|
-
const absPath = path30.join(dir, match);
|
|
6044
|
-
return { absPath, inArchive, relPath: `${rel}/${match}` };
|
|
6045
|
-
}
|
|
6046
|
-
}
|
|
6047
|
-
return null;
|
|
6048
|
-
}
|
|
6049
|
-
function readArtifactStatus(absPath) {
|
|
6050
|
-
let raw;
|
|
6051
|
-
try {
|
|
6052
|
-
raw = fs30.readFileSync(absPath, "utf8");
|
|
6053
|
-
} catch {
|
|
6054
|
-
return { status: null, carryOver: false };
|
|
6055
|
-
}
|
|
6056
|
-
try {
|
|
6057
|
-
const { fm } = parseFrontmatter(raw);
|
|
6058
|
-
const status = typeof fm["status"] === "string" ? fm["status"] : null;
|
|
6059
|
-
const carryOver = fm["carry_over"] === true;
|
|
6060
|
-
return { status, carryOver };
|
|
6061
|
-
} catch {
|
|
6062
|
-
return { status: null, carryOver: false };
|
|
6063
|
-
}
|
|
6064
|
-
}
|
|
6065
|
-
function reconcileLifecycle(opts) {
|
|
6066
|
-
const { since, until = /* @__PURE__ */ new Date(), deliveryRoot, repoRoot } = opts;
|
|
6067
|
-
const gitRunner = opts.gitRunner ?? ((cmd, args) => {
|
|
6068
|
-
const result = spawnSync10(cmd, args, { encoding: "utf8", cwd: repoRoot });
|
|
6069
|
-
return result.stdout ?? "";
|
|
6070
|
-
});
|
|
6071
|
-
const sinceIso = since.toISOString();
|
|
6072
|
-
const untilIso = until.toISOString();
|
|
6073
|
-
const logOutput = gitRunner("git", [
|
|
6074
|
-
"log",
|
|
6075
|
-
`--after=${sinceIso}`,
|
|
6076
|
-
`--before=${untilIso}`,
|
|
6077
|
-
"--format=%H%x00%s%x00%b%x00---COMMIT---",
|
|
6078
|
-
"--"
|
|
6079
|
-
]);
|
|
6080
|
-
const idToItem = /* @__PURE__ */ new Map();
|
|
6081
|
-
const cleanIds = /* @__PURE__ */ new Set();
|
|
6082
|
-
if (logOutput.trim()) {
|
|
6083
|
-
const rawCommits = logOutput.split("---COMMIT---\n").filter((c) => c.trim());
|
|
6084
|
-
for (const raw of rawCommits) {
|
|
6085
|
-
const [sha = "", subject = "", body = ""] = raw.split("\0");
|
|
6086
|
-
const trimSha = sha.trim();
|
|
6087
|
-
const trimSubject = subject.trim();
|
|
6088
|
-
const trimBody = body.trim();
|
|
6089
|
-
if (!trimSha || !trimSubject) continue;
|
|
6090
|
-
const commitMsg = trimSubject + (trimBody ? "\n\n" + trimBody : "");
|
|
6091
|
-
const parsed = parseCommitMessage(commitMsg);
|
|
6092
|
-
for (const { verb, id, type } of parsed) {
|
|
6093
|
-
if (verb === "merge" || verb === "chore" || verb === "docs" || verb === "refactor" || verb === "test" || verb === "file" || verb === "plan") {
|
|
6094
|
-
continue;
|
|
6095
|
-
}
|
|
6096
|
-
if (type === "PROPOSAL") continue;
|
|
6097
|
-
const verbConfig = VERB_STATUS_MAP[verb];
|
|
6098
|
-
if (!verbConfig) continue;
|
|
6099
|
-
const found = findArtifactFile(deliveryRoot, id);
|
|
6100
|
-
if (!found) {
|
|
6101
|
-
continue;
|
|
6102
|
-
}
|
|
6103
|
-
const { status, carryOver } = readArtifactStatus(found.absPath);
|
|
6104
|
-
if (carryOver) continue;
|
|
6105
|
-
let expectedStatuses;
|
|
6106
|
-
if (verb === "feat" && type === "BUG") {
|
|
6107
|
-
expectedStatuses = ["Verified", "Done", "Completed"];
|
|
6108
|
-
} else if (!verbConfig.types.includes(type)) {
|
|
6109
|
-
continue;
|
|
6110
|
-
} else {
|
|
6111
|
-
expectedStatuses = verbConfig.expected;
|
|
6112
|
-
}
|
|
6113
|
-
const isTerminal = status !== null && expectedStatuses.includes(status);
|
|
6114
|
-
const isArchived = found.inArchive;
|
|
6115
|
-
if (isTerminal && isArchived) {
|
|
6116
|
-
cleanIds.add(id);
|
|
6117
|
-
idToItem.delete(id);
|
|
6118
|
-
} else if (!idToItem.has(id)) {
|
|
6119
|
-
const expectedStr = expectedStatuses[0] ?? "Done";
|
|
6120
|
-
idToItem.set(id, {
|
|
6121
|
-
id,
|
|
6122
|
-
type,
|
|
6123
|
-
expected_status: expectedStr,
|
|
6124
|
-
actual_status: status,
|
|
6125
|
-
file_path: found.relPath,
|
|
6126
|
-
in_archive: isArchived,
|
|
6127
|
-
commit_shas: [trimSha],
|
|
6128
|
-
carry_over: carryOver
|
|
6129
|
-
});
|
|
6130
|
-
} else {
|
|
6131
|
-
const existing = idToItem.get(id);
|
|
6132
|
-
if (!existing.commit_shas.includes(trimSha)) {
|
|
6133
|
-
existing.commit_shas.push(trimSha);
|
|
6134
|
-
}
|
|
6135
|
-
}
|
|
6136
|
-
}
|
|
6137
|
-
}
|
|
6138
|
-
}
|
|
6139
|
-
for (const id of cleanIds) {
|
|
6140
|
-
idToItem.delete(id);
|
|
6141
|
-
}
|
|
6142
|
-
const drift = Array.from(idToItem.values());
|
|
6143
|
-
return { drift, clean: cleanIds.size };
|
|
6144
|
-
}
|
|
6145
|
-
function reconcileDecomposition(opts) {
|
|
6146
|
-
const { sprintPlanPath, deliveryRoot } = opts;
|
|
6147
|
-
let raw;
|
|
6148
|
-
try {
|
|
6149
|
-
raw = fs30.readFileSync(sprintPlanPath, "utf8");
|
|
6150
|
-
} catch {
|
|
6151
|
-
return { missing: [], clean: 0 };
|
|
6152
|
-
}
|
|
6153
|
-
let fm;
|
|
6154
|
-
try {
|
|
6155
|
-
({ fm } = parseFrontmatter(raw));
|
|
6156
|
-
} catch {
|
|
6157
|
-
return { missing: [], clean: 0 };
|
|
6158
|
-
}
|
|
6159
|
-
const epics = Array.isArray(fm["epics"]) ? fm["epics"].map(String) : [];
|
|
6160
|
-
const proposals = Array.isArray(fm["proposals"]) ? fm["proposals"].map(String) : [];
|
|
6161
|
-
const pendingDir = path30.join(deliveryRoot, "pending-sync");
|
|
6162
|
-
const archiveDir = path30.join(deliveryRoot, "archive");
|
|
6163
|
-
function listMdFiles(dir) {
|
|
6164
|
-
try {
|
|
6165
|
-
return fs30.readdirSync(dir).filter((f) => f.endsWith(".md"));
|
|
6166
|
-
} catch {
|
|
6167
|
-
return [];
|
|
6168
|
-
}
|
|
6169
|
-
}
|
|
6170
|
-
const pendingFiles = listMdFiles(pendingDir);
|
|
6171
|
-
const archiveFiles = listMdFiles(archiveDir);
|
|
6172
|
-
const allFiles = [...pendingFiles, ...archiveFiles];
|
|
6173
|
-
const missing = [];
|
|
6174
|
-
let clean = 0;
|
|
6175
|
-
for (const epicId of epics) {
|
|
6176
|
-
const epicFile = allFiles.find(
|
|
6177
|
-
(f) => f.startsWith(`${epicId}_`) || f === `${epicId}.md`
|
|
6178
|
-
);
|
|
6179
|
-
if (!epicFile) {
|
|
6180
|
-
missing.push({
|
|
6181
|
-
id: epicId,
|
|
6182
|
-
type: "epic",
|
|
6183
|
-
reason: "file-missing",
|
|
6184
|
-
expected_files: [`pending-sync/${epicId}_<name>.md`]
|
|
6185
|
-
});
|
|
6186
|
-
continue;
|
|
6187
|
-
}
|
|
6188
|
-
const childStories = findChildStories(
|
|
6189
|
-
epicId,
|
|
6190
|
-
pendingDir,
|
|
6191
|
-
pendingFiles,
|
|
6192
|
-
archiveDir,
|
|
6193
|
-
archiveFiles
|
|
6194
|
-
);
|
|
6195
|
-
if (childStories.length === 0) {
|
|
6196
|
-
missing.push({
|
|
6197
|
-
id: epicId,
|
|
6198
|
-
type: "epic",
|
|
6199
|
-
reason: "no-child-stories",
|
|
6200
|
-
expected_files: [
|
|
6201
|
-
`pending-sync/${epicId.replace("EPIC-", "STORY-")}-01_<name>.md`
|
|
6202
|
-
]
|
|
6203
|
-
});
|
|
6204
|
-
} else {
|
|
6205
|
-
clean++;
|
|
6206
|
-
}
|
|
6207
|
-
}
|
|
6208
|
-
for (const proposalId of proposals) {
|
|
6209
|
-
const decomposedEpic = findDecomposedEpic(
|
|
6210
|
-
proposalId,
|
|
6211
|
-
pendingDir,
|
|
6212
|
-
pendingFiles
|
|
6213
|
-
);
|
|
6214
|
-
if (!decomposedEpic) {
|
|
6215
|
-
missing.push({
|
|
6216
|
-
id: proposalId,
|
|
6217
|
-
type: "proposal",
|
|
6218
|
-
reason: "no-decomposed-epic",
|
|
6219
|
-
expected_files: [`pending-sync/EPIC-<NNN>_<name>.md with context_source citing ${proposalId}`]
|
|
6220
|
-
});
|
|
6221
|
-
} else {
|
|
6222
|
-
clean++;
|
|
6223
|
-
}
|
|
6224
|
-
}
|
|
6225
|
-
return { missing, clean };
|
|
6226
|
-
}
|
|
6227
|
-
function findChildStories(epicId, pendingDir, pendingFiles, archiveDir, archiveFiles) {
|
|
6228
|
-
const results = [];
|
|
6229
|
-
const epicNumMatch = /^EPIC-(\d+)$/.exec(epicId);
|
|
6230
|
-
if (!epicNumMatch) return results;
|
|
6231
|
-
const epicNum = epicNumMatch[1];
|
|
6232
|
-
const storyPrefix = `STORY-${epicNum}-`;
|
|
6233
|
-
for (const [files, dir] of [[pendingFiles, pendingDir], [archiveFiles, archiveDir]]) {
|
|
6234
|
-
for (const f of files) {
|
|
6235
|
-
if (!f.startsWith(storyPrefix) && !f.startsWith("STORY-")) continue;
|
|
6236
|
-
if (!f.includes(storyPrefix)) continue;
|
|
6237
|
-
const absPath = path30.join(dir, f);
|
|
6238
|
-
try {
|
|
6239
|
-
const raw = fs30.readFileSync(absPath, "utf8");
|
|
6240
|
-
const { fm } = parseFrontmatter(raw);
|
|
6241
|
-
const parentRef = fm["parent_epic_ref"];
|
|
6242
|
-
if (parentRef === epicId) {
|
|
6243
|
-
results.push(f);
|
|
6244
|
-
}
|
|
6245
|
-
} catch {
|
|
6246
|
-
}
|
|
6247
|
-
}
|
|
6248
|
-
}
|
|
6249
|
-
return results;
|
|
6250
|
-
}
|
|
6251
|
-
function findDecomposedEpic(proposalId, pendingDir, pendingFiles) {
|
|
6252
|
-
for (const f of pendingFiles) {
|
|
6253
|
-
if (!f.startsWith("EPIC-")) continue;
|
|
6254
|
-
const absPath = path30.join(pendingDir, f);
|
|
6255
|
-
try {
|
|
6256
|
-
const raw = fs30.readFileSync(absPath, "utf8");
|
|
6257
|
-
const { fm } = parseFrontmatter(raw);
|
|
6258
|
-
const contextSource = fm["context_source"];
|
|
6259
|
-
if (typeof contextSource === "string" && contextSource.includes(proposalId)) {
|
|
6260
|
-
return f;
|
|
6261
|
-
}
|
|
6262
|
-
} catch {
|
|
6263
|
-
}
|
|
6264
|
-
}
|
|
6265
|
-
return null;
|
|
6266
|
-
}
|
|
6267
|
-
function checkVerbMismatch(verb, type) {
|
|
6268
|
-
if (verb === "feat" && type === "BUG") {
|
|
6269
|
-
return `verb 'feat' unusual for BUG; expected 'fix'`;
|
|
6270
|
-
}
|
|
6271
|
-
if (verb === "fix" && (type === "STORY" || type === "EPIC" || type === "CR")) {
|
|
6272
|
-
return `verb 'fix' unusual for ${type}; expected 'feat'`;
|
|
6273
|
-
}
|
|
6274
|
-
return null;
|
|
6275
|
-
}
|
|
6276
|
-
|
|
6277
|
-
// src/commands/sprint.ts
|
|
6095
|
+
import * as path31 from "path";
|
|
6096
|
+
import { spawnSync as spawnSync10, execSync as execSync2 } from "child_process";
|
|
6097
|
+
import yaml6 from "js-yaml";
|
|
6278
6098
|
var TERMINAL_STATUSES2 = /* @__PURE__ */ new Set(["Completed", "Done", "Abandoned", "Closed", "Resolved"]);
|
|
6279
6099
|
function resolveRunScript(opts) {
|
|
6280
6100
|
if (opts.runScriptPath) return opts.runScriptPath;
|
|
@@ -6288,7 +6108,7 @@ function sprintInitHandler(opts, cli) {
|
|
|
6288
6108
|
const stdoutFn = cli?.stdout ?? ((s) => process.stdout.write(s + "\n"));
|
|
6289
6109
|
const stderrFn = cli?.stderr ?? ((s) => process.stderr.write(s + "\n"));
|
|
6290
6110
|
const exitFn = cli?.exit ?? defaultExit;
|
|
6291
|
-
const spawnFn = cli?.spawnFn ??
|
|
6111
|
+
const spawnFn = cli?.spawnFn ?? spawnSync10;
|
|
6292
6112
|
const cwd = cli?.cwd ?? process.cwd();
|
|
6293
6113
|
const allowDrift = opts.allowDrift ?? cli?.allowDrift ?? false;
|
|
6294
6114
|
const mode = readSprintExecutionMode(opts.sprintId, {
|
|
@@ -6303,13 +6123,13 @@ function sprintInitHandler(opts, cli) {
|
|
|
6303
6123
|
let sprintPlanPath = null;
|
|
6304
6124
|
const pendingDir = path31.join(deliveryRoot, "pending-sync");
|
|
6305
6125
|
try {
|
|
6306
|
-
const entries =
|
|
6126
|
+
const entries = fs30.readdirSync(pendingDir);
|
|
6307
6127
|
const sprintFile = entries.find(
|
|
6308
6128
|
(e) => (e.startsWith(`${opts.sprintId}_`) || e === `${opts.sprintId}.md`) && e.endsWith(".md")
|
|
6309
6129
|
);
|
|
6310
6130
|
if (sprintFile) {
|
|
6311
6131
|
sprintPlanPath = path31.join(pendingDir, sprintFile);
|
|
6312
|
-
const raw =
|
|
6132
|
+
const raw = fs30.readFileSync(sprintPlanPath, "utf8");
|
|
6313
6133
|
const { fm } = parseFileFrontmatter(raw);
|
|
6314
6134
|
if (fm["lifecycle_init_mode"] === "block") {
|
|
6315
6135
|
lifecycleInitMode = "block";
|
|
@@ -6343,7 +6163,7 @@ function sprintInitHandler(opts, cli) {
|
|
|
6343
6163
|
stderrFn("[cleargate sprint init] lifecycle drift waived via --allow-drift flag");
|
|
6344
6164
|
if (sprintPlanPath) {
|
|
6345
6165
|
try {
|
|
6346
|
-
const rawSprint =
|
|
6166
|
+
const rawSprint = fs30.readFileSync(sprintPlanPath, "utf8");
|
|
6347
6167
|
const { fm, body } = parseFileFrontmatter(rawSprint);
|
|
6348
6168
|
const waiverLine = `lifecycle waiver: ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]} for ${lifecycleResult.drift.map((d) => d.id).join(", ")}`;
|
|
6349
6169
|
const currentContextSource = typeof fm["context_source"] === "string" ? fm["context_source"] : "";
|
|
@@ -6392,8 +6212,12 @@ ${waiverLine}` : waiverLine;
|
|
|
6392
6212
|
}
|
|
6393
6213
|
}
|
|
6394
6214
|
const runScript = resolveRunScript(cli ?? {});
|
|
6395
|
-
const
|
|
6396
|
-
const result = spawnFn(
|
|
6215
|
+
const scriptPath = resolveCleargateScript({ cwd }, "init_sprint.mjs");
|
|
6216
|
+
const result = spawnFn(
|
|
6217
|
+
"bash",
|
|
6218
|
+
[runScript, "node", scriptPath, opts.sprintId, "--stories", opts.stories],
|
|
6219
|
+
{ stdio: "inherit" }
|
|
6220
|
+
);
|
|
6397
6221
|
if (result.error) {
|
|
6398
6222
|
stderrFn(`[cleargate sprint init] error: ${result.error.message}`);
|
|
6399
6223
|
return exitFn(1);
|
|
@@ -6408,7 +6232,7 @@ function sprintCloseHandler(opts, cli) {
|
|
|
6408
6232
|
const stdoutFn = cli?.stdout ?? ((s) => process.stdout.write(s + "\n"));
|
|
6409
6233
|
const stderrFn = cli?.stderr ?? ((s) => process.stderr.write(s + "\n"));
|
|
6410
6234
|
const exitFn = cli?.exit ?? defaultExit;
|
|
6411
|
-
const spawnFn = cli?.spawnFn ??
|
|
6235
|
+
const spawnFn = cli?.spawnFn ?? spawnSync10;
|
|
6412
6236
|
const mode = readSprintExecutionMode(opts.sprintId, {
|
|
6413
6237
|
sprintFilePath: cli?.sprintFilePath,
|
|
6414
6238
|
cwd: cli?.cwd
|
|
@@ -6417,11 +6241,13 @@ function sprintCloseHandler(opts, cli) {
|
|
|
6417
6241
|
return printInertAndExit(stdoutFn, exitFn);
|
|
6418
6242
|
}
|
|
6419
6243
|
const runScript = resolveRunScript(cli ?? {});
|
|
6420
|
-
const
|
|
6244
|
+
const closeCwd = cli?.cwd ?? process.cwd();
|
|
6245
|
+
const closeScriptPath = resolveCleargateScript({ cwd: closeCwd }, "close_sprint.mjs");
|
|
6246
|
+
const closeArgs = [runScript, "node", closeScriptPath, opts.sprintId];
|
|
6421
6247
|
if (opts.assumeAck === true) {
|
|
6422
|
-
|
|
6248
|
+
closeArgs.push("--assume-ack");
|
|
6423
6249
|
}
|
|
6424
|
-
const result = spawnFn("bash",
|
|
6250
|
+
const result = spawnFn("bash", closeArgs, { stdio: "inherit" });
|
|
6425
6251
|
if (result.error) {
|
|
6426
6252
|
stderrFn(`[cleargate sprint close] error: ${result.error.message}`);
|
|
6427
6253
|
return exitFn(1);
|
|
@@ -6442,12 +6268,12 @@ function reconcileLifecycleCliHandler(opts, cli) {
|
|
|
6442
6268
|
} else {
|
|
6443
6269
|
try {
|
|
6444
6270
|
const pendingDir = path31.join(deliveryRoot, "pending-sync");
|
|
6445
|
-
const entries =
|
|
6271
|
+
const entries = fs30.readdirSync(pendingDir);
|
|
6446
6272
|
const sprintFile = entries.find(
|
|
6447
6273
|
(e) => (e.startsWith(`${opts.sprintId}_`) || e === `${opts.sprintId}.md`) && e.endsWith(".md")
|
|
6448
6274
|
);
|
|
6449
6275
|
if (sprintFile) {
|
|
6450
|
-
const raw =
|
|
6276
|
+
const raw = fs30.readFileSync(path31.join(pendingDir, sprintFile), "utf8");
|
|
6451
6277
|
const { fm } = parseFileFrontmatter(raw);
|
|
6452
6278
|
const startDate = fm["start_date"];
|
|
6453
6279
|
since = typeof startDate === "string" ? new Date(startDate) : new Date(Date.now() - 90 * 24 * 60 * 60 * 1e3);
|
|
@@ -6504,7 +6330,7 @@ function parseFileFrontmatter(raw) {
|
|
|
6504
6330
|
if (yamlText.trim() === "") return { fm: {}, body };
|
|
6505
6331
|
let parsed;
|
|
6506
6332
|
try {
|
|
6507
|
-
parsed =
|
|
6333
|
+
parsed = yaml6.load(yamlText, { schema: yaml6.CORE_SCHEMA });
|
|
6508
6334
|
} catch {
|
|
6509
6335
|
return { fm: {}, body };
|
|
6510
6336
|
}
|
|
@@ -6512,8 +6338,8 @@ function parseFileFrontmatter(raw) {
|
|
|
6512
6338
|
return { fm: parsed, body };
|
|
6513
6339
|
}
|
|
6514
6340
|
function serializeFileContent(fm, body) {
|
|
6515
|
-
const yamlBody =
|
|
6516
|
-
schema:
|
|
6341
|
+
const yamlBody = yaml6.dump(fm, {
|
|
6342
|
+
schema: yaml6.CORE_SCHEMA,
|
|
6517
6343
|
lineWidth: -1,
|
|
6518
6344
|
noRefs: true,
|
|
6519
6345
|
noCompatMode: true,
|
|
@@ -6528,8 +6354,8 @@ ${body}`;
|
|
|
6528
6354
|
}
|
|
6529
6355
|
function atomicWriteStr(filePath, content) {
|
|
6530
6356
|
const tmp = `${filePath}.tmp.${process.pid}`;
|
|
6531
|
-
|
|
6532
|
-
|
|
6357
|
+
fs30.writeFileSync(tmp, content, "utf8");
|
|
6358
|
+
fs30.renameSync(tmp, filePath);
|
|
6533
6359
|
}
|
|
6534
6360
|
function deriveSprintBranchForArchive(sprintId) {
|
|
6535
6361
|
const match = /^SPRINT-(\d+)/.exec(sprintId);
|
|
@@ -6543,7 +6369,7 @@ function stampFile(raw, status, completedAt) {
|
|
|
6543
6369
|
return serializeFileContent(fm, body);
|
|
6544
6370
|
}
|
|
6545
6371
|
function stampSprintClose(sprintPath, now) {
|
|
6546
|
-
const previousContent =
|
|
6372
|
+
const previousContent = fs30.readFileSync(sprintPath, "utf8");
|
|
6547
6373
|
const { fm, body } = parseFileFrontmatter(previousContent);
|
|
6548
6374
|
const currentStatus = typeof fm["status"] === "string" ? fm["status"] : "";
|
|
6549
6375
|
const alreadyTerminal = TERMINAL_STATUSES2.has(currentStatus);
|
|
@@ -6570,7 +6396,7 @@ async function sprintArchiveHandler(opts, cli) {
|
|
|
6570
6396
|
const stdoutFn = cli?.stdout ?? ((s) => process.stdout.write(s + "\n"));
|
|
6571
6397
|
const stderrFn = cli?.stderr ?? ((s) => process.stderr.write(s + "\n"));
|
|
6572
6398
|
const exitFn = cli?.exit ?? defaultExit;
|
|
6573
|
-
const spawnFn = cli?.spawnFn ??
|
|
6399
|
+
const spawnFn = cli?.spawnFn ?? spawnSync10;
|
|
6574
6400
|
const cwd = cli?.cwd ?? process.cwd();
|
|
6575
6401
|
const wikiBuildFn = cli?.wikiBuildFn ?? (async (wCwd, wStdout) => {
|
|
6576
6402
|
const fakeExit = (code) => {
|
|
@@ -6604,13 +6430,13 @@ async function sprintArchiveHandler(opts, cli) {
|
|
|
6604
6430
|
return printInertAndExit(stdoutFn, exitFn);
|
|
6605
6431
|
}
|
|
6606
6432
|
const stateFile = path31.join(cwd, ".cleargate", "sprint-runs", opts.sprintId, "state.json");
|
|
6607
|
-
if (!
|
|
6433
|
+
if (!fs30.existsSync(stateFile)) {
|
|
6608
6434
|
stderrFn(`[cleargate sprint archive] state.json not found at ${stateFile}`);
|
|
6609
6435
|
return exitFn(1);
|
|
6610
6436
|
}
|
|
6611
6437
|
let state2;
|
|
6612
6438
|
try {
|
|
6613
|
-
state2 = JSON.parse(
|
|
6439
|
+
state2 = JSON.parse(fs30.readFileSync(stateFile, "utf8"));
|
|
6614
6440
|
} catch (err) {
|
|
6615
6441
|
stderrFn(`[cleargate sprint archive] failed to parse state.json: ${err.message}`);
|
|
6616
6442
|
return exitFn(1);
|
|
@@ -6625,15 +6451,15 @@ async function sprintArchiveHandler(opts, cli) {
|
|
|
6625
6451
|
const pendingDir = path31.join(cwd, ".cleargate", "delivery", "pending-sync");
|
|
6626
6452
|
const archiveDir = path31.join(cwd, ".cleargate", "delivery", "archive");
|
|
6627
6453
|
let sprintFile = null;
|
|
6628
|
-
for (const entry of
|
|
6454
|
+
for (const entry of fs30.readdirSync(pendingDir)) {
|
|
6629
6455
|
if ((entry.startsWith(`${opts.sprintId}_`) || entry === `${opts.sprintId}.md`) && entry.endsWith(".md")) {
|
|
6630
6456
|
sprintFile = path31.join(pendingDir, entry);
|
|
6631
6457
|
break;
|
|
6632
6458
|
}
|
|
6633
6459
|
}
|
|
6634
6460
|
let epicIds = [];
|
|
6635
|
-
if (sprintFile &&
|
|
6636
|
-
const { fm } = parseFileFrontmatter(
|
|
6461
|
+
if (sprintFile && fs30.existsSync(sprintFile)) {
|
|
6462
|
+
const { fm } = parseFileFrontmatter(fs30.readFileSync(sprintFile, "utf8"));
|
|
6637
6463
|
const epics = fm["epics"];
|
|
6638
6464
|
if (Array.isArray(epics)) {
|
|
6639
6465
|
epicIds = epics.map(String);
|
|
@@ -6648,7 +6474,7 @@ async function sprintArchiveHandler(opts, cli) {
|
|
|
6648
6474
|
});
|
|
6649
6475
|
}
|
|
6650
6476
|
for (const epicId of epicIds) {
|
|
6651
|
-
for (const entry of
|
|
6477
|
+
for (const entry of fs30.readdirSync(pendingDir)) {
|
|
6652
6478
|
if ((entry.startsWith(`${epicId}_`) || entry === `${epicId}.md`) && entry.endsWith(".md")) {
|
|
6653
6479
|
plan.push({
|
|
6654
6480
|
src: path31.join(pendingDir, entry),
|
|
@@ -6660,7 +6486,7 @@ async function sprintArchiveHandler(opts, cli) {
|
|
|
6660
6486
|
}
|
|
6661
6487
|
const storyKeys = storyKeysForEpic(stateStories, epicId);
|
|
6662
6488
|
for (const storyId of storyKeys) {
|
|
6663
|
-
for (const entry of
|
|
6489
|
+
for (const entry of fs30.readdirSync(pendingDir)) {
|
|
6664
6490
|
if ((entry.startsWith(`${storyId}_`) || entry === `${storyId}.md`) && entry.endsWith(".md")) {
|
|
6665
6491
|
plan.push({
|
|
6666
6492
|
src: path31.join(pendingDir, entry),
|
|
@@ -6675,13 +6501,13 @@ async function sprintArchiveHandler(opts, cli) {
|
|
|
6675
6501
|
const storyIdsInState = new Set(Object.keys(stateStories));
|
|
6676
6502
|
const planSrcs = new Set(plan.map((p) => p.src));
|
|
6677
6503
|
const orphans = [];
|
|
6678
|
-
for (const entry of
|
|
6504
|
+
for (const entry of fs30.readdirSync(pendingDir)) {
|
|
6679
6505
|
if (!entry.startsWith("STORY-") || !entry.endsWith(".md")) continue;
|
|
6680
6506
|
const candidate = path31.join(pendingDir, entry);
|
|
6681
6507
|
if (planSrcs.has(candidate)) continue;
|
|
6682
6508
|
let raw;
|
|
6683
6509
|
try {
|
|
6684
|
-
raw =
|
|
6510
|
+
raw = fs30.readFileSync(candidate, "utf8");
|
|
6685
6511
|
} catch {
|
|
6686
6512
|
continue;
|
|
6687
6513
|
}
|
|
@@ -6719,8 +6545,8 @@ async function sprintArchiveHandler(opts, cli) {
|
|
|
6719
6545
|
}
|
|
6720
6546
|
let sprintFileSnapshot = null;
|
|
6721
6547
|
const wikiRoot = path31.join(cwd, ".cleargate", "wiki");
|
|
6722
|
-
const wikiInitialised =
|
|
6723
|
-
if (sprintFile &&
|
|
6548
|
+
const wikiInitialised = fs30.existsSync(wikiRoot);
|
|
6549
|
+
if (sprintFile && fs30.existsSync(sprintFile)) {
|
|
6724
6550
|
const { previousContent } = stampSprintClose(sprintFile, () => completedAt);
|
|
6725
6551
|
sprintFileSnapshot = previousContent;
|
|
6726
6552
|
if (wikiInitialised) {
|
|
@@ -6747,15 +6573,15 @@ async function sprintArchiveHandler(opts, cli) {
|
|
|
6747
6573
|
}
|
|
6748
6574
|
}
|
|
6749
6575
|
for (const entry of plan) {
|
|
6750
|
-
if (!
|
|
6576
|
+
if (!fs30.existsSync(entry.src)) {
|
|
6751
6577
|
stderrFn(`[cleargate sprint archive] source not found: ${entry.src} \u2014 skipping`);
|
|
6752
6578
|
continue;
|
|
6753
6579
|
}
|
|
6754
|
-
const raw =
|
|
6580
|
+
const raw = fs30.readFileSync(entry.src, "utf8");
|
|
6755
6581
|
const stamped = stampFile(raw, entry.status, completedAt);
|
|
6756
6582
|
const dest = path31.join(archiveDir, entry.destName);
|
|
6757
6583
|
atomicWriteStr(entry.src, stamped);
|
|
6758
|
-
|
|
6584
|
+
fs30.renameSync(entry.src, dest);
|
|
6759
6585
|
stdoutFn(`archived: ${entry.destName}`);
|
|
6760
6586
|
}
|
|
6761
6587
|
try {
|
|
@@ -6819,9 +6645,9 @@ function checkPrevSprintCompleted(sprintId, cwd) {
|
|
|
6819
6645
|
let resolvedPrevId = prevId;
|
|
6820
6646
|
for (const pid of [prevId, prevIdAlt]) {
|
|
6821
6647
|
const stateFile = path31.join(sprintRunsBase, pid, "state.json");
|
|
6822
|
-
if (
|
|
6648
|
+
if (fs30.existsSync(stateFile)) {
|
|
6823
6649
|
try {
|
|
6824
|
-
const raw =
|
|
6650
|
+
const raw = fs30.readFileSync(stateFile, "utf8");
|
|
6825
6651
|
stateJson = JSON.parse(raw);
|
|
6826
6652
|
resolvedPrevId = pid;
|
|
6827
6653
|
break;
|
|
@@ -6928,10 +6754,10 @@ function findSprintFile(sprintId, cwd) {
|
|
|
6928
6754
|
path31.join(cwd, ".cleargate", "delivery", "archive")
|
|
6929
6755
|
];
|
|
6930
6756
|
for (const dir of searchDirs) {
|
|
6931
|
-
if (!
|
|
6757
|
+
if (!fs30.existsSync(dir)) continue;
|
|
6932
6758
|
let entries;
|
|
6933
6759
|
try {
|
|
6934
|
-
entries =
|
|
6760
|
+
entries = fs30.readdirSync(dir);
|
|
6935
6761
|
} catch {
|
|
6936
6762
|
continue;
|
|
6937
6763
|
}
|
|
@@ -6953,7 +6779,7 @@ function findWorkItemFileLocal(cwd, workItemId) {
|
|
|
6953
6779
|
for (const dir of searchDirs) {
|
|
6954
6780
|
let entries;
|
|
6955
6781
|
try {
|
|
6956
|
-
entries =
|
|
6782
|
+
entries = fs30.readdirSync(dir);
|
|
6957
6783
|
} catch {
|
|
6958
6784
|
continue;
|
|
6959
6785
|
}
|
|
@@ -6965,7 +6791,7 @@ function findWorkItemFileLocal(cwd, workItemId) {
|
|
|
6965
6791
|
function readCachedGateSync(absPath) {
|
|
6966
6792
|
let raw;
|
|
6967
6793
|
try {
|
|
6968
|
-
raw =
|
|
6794
|
+
raw = fs30.readFileSync(absPath, "utf8");
|
|
6969
6795
|
} catch {
|
|
6970
6796
|
return null;
|
|
6971
6797
|
}
|
|
@@ -7041,7 +6867,7 @@ function checkPerItemReadinessGates(sprintId, cwd, execFn, mode) {
|
|
|
7041
6867
|
let fm;
|
|
7042
6868
|
let raw;
|
|
7043
6869
|
try {
|
|
7044
|
-
raw =
|
|
6870
|
+
raw = fs30.readFileSync(absPath, "utf8");
|
|
7045
6871
|
({ fm } = parseFrontmatter(raw));
|
|
7046
6872
|
} catch {
|
|
7047
6873
|
totalChecked++;
|
|
@@ -7085,7 +6911,7 @@ function checkPerItemReadinessGates(sprintId, cwd, execFn, mode) {
|
|
|
7085
6911
|
let sprintRaw;
|
|
7086
6912
|
let sprintFm = {};
|
|
7087
6913
|
try {
|
|
7088
|
-
sprintRaw =
|
|
6914
|
+
sprintRaw = fs30.readFileSync(sprintFilePath, "utf8");
|
|
7089
6915
|
({ fm: sprintFm } = parseFrontmatter(sprintRaw));
|
|
7090
6916
|
} catch {
|
|
7091
6917
|
}
|
|
@@ -7126,6 +6952,41 @@ function checkPerItemReadinessGates(sprintId, cwd, execFn, mode) {
|
|
|
7126
6952
|
Run: cleargate gate check <file> -v for each`
|
|
7127
6953
|
};
|
|
7128
6954
|
}
|
|
6955
|
+
function refreshScopedGateCaches(sprintId, cwd, execFn) {
|
|
6956
|
+
const result = { refreshed: [], skipped: [], errors: [] };
|
|
6957
|
+
const sprintFilePath = findSprintFile(sprintId, cwd);
|
|
6958
|
+
if (!sprintFilePath) {
|
|
6959
|
+
return result;
|
|
6960
|
+
}
|
|
6961
|
+
const childIds = extractInScopeWorkItemIds(sprintFilePath, cwd, execFn);
|
|
6962
|
+
if (!childIds || childIds.length === 0) {
|
|
6963
|
+
return result;
|
|
6964
|
+
}
|
|
6965
|
+
for (const id of childIds) {
|
|
6966
|
+
const absPath = findWorkItemFileLocal(cwd, id);
|
|
6967
|
+
if (!absPath) {
|
|
6968
|
+
continue;
|
|
6969
|
+
}
|
|
6970
|
+
let status = "";
|
|
6971
|
+
try {
|
|
6972
|
+
const raw = fs30.readFileSync(absPath, "utf8");
|
|
6973
|
+
const { fm } = parseFrontmatter(raw);
|
|
6974
|
+
status = String(fm["status"] ?? "");
|
|
6975
|
+
} catch {
|
|
6976
|
+
}
|
|
6977
|
+
if (TERMINAL_STATUSES2.has(status)) {
|
|
6978
|
+
result.skipped.push(id);
|
|
6979
|
+
continue;
|
|
6980
|
+
}
|
|
6981
|
+
try {
|
|
6982
|
+
execFn(`cleargate gate check "${absPath}"`, { cwd, encoding: "utf8" });
|
|
6983
|
+
result.refreshed.push(id);
|
|
6984
|
+
} catch (err) {
|
|
6985
|
+
result.errors.push({ id, message: String(err) });
|
|
6986
|
+
}
|
|
6987
|
+
}
|
|
6988
|
+
return result;
|
|
6989
|
+
}
|
|
7129
6990
|
function emitPunchList(sprintId, results, stdoutFn, stderrFn) {
|
|
7130
6991
|
const failures = results.filter((r) => !r.pass && !r.skipped);
|
|
7131
6992
|
if (failures.length === 0) {
|
|
@@ -7157,6 +7018,13 @@ function sprintPreflightHandler(opts, cli) {
|
|
|
7157
7018
|
}
|
|
7158
7019
|
const execFn = cli?.execFn ?? ((cmd, execOpts) => execSync2(cmd, { ...execOpts, stdio: "pipe" }));
|
|
7159
7020
|
const mode = readSprintExecutionMode(opts.sprintId, { cwd });
|
|
7021
|
+
const refresh = refreshScopedGateCaches(opts.sprintId, cwd, execFn);
|
|
7022
|
+
stdoutFn(`Step 0: refreshed ${refresh.refreshed.length} items, ${refresh.errors.length} errors.
|
|
7023
|
+
`);
|
|
7024
|
+
for (const e of refresh.errors) {
|
|
7025
|
+
stdoutFn(` - ${e.id}: ${e.message}
|
|
7026
|
+
`);
|
|
7027
|
+
}
|
|
7160
7028
|
const results = [
|
|
7161
7029
|
checkPrevSprintCompleted(opts.sprintId, cwd),
|
|
7162
7030
|
checkNoLeftoverWorktrees(cwd, execFn),
|
|
@@ -7173,9 +7041,9 @@ function sprintPreflightHandler(opts, cli) {
|
|
|
7173
7041
|
}
|
|
7174
7042
|
|
|
7175
7043
|
// src/commands/story.ts
|
|
7176
|
-
import * as
|
|
7044
|
+
import * as fs31 from "fs";
|
|
7177
7045
|
import * as path32 from "path";
|
|
7178
|
-
import { spawnSync as
|
|
7046
|
+
import { spawnSync as spawnSync11 } from "child_process";
|
|
7179
7047
|
function defaultExit2(code) {
|
|
7180
7048
|
return process.exit(code);
|
|
7181
7049
|
}
|
|
@@ -7191,8 +7059,8 @@ function deriveSprintBranch(sprintId) {
|
|
|
7191
7059
|
}
|
|
7192
7060
|
function atomicWriteString(filePath, text) {
|
|
7193
7061
|
const tmpFile = `${filePath}.tmp.${process.pid}`;
|
|
7194
|
-
|
|
7195
|
-
|
|
7062
|
+
fs31.writeFileSync(tmpFile, text, "utf8");
|
|
7063
|
+
fs31.renameSync(tmpFile, filePath);
|
|
7196
7064
|
}
|
|
7197
7065
|
function stateJsonPath(cwd, sprintId) {
|
|
7198
7066
|
return path32.join(cwd, ".cleargate", "sprint-runs", sprintId, "state.json");
|
|
@@ -7201,7 +7069,7 @@ function storyStartHandler(opts, cli) {
|
|
|
7201
7069
|
const stdoutFn = cli?.stdout ?? ((s) => process.stdout.write(s + "\n"));
|
|
7202
7070
|
const stderrFn = cli?.stderr ?? ((s) => process.stderr.write(s + "\n"));
|
|
7203
7071
|
const exitFn = cli?.exit ?? defaultExit2;
|
|
7204
|
-
const spawnFn = cli?.spawnFn ??
|
|
7072
|
+
const spawnFn = cli?.spawnFn ?? spawnSync11;
|
|
7205
7073
|
const cwd = cli?.cwd ?? process.cwd();
|
|
7206
7074
|
const sprintId = cli?.sprintId ?? resolveSprintIdFromSentinel(cwd) ?? "SPRINT-UNKNOWN";
|
|
7207
7075
|
const mode = readSprintExecutionMode(sprintId, {
|
|
@@ -7229,9 +7097,10 @@ function storyStartHandler(opts, cli) {
|
|
|
7229
7097
|
return exitFn(step1.status ?? 1);
|
|
7230
7098
|
}
|
|
7231
7099
|
const runScript = resolveRunScript2(cli ?? { cwd });
|
|
7100
|
+
const updateStateScript = resolveCleargateScript({ cwd }, "update_state.mjs");
|
|
7232
7101
|
const step2 = spawnFn(
|
|
7233
7102
|
"bash",
|
|
7234
|
-
[runScript, "
|
|
7103
|
+
[runScript, "node", updateStateScript, opts.storyId, "Bouncing"],
|
|
7235
7104
|
{ stdio: "pipe", cwd, encoding: "utf8" }
|
|
7236
7105
|
);
|
|
7237
7106
|
if (step2.error) {
|
|
@@ -7244,13 +7113,13 @@ function storyStartHandler(opts, cli) {
|
|
|
7244
7113
|
return exitFn(step2.status ?? 1);
|
|
7245
7114
|
}
|
|
7246
7115
|
const stateFile = stateJsonPath(cwd, sprintId);
|
|
7247
|
-
if (!
|
|
7116
|
+
if (!fs31.existsSync(stateFile)) {
|
|
7248
7117
|
stderrFn(`[cleargate story start] step 3: state.json not found at ${stateFile}`);
|
|
7249
7118
|
return exitFn(1);
|
|
7250
7119
|
}
|
|
7251
7120
|
let state2;
|
|
7252
7121
|
try {
|
|
7253
|
-
state2 = JSON.parse(
|
|
7122
|
+
state2 = JSON.parse(fs31.readFileSync(stateFile, "utf8"));
|
|
7254
7123
|
} catch (err) {
|
|
7255
7124
|
stderrFn(`[cleargate story start] step 3: failed to parse state.json: ${err.message}`);
|
|
7256
7125
|
return exitFn(1);
|
|
@@ -7279,7 +7148,7 @@ function storyCompleteHandler(opts, cli) {
|
|
|
7279
7148
|
const stdoutFn = cli?.stdout ?? ((s) => process.stdout.write(s + "\n"));
|
|
7280
7149
|
const stderrFn = cli?.stderr ?? ((s) => process.stderr.write(s + "\n"));
|
|
7281
7150
|
const exitFn = cli?.exit ?? defaultExit2;
|
|
7282
|
-
const spawnFn = cli?.spawnFn ??
|
|
7151
|
+
const spawnFn = cli?.spawnFn ?? spawnSync11;
|
|
7283
7152
|
const cwd = cli?.cwd ?? process.cwd();
|
|
7284
7153
|
const sprintId = cli?.sprintId ?? resolveSprintIdFromSentinel(cwd) ?? "SPRINT-UNKNOWN";
|
|
7285
7154
|
const mode = readSprintExecutionMode(sprintId, {
|
|
@@ -7369,9 +7238,10 @@ function storyCompleteHandler(opts, cli) {
|
|
|
7369
7238
|
return exitFn(step5.status ?? 1);
|
|
7370
7239
|
}
|
|
7371
7240
|
const runScript = resolveRunScript2(cli ?? { cwd });
|
|
7241
|
+
const updateStateDoneScript = resolveCleargateScript({ cwd }, "update_state.mjs");
|
|
7372
7242
|
const step6 = spawnFn(
|
|
7373
7243
|
"bash",
|
|
7374
|
-
[runScript, "
|
|
7244
|
+
[runScript, "node", updateStateDoneScript, opts.storyId, "Done"],
|
|
7375
7245
|
{ stdio: "pipe", cwd, encoding: "utf8" }
|
|
7376
7246
|
);
|
|
7377
7247
|
if (step6.error) {
|
|
@@ -7389,7 +7259,7 @@ function storyCompleteHandler(opts, cli) {
|
|
|
7389
7259
|
|
|
7390
7260
|
// src/commands/state.ts
|
|
7391
7261
|
import * as path33 from "path";
|
|
7392
|
-
import { spawnSync as
|
|
7262
|
+
import { spawnSync as spawnSync12 } from "child_process";
|
|
7393
7263
|
function defaultExit3(code) {
|
|
7394
7264
|
return process.exit(code);
|
|
7395
7265
|
}
|
|
@@ -7402,7 +7272,7 @@ function stateUpdateHandler(opts, cli) {
|
|
|
7402
7272
|
const stdoutFn = cli?.stdout ?? ((s) => process.stdout.write(s + "\n"));
|
|
7403
7273
|
const stderrFn = cli?.stderr ?? ((s) => process.stderr.write(s + "\n"));
|
|
7404
7274
|
const exitFn = cli?.exit ?? defaultExit3;
|
|
7405
|
-
const spawnFn = cli?.spawnFn ??
|
|
7275
|
+
const spawnFn = cli?.spawnFn ?? spawnSync12;
|
|
7406
7276
|
const cwd = cli?.cwd;
|
|
7407
7277
|
const sprintId = cli?.sprintId ?? resolveSprintIdFromSentinel(cwd) ?? "SPRINT-UNKNOWN";
|
|
7408
7278
|
const mode = readSprintExecutionMode(sprintId, {
|
|
@@ -7413,9 +7283,11 @@ function stateUpdateHandler(opts, cli) {
|
|
|
7413
7283
|
return printInertAndExit(stdoutFn, exitFn);
|
|
7414
7284
|
}
|
|
7415
7285
|
const runScript = resolveRunScript3(cli ?? {});
|
|
7286
|
+
const updateCwd = cli?.cwd ?? process.cwd();
|
|
7287
|
+
const updateScriptPath = resolveCleargateScript({ cwd: updateCwd }, "update_state.mjs");
|
|
7416
7288
|
const result = spawnFn(
|
|
7417
7289
|
"bash",
|
|
7418
|
-
[runScript, "
|
|
7290
|
+
[runScript, "node", updateScriptPath, opts.storyId, opts.newState],
|
|
7419
7291
|
{ stdio: "inherit" }
|
|
7420
7292
|
);
|
|
7421
7293
|
if (result.error) {
|
|
@@ -7429,7 +7301,7 @@ function stateValidateHandler(opts, cli) {
|
|
|
7429
7301
|
const stdoutFn = cli?.stdout ?? ((s) => process.stdout.write(s + "\n"));
|
|
7430
7302
|
const stderrFn = cli?.stderr ?? ((s) => process.stderr.write(s + "\n"));
|
|
7431
7303
|
const exitFn = cli?.exit ?? defaultExit3;
|
|
7432
|
-
const spawnFn = cli?.spawnFn ??
|
|
7304
|
+
const spawnFn = cli?.spawnFn ?? spawnSync12;
|
|
7433
7305
|
const mode = readSprintExecutionMode(opts.sprintId, {
|
|
7434
7306
|
sprintFilePath: cli?.sprintFilePath,
|
|
7435
7307
|
cwd: cli?.cwd
|
|
@@ -7438,9 +7310,11 @@ function stateValidateHandler(opts, cli) {
|
|
|
7438
7310
|
return printInertAndExit(stdoutFn, exitFn);
|
|
7439
7311
|
}
|
|
7440
7312
|
const runScript = resolveRunScript3(cli ?? {});
|
|
7313
|
+
const validateCwd = cli?.cwd ?? process.cwd();
|
|
7314
|
+
const validateScriptPath = resolveCleargateScript({ cwd: validateCwd }, "validate_state.mjs");
|
|
7441
7315
|
const result = spawnFn(
|
|
7442
7316
|
"bash",
|
|
7443
|
-
[runScript, "
|
|
7317
|
+
[runScript, "node", validateScriptPath, opts.sprintId],
|
|
7444
7318
|
{ stdio: "inherit" }
|
|
7445
7319
|
);
|
|
7446
7320
|
if (result.error) {
|
|
@@ -7452,17 +7326,17 @@ function stateValidateHandler(opts, cli) {
|
|
|
7452
7326
|
}
|
|
7453
7327
|
|
|
7454
7328
|
// src/commands/stamp-tokens.ts
|
|
7455
|
-
import * as
|
|
7329
|
+
import * as fs33 from "fs";
|
|
7456
7330
|
import * as path35 from "path";
|
|
7457
7331
|
|
|
7458
7332
|
// src/lib/ledger-reader.ts
|
|
7459
|
-
import * as
|
|
7333
|
+
import * as fs32 from "fs";
|
|
7460
7334
|
import * as path34 from "path";
|
|
7461
7335
|
function findSprintRunsRoot(startDir) {
|
|
7462
7336
|
let dir = startDir;
|
|
7463
7337
|
while (true) {
|
|
7464
7338
|
const candidate = path34.join(dir, ".cleargate", "sprint-runs");
|
|
7465
|
-
if (
|
|
7339
|
+
if (fs32.existsSync(candidate)) {
|
|
7466
7340
|
return candidate;
|
|
7467
7341
|
}
|
|
7468
7342
|
const parent = path34.dirname(dir);
|
|
@@ -7518,13 +7392,13 @@ function readLedgerForWorkItem(workItemId, opts = {}) {
|
|
|
7518
7392
|
}
|
|
7519
7393
|
sprintRunsRoot = found;
|
|
7520
7394
|
}
|
|
7521
|
-
if (!
|
|
7395
|
+
if (!fs32.existsSync(sprintRunsRoot)) {
|
|
7522
7396
|
return [];
|
|
7523
7397
|
}
|
|
7524
7398
|
let ledgerFiles;
|
|
7525
7399
|
try {
|
|
7526
|
-
const entries =
|
|
7527
|
-
ledgerFiles = entries.filter((e) => e.isDirectory()).map((e) => path34.join(sprintRunsRoot, e.name, "token-ledger.jsonl")).filter((f) =>
|
|
7400
|
+
const entries = fs32.readdirSync(sprintRunsRoot, { withFileTypes: true });
|
|
7401
|
+
ledgerFiles = entries.filter((e) => e.isDirectory()).map((e) => path34.join(sprintRunsRoot, e.name, "token-ledger.jsonl")).filter((f) => fs32.existsSync(f));
|
|
7528
7402
|
} catch {
|
|
7529
7403
|
return [];
|
|
7530
7404
|
}
|
|
@@ -7532,7 +7406,7 @@ function readLedgerForWorkItem(workItemId, opts = {}) {
|
|
|
7532
7406
|
for (const ledgerFile of ledgerFiles) {
|
|
7533
7407
|
let content;
|
|
7534
7408
|
try {
|
|
7535
|
-
content =
|
|
7409
|
+
content = fs32.readFileSync(ledgerFile, "utf-8");
|
|
7536
7410
|
} catch {
|
|
7537
7411
|
continue;
|
|
7538
7412
|
}
|
|
@@ -7591,7 +7465,7 @@ async function stampTokensHandler(file, opts, cli) {
|
|
|
7591
7465
|
}
|
|
7592
7466
|
let rawContent;
|
|
7593
7467
|
try {
|
|
7594
|
-
rawContent =
|
|
7468
|
+
rawContent = fs33.readFileSync(absPath, "utf-8");
|
|
7595
7469
|
} catch {
|
|
7596
7470
|
stdoutFn(`[stamp-tokens] error: cannot read file: ${absPath}`);
|
|
7597
7471
|
exitFn(1);
|
|
@@ -7663,7 +7537,7 @@ async function stampTokensHandler(file, opts, cli) {
|
|
|
7663
7537
|
return;
|
|
7664
7538
|
}
|
|
7665
7539
|
try {
|
|
7666
|
-
|
|
7540
|
+
fs33.writeFileSync(absPath, serialized, "utf-8");
|
|
7667
7541
|
} catch {
|
|
7668
7542
|
stdoutFn(`[stamp-tokens] error: cannot write file: ${absPath}`);
|
|
7669
7543
|
exitFn(1);
|
|
@@ -7673,7 +7547,7 @@ async function stampTokensHandler(file, opts, cli) {
|
|
|
7673
7547
|
exitFn(0);
|
|
7674
7548
|
}
|
|
7675
7549
|
function extractWorkItemId(fm, absPath) {
|
|
7676
|
-
const idKeys = ["story_id", "epic_id", "proposal_id", "cr_id", "bug_id"];
|
|
7550
|
+
const idKeys = ["story_id", "epic_id", "proposal_id", "cr_id", "bug_id", "initiative_id", "sprint_id"];
|
|
7677
7551
|
for (const key of idKeys) {
|
|
7678
7552
|
const val = fm[key];
|
|
7679
7553
|
if (typeof val === "string" && val.trim() !== "") {
|
|
@@ -7681,13 +7555,13 @@ function extractWorkItemId(fm, absPath) {
|
|
|
7681
7555
|
}
|
|
7682
7556
|
}
|
|
7683
7557
|
const basename13 = path35.basename(absPath);
|
|
7684
|
-
const match = basename13.match(/^(STORY|EPIC|PROPOSAL|CR|BUG)-\d+(-\d+)?/i);
|
|
7558
|
+
const match = basename13.match(/^(STORY|EPIC|PROPOSAL|CR|BUG|INITIATIVE|SPRINT)-\d+(-\d+)?/i);
|
|
7685
7559
|
if (match) {
|
|
7686
7560
|
return match[0].toUpperCase();
|
|
7687
7561
|
}
|
|
7688
7562
|
const typeFromPath = detectWorkItemType(absPath);
|
|
7689
7563
|
if (typeFromPath) {
|
|
7690
|
-
const idMatch = basename13.match(/((?:STORY|EPIC|PROPOSAL|CR|BUG)-\d+(?:-\d+)?)/i);
|
|
7564
|
+
const idMatch = basename13.match(/((?:STORY|EPIC|PROPOSAL|CR|BUG|INITIATIVE|SPRINT)-\d+(?:-\d+)?)/i);
|
|
7691
7565
|
if (idMatch) {
|
|
7692
7566
|
return idMatch[1].toUpperCase();
|
|
7693
7567
|
}
|
|
@@ -7784,7 +7658,7 @@ ${body}`;
|
|
|
7784
7658
|
}
|
|
7785
7659
|
|
|
7786
7660
|
// src/commands/upgrade.ts
|
|
7787
|
-
import * as
|
|
7661
|
+
import * as fs34 from "fs";
|
|
7788
7662
|
import * as fsp from "fs/promises";
|
|
7789
7663
|
import * as path36 from "path";
|
|
7790
7664
|
|
|
@@ -8149,7 +8023,7 @@ async function upgradeHandler(flags, cli) {
|
|
|
8149
8023
|
const pkgRoot = cli?.packageRoot ?? path36.join(path36.dirname(new URL(import.meta.url).pathname), "..", "..");
|
|
8150
8024
|
const changelogPath = path36.join(pkgRoot, "CHANGELOG.md");
|
|
8151
8025
|
try {
|
|
8152
|
-
const changelogContent =
|
|
8026
|
+
const changelogContent = fs34.readFileSync(changelogPath, "utf-8");
|
|
8153
8027
|
const sections = sliceChangelog(changelogContent, installedVersion, targetVersion);
|
|
8154
8028
|
if (sections.length > 0) {
|
|
8155
8029
|
const deltaText = sections.map((s) => s.body).join("\n\n");
|
|
@@ -8239,7 +8113,7 @@ async function upgradeHandler(flags, cli) {
|
|
|
8239
8113
|
}
|
|
8240
8114
|
|
|
8241
8115
|
// src/commands/uninstall.ts
|
|
8242
|
-
import * as
|
|
8116
|
+
import * as fs35 from "fs";
|
|
8243
8117
|
import * as fsp2 from "fs/promises";
|
|
8244
8118
|
import * as path37 from "path";
|
|
8245
8119
|
import { execSync as execSync3 } from "child_process";
|
|
@@ -8268,9 +8142,9 @@ function shouldPreserve(entry, preserveSet, removeSet) {
|
|
|
8268
8142
|
}
|
|
8269
8143
|
function resolveProjectName(target) {
|
|
8270
8144
|
const pkgPath = path37.join(target, "package.json");
|
|
8271
|
-
if (
|
|
8145
|
+
if (fs35.existsSync(pkgPath)) {
|
|
8272
8146
|
try {
|
|
8273
|
-
const raw =
|
|
8147
|
+
const raw = fs35.readFileSync(pkgPath, "utf-8");
|
|
8274
8148
|
const parsed = JSON.parse(raw);
|
|
8275
8149
|
if (parsed.name && typeof parsed.name === "string") {
|
|
8276
8150
|
return parsed.name;
|
|
@@ -8308,7 +8182,7 @@ function detectUncommittedChanges(target, manifestPaths, gitRunner) {
|
|
|
8308
8182
|
}
|
|
8309
8183
|
async function removeFromPackageJson(target, dryRun) {
|
|
8310
8184
|
const pkgPath = path37.join(target, "package.json");
|
|
8311
|
-
if (!
|
|
8185
|
+
if (!fs35.existsSync(pkgPath)) return false;
|
|
8312
8186
|
let raw;
|
|
8313
8187
|
try {
|
|
8314
8188
|
raw = await fsp2.readFile(pkgPath, "utf-8");
|
|
@@ -8349,7 +8223,7 @@ async function removeFile(filePath) {
|
|
|
8349
8223
|
}
|
|
8350
8224
|
async function removeDir(dirPath) {
|
|
8351
8225
|
try {
|
|
8352
|
-
|
|
8226
|
+
fs35.rmSync(dirPath, { recursive: true, force: true });
|
|
8353
8227
|
} catch {
|
|
8354
8228
|
}
|
|
8355
8229
|
}
|
|
@@ -8373,8 +8247,8 @@ async function uninstallHandler(opts) {
|
|
|
8373
8247
|
const cleargateDir = path37.join(target, ".cleargate");
|
|
8374
8248
|
const manifestPath = path37.join(cleargateDir, ".install-manifest.json");
|
|
8375
8249
|
const uninstalledPath = path37.join(cleargateDir, ".uninstalled");
|
|
8376
|
-
if (!
|
|
8377
|
-
if (
|
|
8250
|
+
if (!fs35.existsSync(manifestPath)) {
|
|
8251
|
+
if (fs35.existsSync(uninstalledPath)) {
|
|
8378
8252
|
stdout("already uninstalled");
|
|
8379
8253
|
exit(0);
|
|
8380
8254
|
return;
|
|
@@ -8383,7 +8257,7 @@ async function uninstallHandler(opts) {
|
|
|
8383
8257
|
exit(0);
|
|
8384
8258
|
return;
|
|
8385
8259
|
}
|
|
8386
|
-
if (
|
|
8260
|
+
if (fs35.existsSync(uninstalledPath) && !fs35.existsSync(manifestPath)) {
|
|
8387
8261
|
stdout("already uninstalled");
|
|
8388
8262
|
exit(0);
|
|
8389
8263
|
return;
|
|
@@ -8407,8 +8281,8 @@ async function uninstallHandler(opts) {
|
|
|
8407
8281
|
}
|
|
8408
8282
|
const claudeMdPath = path37.join(target, "CLAUDE.md");
|
|
8409
8283
|
let claudeMdContent = null;
|
|
8410
|
-
if (
|
|
8411
|
-
claudeMdContent =
|
|
8284
|
+
if (fs35.existsSync(claudeMdPath)) {
|
|
8285
|
+
claudeMdContent = fs35.readFileSync(claudeMdPath, "utf-8");
|
|
8412
8286
|
if (!claudeMdContent.includes(CLEARGATE_START)) {
|
|
8413
8287
|
stderr("CLAUDE.md is missing <!-- CLEARGATE:START --> marker");
|
|
8414
8288
|
exit(1);
|
|
@@ -8425,7 +8299,7 @@ async function uninstallHandler(opts) {
|
|
|
8425
8299
|
const toSkip = [];
|
|
8426
8300
|
for (const entry of snapshot.files) {
|
|
8427
8301
|
const filePath = path37.join(target, entry.path);
|
|
8428
|
-
if (!
|
|
8302
|
+
if (!fs35.existsSync(filePath)) {
|
|
8429
8303
|
toSkip.push(entry);
|
|
8430
8304
|
continue;
|
|
8431
8305
|
}
|
|
@@ -8499,9 +8373,9 @@ async function uninstallHandler(opts) {
|
|
|
8499
8373
|
}
|
|
8500
8374
|
}
|
|
8501
8375
|
const settingsPath = path37.join(target, ".claude", "settings.json");
|
|
8502
|
-
if (
|
|
8376
|
+
if (fs35.existsSync(settingsPath)) {
|
|
8503
8377
|
try {
|
|
8504
|
-
const raw =
|
|
8378
|
+
const raw = fs35.readFileSync(settingsPath, "utf-8");
|
|
8505
8379
|
const settings = JSON.parse(raw);
|
|
8506
8380
|
const cleaned = removeClearGateHooks(settings);
|
|
8507
8381
|
await writeAtomic3(settingsPath, JSON.stringify(cleaned, null, 2) + "\n");
|
|
@@ -8543,26 +8417,26 @@ import * as fsPromises8 from "fs/promises";
|
|
|
8543
8417
|
import * as path45 from "path";
|
|
8544
8418
|
|
|
8545
8419
|
// src/lib/sync-log.ts
|
|
8546
|
-
import * as
|
|
8420
|
+
import * as fs36 from "fs";
|
|
8547
8421
|
import * as fsPromises2 from "fs/promises";
|
|
8548
8422
|
import * as path38 from "path";
|
|
8549
8423
|
function resolveActiveSprintDir(projectRoot, _opts) {
|
|
8550
8424
|
const sprintRunsRoot = path38.join(projectRoot, ".cleargate", "sprint-runs");
|
|
8551
8425
|
const offSprint = path38.join(sprintRunsRoot, "_off-sprint");
|
|
8552
|
-
if (!
|
|
8553
|
-
|
|
8554
|
-
|
|
8426
|
+
if (!fs36.existsSync(sprintRunsRoot)) {
|
|
8427
|
+
fs36.mkdirSync(sprintRunsRoot, { recursive: true });
|
|
8428
|
+
fs36.mkdirSync(offSprint, { recursive: true });
|
|
8555
8429
|
return offSprint;
|
|
8556
8430
|
}
|
|
8557
|
-
const entries =
|
|
8431
|
+
const entries = fs36.readdirSync(sprintRunsRoot, { withFileTypes: true });
|
|
8558
8432
|
const sprintDirs = entries.filter((e) => e.isDirectory() && e.name !== "_off-sprint").map((e) => {
|
|
8559
8433
|
const fullPath = path38.join(sprintRunsRoot, e.name);
|
|
8560
|
-
const stat =
|
|
8434
|
+
const stat = fs36.statSync(fullPath);
|
|
8561
8435
|
return { name: e.name, fullPath, mtimeMs: stat.mtimeMs };
|
|
8562
8436
|
}).sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
8563
8437
|
if (sprintDirs.length === 0) {
|
|
8564
|
-
if (!
|
|
8565
|
-
|
|
8438
|
+
if (!fs36.existsSync(offSprint)) {
|
|
8439
|
+
fs36.mkdirSync(offSprint, { recursive: true });
|
|
8566
8440
|
}
|
|
8567
8441
|
return offSprint;
|
|
8568
8442
|
}
|
|
@@ -8685,7 +8559,7 @@ function classify2(local, remote, since) {
|
|
|
8685
8559
|
}
|
|
8686
8560
|
|
|
8687
8561
|
// src/lib/merge-helper.ts
|
|
8688
|
-
import { promises as
|
|
8562
|
+
import { promises as fs37 } from "fs";
|
|
8689
8563
|
import * as os5 from "os";
|
|
8690
8564
|
import * as path39 from "path";
|
|
8691
8565
|
function promptFourChoice(opts) {
|
|
@@ -8765,16 +8639,16 @@ ${remote}
|
|
|
8765
8639
|
>>>>>>> remote
|
|
8766
8640
|
`;
|
|
8767
8641
|
try {
|
|
8768
|
-
await
|
|
8642
|
+
await fs37.writeFile(tmpFile, markerContent, "utf-8");
|
|
8769
8643
|
await openInEditor(tmpFile, { editor: editor ?? process.env["EDITOR"] ?? "vi" });
|
|
8770
|
-
const edited = await
|
|
8644
|
+
const edited = await fs37.readFile(tmpFile, "utf-8");
|
|
8771
8645
|
if (containsConflictMarkers(edited)) {
|
|
8772
8646
|
stdout("File still contains conflict markers \u2014 please resolve all conflicts.\n");
|
|
8773
8647
|
continue;
|
|
8774
8648
|
}
|
|
8775
8649
|
return { resolution: "edited", body: edited };
|
|
8776
8650
|
} finally {
|
|
8777
|
-
await
|
|
8651
|
+
await fs37.unlink(tmpFile).catch(() => {
|
|
8778
8652
|
});
|
|
8779
8653
|
}
|
|
8780
8654
|
}
|
|
@@ -9117,7 +8991,7 @@ async function hasAnyRemoteAuthored(projectRoot) {
|
|
|
9117
8991
|
}
|
|
9118
8992
|
|
|
9119
8993
|
// src/lib/active-criteria.ts
|
|
9120
|
-
import * as
|
|
8994
|
+
import * as fs38 from "fs";
|
|
9121
8995
|
import * as fsPromises5 from "fs/promises";
|
|
9122
8996
|
import * as path42 from "path";
|
|
9123
8997
|
async function resolveActiveItems(projectRoot, localItems, nowFn = () => (/* @__PURE__ */ new Date()).toISOString()) {
|
|
@@ -9163,7 +9037,7 @@ async function findSprintFile2(projectRoot, sprintId) {
|
|
|
9163
9037
|
const archive = path42.join(projectRoot, ".cleargate", "delivery", "archive");
|
|
9164
9038
|
for (const dir of [pendingSync, archive]) {
|
|
9165
9039
|
try {
|
|
9166
|
-
const entries =
|
|
9040
|
+
const entries = fs38.readdirSync(dir, { withFileTypes: true });
|
|
9167
9041
|
for (const entry of entries) {
|
|
9168
9042
|
if (entry.isFile() && entry.name.startsWith(sprintId) && entry.name.endsWith(".md")) {
|
|
9169
9043
|
return path42.join(dir, entry.name);
|
|
@@ -9917,7 +9791,7 @@ async function syncWorkItems(opts) {
|
|
|
9917
9791
|
}
|
|
9918
9792
|
|
|
9919
9793
|
// src/lib/admin-url.ts
|
|
9920
|
-
import * as
|
|
9794
|
+
import * as fs39 from "fs";
|
|
9921
9795
|
import * as os6 from "os";
|
|
9922
9796
|
import * as path47 from "path";
|
|
9923
9797
|
var DEFAULT_BASE = "https://admin.cleargate.soula.ge/";
|
|
@@ -9943,7 +9817,7 @@ function readLocalConfig() {
|
|
|
9943
9817
|
const home = os6.homedir();
|
|
9944
9818
|
if (!home) return null;
|
|
9945
9819
|
const configPath = path47.join(home, ".cleargate", "config.json");
|
|
9946
|
-
const raw =
|
|
9820
|
+
const raw = fs39.readFileSync(configPath, "utf8");
|
|
9947
9821
|
return JSON.parse(raw);
|
|
9948
9822
|
}
|
|
9949
9823
|
|
|
@@ -10631,7 +10505,7 @@ function formatEntry(entry) {
|
|
|
10631
10505
|
}
|
|
10632
10506
|
|
|
10633
10507
|
// src/commands/admin-login.ts
|
|
10634
|
-
import * as
|
|
10508
|
+
import * as fs40 from "fs";
|
|
10635
10509
|
import * as path51 from "path";
|
|
10636
10510
|
import * as os7 from "os";
|
|
10637
10511
|
var DEFAULT_MCP_URL = "http://localhost:3000";
|
|
@@ -10645,10 +10519,10 @@ function resolveAuthFilePath(opts) {
|
|
|
10645
10519
|
}
|
|
10646
10520
|
function writeAdminAuth(filePath, token) {
|
|
10647
10521
|
const dir = path51.dirname(filePath);
|
|
10648
|
-
|
|
10522
|
+
fs40.mkdirSync(dir, { recursive: true });
|
|
10649
10523
|
const payload = JSON.stringify({ version: 1, token }, null, 2);
|
|
10650
|
-
|
|
10651
|
-
|
|
10524
|
+
fs40.writeFileSync(filePath, payload, { encoding: "utf8", mode: 384 });
|
|
10525
|
+
fs40.chmodSync(filePath, 384);
|
|
10652
10526
|
}
|
|
10653
10527
|
async function adminLoginHandler(opts = {}) {
|
|
10654
10528
|
const fetchFn = opts.fetch ?? globalThis.fetch;
|
|
@@ -10757,7 +10631,7 @@ async function adminLoginHandler(opts = {}) {
|
|
|
10757
10631
|
}
|
|
10758
10632
|
|
|
10759
10633
|
// src/commands/hotfix.ts
|
|
10760
|
-
import * as
|
|
10634
|
+
import * as fs41 from "fs";
|
|
10761
10635
|
import * as path52 from "path";
|
|
10762
10636
|
function defaultExit4(code) {
|
|
10763
10637
|
return process.exit(code);
|
|
@@ -10768,7 +10642,7 @@ function maxHotfixId(pendingDir) {
|
|
|
10768
10642
|
let max = 0;
|
|
10769
10643
|
let entries;
|
|
10770
10644
|
try {
|
|
10771
|
-
entries =
|
|
10645
|
+
entries = fs41.readdirSync(pendingDir);
|
|
10772
10646
|
} catch {
|
|
10773
10647
|
return 0;
|
|
10774
10648
|
}
|
|
@@ -10788,7 +10662,7 @@ function countActiveHotfixes(repoRoot) {
|
|
|
10788
10662
|
let count = 0;
|
|
10789
10663
|
let pendingEntries = [];
|
|
10790
10664
|
try {
|
|
10791
|
-
pendingEntries =
|
|
10665
|
+
pendingEntries = fs41.readdirSync(pendingDir);
|
|
10792
10666
|
} catch {
|
|
10793
10667
|
}
|
|
10794
10668
|
for (const entry of pendingEntries) {
|
|
@@ -10796,13 +10670,13 @@ function countActiveHotfixes(repoRoot) {
|
|
|
10796
10670
|
}
|
|
10797
10671
|
let archiveEntries = [];
|
|
10798
10672
|
try {
|
|
10799
|
-
archiveEntries =
|
|
10673
|
+
archiveEntries = fs41.readdirSync(archiveDir);
|
|
10800
10674
|
} catch {
|
|
10801
10675
|
}
|
|
10802
10676
|
for (const entry of archiveEntries) {
|
|
10803
10677
|
if (entry.startsWith("HOTFIX-") && entry.endsWith(".md")) {
|
|
10804
10678
|
try {
|
|
10805
|
-
const stat =
|
|
10679
|
+
const stat = fs41.statSync(path52.join(archiveDir, entry));
|
|
10806
10680
|
if (stat.mtimeMs >= sevenDaysAgo) count++;
|
|
10807
10681
|
} catch {
|
|
10808
10682
|
}
|
|
@@ -10837,7 +10711,7 @@ function hotfixNewHandler(opts, cli) {
|
|
|
10837
10711
|
const templatePath = resolveTemplatePath(repoRoot);
|
|
10838
10712
|
let templateContent;
|
|
10839
10713
|
try {
|
|
10840
|
-
templateContent =
|
|
10714
|
+
templateContent = fs41.readFileSync(templatePath, "utf8");
|
|
10841
10715
|
} catch {
|
|
10842
10716
|
stderrFn(`[cleargate hotfix new] template not found: ${templatePath}`);
|
|
10843
10717
|
return exitFn(2);
|
|
@@ -10847,8 +10721,8 @@ function hotfixNewHandler(opts, cli) {
|
|
|
10847
10721
|
const fileName = `${idStr}_${fileSlug}.md`;
|
|
10848
10722
|
const outPath = path52.join(pendingDir, fileName);
|
|
10849
10723
|
try {
|
|
10850
|
-
|
|
10851
|
-
|
|
10724
|
+
fs41.mkdirSync(pendingDir, { recursive: true });
|
|
10725
|
+
fs41.writeFileSync(outPath, content, "utf8");
|
|
10852
10726
|
} catch (err) {
|
|
10853
10727
|
const msg = err instanceof Error ? err.message : String(err);
|
|
10854
10728
|
stderrFn(`[cleargate hotfix new] write failed: ${msg}`);
|