cclaw-cli 0.48.1 → 0.48.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -3
- package/dist/artifact-linter.js +2 -8
- package/dist/cli.js +8 -1
- package/dist/config.d.ts +3 -0
- package/dist/config.js +13 -3
- package/dist/constants.d.ts +6 -0
- package/dist/constants.js +11 -0
- package/dist/content/contracts.d.ts +2 -2
- package/dist/content/contracts.js +2 -2
- package/dist/content/core-agents.d.ts +1 -1
- package/dist/content/core-agents.js +1 -1
- package/dist/content/hooks.js +16 -15
- package/dist/content/next-command.js +4 -2
- package/dist/content/observe.d.ts +2 -2
- package/dist/content/observe.js +83 -13
- package/dist/content/opencode-plugin.js +227 -45
- package/dist/content/stage-schema.js +1 -1
- package/dist/content/stages/ship.js +2 -5
- package/dist/content/templates.js +3 -6
- package/dist/delegation.d.ts +5 -1
- package/dist/delegation.js +12 -8
- package/dist/doctor.js +132 -15
- package/dist/eval/runner.js +36 -4
- package/dist/feature-system.d.ts +11 -4
- package/dist/feature-system.js +54 -10
- package/dist/flow-state.d.ts +2 -0
- package/dist/flow-state.js +19 -2
- package/dist/fs-utils.d.ts +4 -1
- package/dist/fs-utils.js +20 -4
- package/dist/gate-evidence.d.ts +2 -0
- package/dist/gate-evidence.js +13 -4
- package/dist/install.js +25 -23
- package/dist/internal/advance-stage.js +49 -10
- package/dist/knowledge-store.d.ts +8 -0
- package/dist/knowledge-store.js +113 -33
- package/dist/retro-gate.js +33 -23
- package/dist/run-archive.js +166 -128
- package/dist/run-persistence.d.ts +8 -1
- package/dist/run-persistence.js +7 -6
- package/dist/trace-matrix.js +7 -7
- package/package.json +1 -1
package/dist/doctor.js
CHANGED
|
@@ -5,16 +5,16 @@ import { pathToFileURL } from "node:url";
|
|
|
5
5
|
import { promisify } from "node:util";
|
|
6
6
|
import { REQUIRED_DIRS, RUNTIME_ROOT, UTILITY_COMMANDS } from "./constants.js";
|
|
7
7
|
import { CCLAW_AGENTS } from "./content/core-agents.js";
|
|
8
|
-
import { detectAdvancedKeys, readConfig } from "./config.js";
|
|
8
|
+
import { detectAdvancedKeys, InvalidConfigError, readConfig } from "./config.js";
|
|
9
9
|
import { exists } from "./fs-utils.js";
|
|
10
10
|
import { gitignoreHasRequiredPatterns } from "./gitignore.js";
|
|
11
11
|
import { HARNESS_ADAPTERS, CCLAW_MARKER_START, CCLAW_MARKER_END, harnessShimFileNames, harnessShimSkillNames } from "./harness-adapters.js";
|
|
12
12
|
import { policyChecks } from "./policy.js";
|
|
13
|
-
import { readFlowState } from "./runs.js";
|
|
14
|
-
import { skippedStagesForTrack } from "./flow-state.js";
|
|
13
|
+
import { CorruptFlowStateError, readFlowState } from "./runs.js";
|
|
14
|
+
import { createInitialFlowState, skippedStagesForTrack } from "./flow-state.js";
|
|
15
15
|
import { FLOW_STAGES, TRACK_STAGES } from "./types.js";
|
|
16
16
|
import { checkMandatoryDelegations } from "./delegation.js";
|
|
17
|
-
import { ensureFeatureSystem, listFeatures, readActiveFeature, readFeatureWorktreeRegistry, resolveFeatureWorkspacePath, worktreeRegistryPath } from "./feature-system.js";
|
|
17
|
+
import { activeFeatureMetaPath, ensureFeatureSystem, listFeatures, readActiveFeature, readFeatureWorktreeRegistry, resolveFeatureWorkspacePath, worktreeRegistryPath } from "./feature-system.js";
|
|
18
18
|
import { buildTraceMatrix } from "./trace-matrix.js";
|
|
19
19
|
import { classifyReconciliationNotices, reconcileAndWriteCurrentStageGateCatalog, readReconciliationNotices, RECONCILIATION_NOTICES_REL_PATH, verifyCompletedStagesGateClosure, verifyCurrentStageGateEvidence } from "./gate-evidence.js";
|
|
20
20
|
import { parseTddCycleLog, validateTddCycleOrder } from "./tdd-cycle.js";
|
|
@@ -190,6 +190,34 @@ async function readHookDocument(filePath) {
|
|
|
190
190
|
return null;
|
|
191
191
|
}
|
|
192
192
|
}
|
|
193
|
+
async function readJsonObjectStatus(filePath) {
|
|
194
|
+
if (!(await exists(filePath))) {
|
|
195
|
+
return { exists: false, ok: false, error: "file is missing" };
|
|
196
|
+
}
|
|
197
|
+
try {
|
|
198
|
+
const parsed = JSON.parse(await fs.readFile(filePath, "utf8"));
|
|
199
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
200
|
+
return { exists: true, ok: false, error: "JSON root must be an object" };
|
|
201
|
+
}
|
|
202
|
+
return { exists: true, ok: true };
|
|
203
|
+
}
|
|
204
|
+
catch (error) {
|
|
205
|
+
return {
|
|
206
|
+
exists: true,
|
|
207
|
+
ok: false,
|
|
208
|
+
error: error instanceof Error ? error.message : String(error)
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
async function readPermissionBits(filePath) {
|
|
213
|
+
try {
|
|
214
|
+
const stat = await fs.stat(filePath);
|
|
215
|
+
return stat.mode & 0o777;
|
|
216
|
+
}
|
|
217
|
+
catch {
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
193
221
|
function normalizeOpenCodePluginEntry(entry) {
|
|
194
222
|
if (typeof entry === "string" && entry.trim().length > 0)
|
|
195
223
|
return entry.trim();
|
|
@@ -212,12 +240,16 @@ async function opencodeRegistrationCheck(projectRoot) {
|
|
|
212
240
|
path.join(projectRoot, ".opencode/opencode.json"),
|
|
213
241
|
path.join(projectRoot, ".opencode/opencode.jsonc")
|
|
214
242
|
];
|
|
243
|
+
const mismatches = [];
|
|
244
|
+
let foundAnyConfig = false;
|
|
215
245
|
for (const configPath of candidates) {
|
|
216
246
|
if (!(await exists(configPath))) {
|
|
217
247
|
continue;
|
|
218
248
|
}
|
|
249
|
+
foundAnyConfig = true;
|
|
219
250
|
const parsed = await readHookDocument(configPath);
|
|
220
251
|
if (!parsed) {
|
|
252
|
+
mismatches.push(`${path.relative(projectRoot, configPath)} is unreadable or invalid JSON`);
|
|
221
253
|
continue;
|
|
222
254
|
}
|
|
223
255
|
const plugins = Array.isArray(parsed.plugin) ? parsed.plugin : [];
|
|
@@ -225,7 +257,10 @@ async function opencodeRegistrationCheck(projectRoot) {
|
|
|
225
257
|
if (registered) {
|
|
226
258
|
return { ok: true, details: `${path.relative(projectRoot, configPath)} registers ${expected}` };
|
|
227
259
|
}
|
|
228
|
-
|
|
260
|
+
mismatches.push(`${path.relative(projectRoot, configPath)} missing plugin ${expected}`);
|
|
261
|
+
}
|
|
262
|
+
if (foundAnyConfig) {
|
|
263
|
+
return { ok: false, details: mismatches.join(" | ") };
|
|
229
264
|
}
|
|
230
265
|
return { ok: false, details: `No opencode.json/opencode.jsonc found with plugin ${expected}` };
|
|
231
266
|
}
|
|
@@ -244,6 +279,12 @@ async function opencodePluginRuntimeShapeCheck(projectRoot) {
|
|
|
244
279
|
};
|
|
245
280
|
}
|
|
246
281
|
const plugin = imported.default({ directory: projectRoot });
|
|
282
|
+
if (!plugin || typeof plugin !== "object" || Array.isArray(plugin)) {
|
|
283
|
+
return {
|
|
284
|
+
ok: false,
|
|
285
|
+
details: `${path.relative(projectRoot, pluginPath)} factory must return a plugin object`
|
|
286
|
+
};
|
|
287
|
+
}
|
|
247
288
|
const requiredHandlers = [
|
|
248
289
|
"event",
|
|
249
290
|
"tool.execute.before",
|
|
@@ -257,7 +298,6 @@ async function opencodePluginRuntimeShapeCheck(projectRoot) {
|
|
|
257
298
|
details: `${path.relative(projectRoot, pluginPath)} missing runtime handlers: ${missing.join(", ")}`
|
|
258
299
|
};
|
|
259
300
|
}
|
|
260
|
-
await plugin.event({ event: { type: "session.updated", data: {} } });
|
|
261
301
|
return {
|
|
262
302
|
ok: true,
|
|
263
303
|
details: `${path.relative(projectRoot, pluginPath)} exports compatible runtime handler shape`
|
|
@@ -426,6 +466,7 @@ export async function doctorChecks(projectRoot, options = {}) {
|
|
|
426
466
|
checks.push({
|
|
427
467
|
name: "config:valid",
|
|
428
468
|
ok: false,
|
|
469
|
+
severity: error instanceof InvalidConfigError ? "error" : "warning",
|
|
429
470
|
details: error instanceof Error ? error.message : "Invalid config"
|
|
430
471
|
});
|
|
431
472
|
}
|
|
@@ -1289,10 +1330,28 @@ export async function doctorChecks(projectRoot, options = {}) {
|
|
|
1289
1330
|
details: modePath
|
|
1290
1331
|
});
|
|
1291
1332
|
}
|
|
1292
|
-
await ensureFeatureSystem(projectRoot);
|
|
1293
|
-
const activeFeature = await readActiveFeature(projectRoot);
|
|
1294
|
-
let flowState =
|
|
1295
|
-
|
|
1333
|
+
await ensureFeatureSystem(projectRoot, { repair: false });
|
|
1334
|
+
const activeFeature = await readActiveFeature(projectRoot, { repair: false });
|
|
1335
|
+
let flowState = createInitialFlowState();
|
|
1336
|
+
let flowStateCorruptError = null;
|
|
1337
|
+
try {
|
|
1338
|
+
flowState = await readFlowState(projectRoot, { repairFeatureSystem: false });
|
|
1339
|
+
}
|
|
1340
|
+
catch (error) {
|
|
1341
|
+
if (error instanceof CorruptFlowStateError) {
|
|
1342
|
+
flowStateCorruptError = error;
|
|
1343
|
+
checks.push({
|
|
1344
|
+
name: "flow_state:readable",
|
|
1345
|
+
ok: false,
|
|
1346
|
+
severity: "error",
|
|
1347
|
+
details: error.message
|
|
1348
|
+
});
|
|
1349
|
+
}
|
|
1350
|
+
else {
|
|
1351
|
+
throw error;
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
if (options.reconcileCurrentStageGates === true && !flowStateCorruptError) {
|
|
1296
1355
|
const reconciliation = await reconcileAndWriteCurrentStageGateCatalog(projectRoot);
|
|
1297
1356
|
if (reconciliation.wrote) {
|
|
1298
1357
|
flowState = {
|
|
@@ -1311,13 +1370,53 @@ export async function doctorChecks(projectRoot, options = {}) {
|
|
|
1311
1370
|
: `no gate reconciliation changes needed for stage "${reconciliation.stage}"`
|
|
1312
1371
|
});
|
|
1313
1372
|
}
|
|
1373
|
+
else if (options.reconcileCurrentStageGates === true && flowStateCorruptError) {
|
|
1374
|
+
checks.push({
|
|
1375
|
+
name: "gates:reconcile:writeback",
|
|
1376
|
+
ok: false,
|
|
1377
|
+
details: "skipped gate reconciliation because flow-state.json is corrupt"
|
|
1378
|
+
});
|
|
1379
|
+
}
|
|
1314
1380
|
const activeRunId = typeof flowState.activeRunId === "string" ? flowState.activeRunId.trim() : "";
|
|
1315
1381
|
checks.push({
|
|
1316
1382
|
name: "flow_state:active_run_id",
|
|
1317
1383
|
ok: activeRunId.length > 0,
|
|
1318
1384
|
details: `${RUNTIME_ROOT}/state/flow-state.json must include activeRunId`
|
|
1319
1385
|
});
|
|
1386
|
+
const sensitivePermissionTargets = [
|
|
1387
|
+
path.join(projectRoot, RUNTIME_ROOT, "state", "flow-state.json"),
|
|
1388
|
+
path.join(projectRoot, RUNTIME_ROOT, "state", "delegation-log.json"),
|
|
1389
|
+
path.join(projectRoot, RUNTIME_ROOT, "state", "reconciliation-notices.json"),
|
|
1390
|
+
path.join(projectRoot, RUNTIME_ROOT, "state", "worktrees.json"),
|
|
1391
|
+
path.join(projectRoot, RUNTIME_ROOT, "state", "active-feature.json"),
|
|
1392
|
+
path.join(projectRoot, RUNTIME_ROOT, "knowledge.jsonl")
|
|
1393
|
+
];
|
|
1394
|
+
const permissiveStateFiles = [];
|
|
1395
|
+
for (const targetPath of sensitivePermissionTargets) {
|
|
1396
|
+
const bits = await readPermissionBits(targetPath);
|
|
1397
|
+
if (bits === null)
|
|
1398
|
+
continue;
|
|
1399
|
+
if (bits > 0o640) {
|
|
1400
|
+
permissiveStateFiles.push(`${path.relative(projectRoot, targetPath)}:${bits.toString(8)}`);
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
checks.push({
|
|
1404
|
+
name: "warning:state:file_permissions",
|
|
1405
|
+
ok: true,
|
|
1406
|
+
details: permissiveStateFiles.length === 0
|
|
1407
|
+
? "sensitive state files are <=0640 permissions"
|
|
1408
|
+
: `warning: sensitive state files are overly permissive (${permissiveStateFiles.join(", ")}). Run \`chmod 600 .cclaw/state/*.json .cclaw/state/*.jsonl .cclaw/knowledge.jsonl\` if this machine is multi-user.`
|
|
1409
|
+
});
|
|
1320
1410
|
const reconciliationNotices = await readReconciliationNotices(projectRoot);
|
|
1411
|
+
checks.push({
|
|
1412
|
+
name: "state:reconciliation_notices_parse",
|
|
1413
|
+
ok: reconciliationNotices.parseOk && reconciliationNotices.schemaOk,
|
|
1414
|
+
details: !reconciliationNotices.parseOk
|
|
1415
|
+
? `unable to parse ${RECONCILIATION_NOTICES_REL_PATH}; reset with \`cclaw sync\` or repair JSON by hand`
|
|
1416
|
+
: !reconciliationNotices.schemaOk
|
|
1417
|
+
? `${RECONCILIATION_NOTICES_REL_PATH} schemaVersion mismatch; expected ${reconciliationNotices.schemaVersion}`
|
|
1418
|
+
: `${RECONCILIATION_NOTICES_REL_PATH} parsed successfully`
|
|
1419
|
+
});
|
|
1321
1420
|
const noticeBuckets = classifyReconciliationNotices(flowState, reconciliationNotices.notices);
|
|
1322
1421
|
const formatNoticeList = (items) => items
|
|
1323
1422
|
.slice(0, 8)
|
|
@@ -1425,22 +1524,38 @@ export async function doctorChecks(projectRoot, options = {}) {
|
|
|
1425
1524
|
? "no TODO/TBD/FIXME placeholder markers found in active artifacts"
|
|
1426
1525
|
: `warning: placeholder markers detected in active artifacts (${artifactPlaceholderHits.join(", ")}). Clear before marking completion.`
|
|
1427
1526
|
});
|
|
1428
|
-
const
|
|
1429
|
-
const
|
|
1527
|
+
const activeMetaStatus = await readJsonObjectStatus(activeFeatureMetaPath(projectRoot));
|
|
1528
|
+
const worktreeRegistryStatus = await readJsonObjectStatus(worktreeRegistryPath(projectRoot));
|
|
1529
|
+
const features = await listFeatures(projectRoot, { repair: false });
|
|
1530
|
+
const worktreeRegistry = await readFeatureWorktreeRegistry(projectRoot, { repair: false });
|
|
1430
1531
|
const activeFeatureEntry = worktreeRegistry.entries.find((entry) => entry.featureId === activeFeature);
|
|
1431
1532
|
const activeFeatureWorkspacePath = activeFeatureEntry
|
|
1432
1533
|
? resolveFeatureWorkspacePath(projectRoot, activeFeatureEntry)
|
|
1433
1534
|
: "";
|
|
1434
1535
|
checks.push({
|
|
1435
1536
|
name: "state:active_feature_meta",
|
|
1436
|
-
ok:
|
|
1537
|
+
ok: activeMetaStatus.exists,
|
|
1437
1538
|
details: `${RUNTIME_ROOT}/state/active-feature.json must exist`
|
|
1438
1539
|
});
|
|
1540
|
+
checks.push({
|
|
1541
|
+
name: "state:active_feature_meta_valid_json",
|
|
1542
|
+
ok: activeMetaStatus.ok,
|
|
1543
|
+
details: activeMetaStatus.ok
|
|
1544
|
+
? `${RUNTIME_ROOT}/state/active-feature.json parsed successfully`
|
|
1545
|
+
: `${RUNTIME_ROOT}/state/active-feature.json is invalid: ${activeMetaStatus.error ?? "unknown error"}`
|
|
1546
|
+
});
|
|
1439
1547
|
checks.push({
|
|
1440
1548
|
name: "state:worktree_registry_exists",
|
|
1441
|
-
ok:
|
|
1549
|
+
ok: worktreeRegistryStatus.exists,
|
|
1442
1550
|
details: `${RUNTIME_ROOT}/state/worktrees.json must exist and track feature->worktree mapping`
|
|
1443
1551
|
});
|
|
1552
|
+
checks.push({
|
|
1553
|
+
name: "state:worktree_registry_valid_json",
|
|
1554
|
+
ok: worktreeRegistryStatus.ok,
|
|
1555
|
+
details: worktreeRegistryStatus.ok
|
|
1556
|
+
? `${RUNTIME_ROOT}/state/worktrees.json parsed successfully`
|
|
1557
|
+
: `${RUNTIME_ROOT}/state/worktrees.json is invalid: ${worktreeRegistryStatus.error ?? "unknown error"}`
|
|
1558
|
+
});
|
|
1444
1559
|
checks.push({
|
|
1445
1560
|
name: "state:active_feature_exists",
|
|
1446
1561
|
ok: features.includes(activeFeature),
|
|
@@ -1590,7 +1705,9 @@ export async function doctorChecks(projectRoot, options = {}) {
|
|
|
1590
1705
|
ok: await exists(path.join(projectRoot, RUNTIME_ROOT, "runs")),
|
|
1591
1706
|
details: `${RUNTIME_ROOT}/runs must exist for archived feature snapshots`
|
|
1592
1707
|
});
|
|
1593
|
-
const delegation = await checkMandatoryDelegations(projectRoot, flowState.currentStage
|
|
1708
|
+
const delegation = await checkMandatoryDelegations(projectRoot, flowState.currentStage, {
|
|
1709
|
+
repairFeatureSystem: false
|
|
1710
|
+
});
|
|
1594
1711
|
const missingEvidenceNote = delegation.missingEvidence && delegation.missingEvidence.length > 0
|
|
1595
1712
|
? ` (role-switch rows without evidenceRefs: ${delegation.missingEvidence.join(", ")})`
|
|
1596
1713
|
: "";
|
package/dist/eval/runner.js
CHANGED
|
@@ -524,6 +524,34 @@ function stagesInResults(caseResults) {
|
|
|
524
524
|
set.add(c.stage);
|
|
525
525
|
return FLOW_STAGES.filter((s) => set.has(s));
|
|
526
526
|
}
|
|
527
|
+
const MAX_PARALLEL_CASES = 4;
|
|
528
|
+
async function runCasesWithBoundedConcurrency(items, concurrency, worker) {
|
|
529
|
+
if (items.length === 0) {
|
|
530
|
+
return [];
|
|
531
|
+
}
|
|
532
|
+
const limit = Math.max(1, Math.min(concurrency, items.length));
|
|
533
|
+
if (limit === 1) {
|
|
534
|
+
const results = [];
|
|
535
|
+
for (let i = 0; i < items.length; i += 1) {
|
|
536
|
+
results.push(await worker(items[i], i));
|
|
537
|
+
}
|
|
538
|
+
return results;
|
|
539
|
+
}
|
|
540
|
+
const results = new Array(items.length);
|
|
541
|
+
let cursor = 0;
|
|
542
|
+
const runners = Array.from({ length: limit }, async () => {
|
|
543
|
+
while (true) {
|
|
544
|
+
const index = cursor;
|
|
545
|
+
cursor += 1;
|
|
546
|
+
if (index >= items.length) {
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
results[index] = await worker(items[index], index);
|
|
550
|
+
}
|
|
551
|
+
});
|
|
552
|
+
await Promise.all(runners);
|
|
553
|
+
return results;
|
|
554
|
+
}
|
|
527
555
|
/**
|
|
528
556
|
* Main eval runner. Dispatches between fixture-backed verification, the
|
|
529
557
|
* single-stage agent-with-tools loop, and the multi-stage workflow
|
|
@@ -653,8 +681,11 @@ export async function runEval(options) {
|
|
|
653
681
|
}
|
|
654
682
|
}
|
|
655
683
|
else {
|
|
656
|
-
|
|
657
|
-
|
|
684
|
+
// Only parallelize fixture/rules verification passes that do not depend on
|
|
685
|
+
// LLM judge/agent loops. Those modes touch cost guards and retries where
|
|
686
|
+
// ordered execution is safer.
|
|
687
|
+
const caseConcurrency = flags.runJudge || flags.runAgent ? 1 : MAX_PARALLEL_CASES;
|
|
688
|
+
const results = await runCasesWithBoundedConcurrency(corpus, caseConcurrency, async (item, i) => {
|
|
658
689
|
progress.emit({
|
|
659
690
|
kind: "case-start",
|
|
660
691
|
caseId: item.id,
|
|
@@ -682,8 +713,9 @@ export async function runEval(options) {
|
|
|
682
713
|
durationMs: result.durationMs,
|
|
683
714
|
...(result.costUsd !== undefined ? { costUsd: result.costUsd } : {})
|
|
684
715
|
});
|
|
685
|
-
|
|
686
|
-
}
|
|
716
|
+
return result;
|
|
717
|
+
});
|
|
718
|
+
caseResults.push(...results);
|
|
687
719
|
}
|
|
688
720
|
const stages = stagesInResults(caseResults);
|
|
689
721
|
const baselines = await loadBaselinesByStage(options.projectRoot, stages);
|
package/dist/feature-system.d.ts
CHANGED
|
@@ -19,16 +19,23 @@ export interface CreateFeatureOptions {
|
|
|
19
19
|
cloneActive?: boolean;
|
|
20
20
|
switchTo?: boolean;
|
|
21
21
|
}
|
|
22
|
+
export interface FeatureSystemAccessOptions {
|
|
23
|
+
/**
|
|
24
|
+
* When false, read metadata without auto-repair writes. Useful for pure
|
|
25
|
+
* diagnostics (doctor) that should not mutate state as a side effect.
|
|
26
|
+
*/
|
|
27
|
+
repair?: boolean;
|
|
28
|
+
}
|
|
22
29
|
export declare function activeFeatureMetaPath(projectRoot: string): string;
|
|
23
30
|
export declare function worktreeRegistryPath(projectRoot: string): string;
|
|
24
31
|
export declare function featureRootPath(projectRoot: string, featureId: string): string;
|
|
25
32
|
export declare function featureArtifactsPath(projectRoot: string, featureId: string): string;
|
|
26
33
|
export declare function featureStatePath(projectRoot: string, featureId: string): string;
|
|
27
34
|
export declare function resolveFeatureWorkspacePath(projectRoot: string, entry: FeatureWorkspaceEntry): string;
|
|
28
|
-
export declare function ensureFeatureSystem(projectRoot: string): Promise<ActiveFeatureMeta>;
|
|
29
|
-
export declare function readFeatureWorktreeRegistry(projectRoot: string): Promise<FeatureWorktreeRegistry>;
|
|
30
|
-
export declare function readActiveFeature(projectRoot: string): Promise<string>;
|
|
31
|
-
export declare function listFeatures(projectRoot: string): Promise<string[]>;
|
|
35
|
+
export declare function ensureFeatureSystem(projectRoot: string, options?: FeatureSystemAccessOptions): Promise<ActiveFeatureMeta>;
|
|
36
|
+
export declare function readFeatureWorktreeRegistry(projectRoot: string, options?: FeatureSystemAccessOptions): Promise<FeatureWorktreeRegistry>;
|
|
37
|
+
export declare function readActiveFeature(projectRoot: string, options?: FeatureSystemAccessOptions): Promise<string>;
|
|
38
|
+
export declare function listFeatures(projectRoot: string, options?: FeatureSystemAccessOptions): Promise<string[]>;
|
|
32
39
|
export declare function syncActiveFeatureSnapshot(projectRoot: string): Promise<void>;
|
|
33
40
|
export declare function switchActiveFeature(projectRoot: string, featureId: string): Promise<ActiveFeatureMeta>;
|
|
34
41
|
export declare function createFeature(projectRoot: string, rawFeatureId: string, options?: CreateFeatureOptions): Promise<string>;
|
package/dist/feature-system.js
CHANGED
|
@@ -178,7 +178,7 @@ async function writeRegistry(projectRoot, registry) {
|
|
|
178
178
|
updatedAt: registry.updatedAt,
|
|
179
179
|
entries: dedupeEntries(registry.entries)
|
|
180
180
|
};
|
|
181
|
-
await writeFileSafe(worktreeRegistryPath(projectRoot), `${JSON.stringify(normalized, null, 2)}\n
|
|
181
|
+
await writeFileSafe(worktreeRegistryPath(projectRoot), `${JSON.stringify(normalized, null, 2)}\n`, { mode: 0o600 });
|
|
182
182
|
}
|
|
183
183
|
async function readActiveFeatureMetaInternal(projectRoot) {
|
|
184
184
|
const filePath = activeFeatureMetaPath(projectRoot);
|
|
@@ -213,7 +213,7 @@ async function writeActiveFeatureMeta(projectRoot, meta) {
|
|
|
213
213
|
activeFeature: normalizedFeatureId(meta.activeFeature),
|
|
214
214
|
updatedAt: meta.updatedAt
|
|
215
215
|
};
|
|
216
|
-
await writeFileSafe(activeFeatureMetaPath(projectRoot), `${JSON.stringify(normalized, null, 2)}\n
|
|
216
|
+
await writeFileSafe(activeFeatureMetaPath(projectRoot), `${JSON.stringify(normalized, null, 2)}\n`, { mode: 0o600 });
|
|
217
217
|
}
|
|
218
218
|
function registryHasFeature(registry, featureId) {
|
|
219
219
|
return registry.entries.some((entry) => entry.featureId === featureId);
|
|
@@ -278,20 +278,64 @@ async function ensureRegistryState(projectRoot) {
|
|
|
278
278
|
await writeActiveFeatureMeta(projectRoot, activeMeta);
|
|
279
279
|
return { registry, activeMeta };
|
|
280
280
|
}
|
|
281
|
-
|
|
282
|
-
const
|
|
281
|
+
async function readRegistryStateReadonly(projectRoot) {
|
|
282
|
+
const currentRegistry = await readRegistry(projectRoot);
|
|
283
|
+
const entries = [...currentRegistry.entries];
|
|
284
|
+
const gitRepo = await isGitRepository(projectRoot);
|
|
285
|
+
const source = gitRepo ? "git-worktree" : "workspace";
|
|
286
|
+
const branch = gitRepo ? await currentBranch(projectRoot) : "workspace/default";
|
|
287
|
+
if (!entries.some((entry) => entry.featureId === DEFAULT_FEATURE_ID)) {
|
|
288
|
+
entries.push(buildDefaultEntry(source, branch));
|
|
289
|
+
}
|
|
290
|
+
const legacyFeatureIds = await listLegacySnapshotIds(projectRoot);
|
|
291
|
+
for (const legacyId of legacyFeatureIds) {
|
|
292
|
+
if (entries.some((entry) => entry.featureId === legacyId)) {
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
entries.push({
|
|
296
|
+
featureId: legacyId,
|
|
297
|
+
branch: `legacy/${legacyId}`,
|
|
298
|
+
path: `${LEGACY_FEATURES_DIR_REL_PATH}/${legacyId}`,
|
|
299
|
+
source: "legacy-snapshot",
|
|
300
|
+
createdAt: new Date().toISOString()
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
const registry = {
|
|
304
|
+
schemaVersion: WORKTREE_REGISTRY_SCHEMA_VERSION,
|
|
305
|
+
updatedAt: currentRegistry.updatedAt,
|
|
306
|
+
entries: dedupeEntries(entries)
|
|
307
|
+
};
|
|
308
|
+
const active = await readActiveFeatureMetaInternal(projectRoot);
|
|
309
|
+
return {
|
|
310
|
+
registry,
|
|
311
|
+
activeMeta: {
|
|
312
|
+
activeFeature: registryHasFeature(registry, active.activeFeature)
|
|
313
|
+
? active.activeFeature
|
|
314
|
+
: DEFAULT_FEATURE_ID,
|
|
315
|
+
updatedAt: active.updatedAt
|
|
316
|
+
}
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
async function resolveFeatureSystemState(projectRoot, options = {}) {
|
|
320
|
+
if (options.repair === false) {
|
|
321
|
+
return readRegistryStateReadonly(projectRoot);
|
|
322
|
+
}
|
|
323
|
+
return ensureRegistryState(projectRoot);
|
|
324
|
+
}
|
|
325
|
+
export async function ensureFeatureSystem(projectRoot, options = {}) {
|
|
326
|
+
const { activeMeta } = await resolveFeatureSystemState(projectRoot, options);
|
|
283
327
|
return activeMeta;
|
|
284
328
|
}
|
|
285
|
-
export async function readFeatureWorktreeRegistry(projectRoot) {
|
|
286
|
-
const { registry } = await
|
|
329
|
+
export async function readFeatureWorktreeRegistry(projectRoot, options = {}) {
|
|
330
|
+
const { registry } = await resolveFeatureSystemState(projectRoot, options);
|
|
287
331
|
return registry;
|
|
288
332
|
}
|
|
289
|
-
export async function readActiveFeature(projectRoot) {
|
|
290
|
-
const meta = await ensureFeatureSystem(projectRoot);
|
|
333
|
+
export async function readActiveFeature(projectRoot, options = {}) {
|
|
334
|
+
const meta = await ensureFeatureSystem(projectRoot, options);
|
|
291
335
|
return normalizedFeatureId(meta.activeFeature);
|
|
292
336
|
}
|
|
293
|
-
export async function listFeatures(projectRoot) {
|
|
294
|
-
const registry = await readFeatureWorktreeRegistry(projectRoot);
|
|
337
|
+
export async function listFeatures(projectRoot, options = {}) {
|
|
338
|
+
const registry = await readFeatureWorktreeRegistry(projectRoot, options);
|
|
295
339
|
return registry.entries.map((entry) => entry.featureId).sort((a, b) => a.localeCompare(b));
|
|
296
340
|
}
|
|
297
341
|
export async function syncActiveFeatureSnapshot(projectRoot) {
|
package/dist/flow-state.d.ts
CHANGED
|
@@ -92,8 +92,10 @@ export declare function isFlowTrack(value: unknown): value is FlowTrack;
|
|
|
92
92
|
export declare function trackStages(track: FlowTrack): FlowStage[];
|
|
93
93
|
export declare function skippedStagesForTrack(track: FlowTrack): FlowStage[];
|
|
94
94
|
export declare function firstStageForTrack(track: FlowTrack): FlowStage;
|
|
95
|
+
export declare function createRunId(date?: Date): string;
|
|
95
96
|
export declare function createInitialFlowState(activeRunIdOrOptions?: string | InitialFlowStateOptions, maybeTrack?: FlowTrack): FlowState;
|
|
96
97
|
export declare function canTransition(from: FlowStage, to: FlowStage): boolean;
|
|
98
|
+
export declare function getAvailableTransitions(from: FlowStage, track?: FlowTrack): TransitionRule[];
|
|
97
99
|
export declare function getTransitionGuards(from: FlowStage, to: FlowStage, track?: FlowTrack): string[];
|
|
98
100
|
export declare function nextStage(stage: FlowStage, track?: FlowTrack): FlowStage | null;
|
|
99
101
|
export declare function previousStage(stage: FlowStage, track?: FlowTrack): FlowStage | null;
|
package/dist/flow-state.js
CHANGED
|
@@ -58,11 +58,14 @@ export function firstStageForTrack(track) {
|
|
|
58
58
|
const stages = TRACK_STAGES[track];
|
|
59
59
|
return stages[0] ?? "brainstorm";
|
|
60
60
|
}
|
|
61
|
-
export function
|
|
61
|
+
export function createRunId(date = new Date()) {
|
|
62
|
+
return `run-${date.getTime().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
63
|
+
}
|
|
64
|
+
export function createInitialFlowState(activeRunIdOrOptions = {}, maybeTrack) {
|
|
62
65
|
const options = typeof activeRunIdOrOptions === "string"
|
|
63
66
|
? { activeRunId: activeRunIdOrOptions, track: maybeTrack }
|
|
64
67
|
: activeRunIdOrOptions;
|
|
65
|
-
const activeRunId = options.activeRunId ??
|
|
68
|
+
const activeRunId = options.activeRunId ?? createRunId();
|
|
66
69
|
const track = options.track ?? "standard";
|
|
67
70
|
const skippedStages = skippedStagesForTrack(track);
|
|
68
71
|
const stageGateCatalog = {};
|
|
@@ -97,6 +100,20 @@ export function createInitialFlowState(activeRunIdOrOptions = "active", maybeTra
|
|
|
97
100
|
export function canTransition(from, to) {
|
|
98
101
|
return TRANSITION_RULES.some((rule) => rule.from === from && rule.to === to);
|
|
99
102
|
}
|
|
103
|
+
export function getAvailableTransitions(from, track = "standard") {
|
|
104
|
+
const natural = nextStage(from, track);
|
|
105
|
+
const fromRules = TRANSITION_RULES.filter((rule) => rule.from === from);
|
|
106
|
+
if (!natural) {
|
|
107
|
+
return fromRules;
|
|
108
|
+
}
|
|
109
|
+
return fromRules.sort((a, b) => {
|
|
110
|
+
if (a.to === natural && b.to !== natural)
|
|
111
|
+
return -1;
|
|
112
|
+
if (b.to === natural && a.to !== natural)
|
|
113
|
+
return 1;
|
|
114
|
+
return a.to.localeCompare(b.to);
|
|
115
|
+
});
|
|
116
|
+
}
|
|
100
117
|
export function getTransitionGuards(from, to, track = "standard") {
|
|
101
118
|
// Natural forward edge on this track: derive guards fresh from the
|
|
102
119
|
// track-specific gate schema. `TRANSITION_RULES` collapses shared edges
|
package/dist/fs-utils.d.ts
CHANGED
|
@@ -18,7 +18,10 @@ export interface DirectoryLockOptions {
|
|
|
18
18
|
* The lock is removed in a finally block.
|
|
19
19
|
*/
|
|
20
20
|
export declare function withDirectoryLock<T>(lockPath: string, fn: () => Promise<T>, options?: DirectoryLockOptions): Promise<T>;
|
|
21
|
-
export
|
|
21
|
+
export interface WriteFileSafeOptions {
|
|
22
|
+
mode?: number;
|
|
23
|
+
}
|
|
24
|
+
export declare function writeFileSafe(filePath: string, content: string, options?: WriteFileSafeOptions): Promise<void>;
|
|
22
25
|
export declare function exists(filePath: string): Promise<boolean>;
|
|
23
26
|
export declare function removeIfExists(targetPath: string): Promise<void>;
|
|
24
27
|
export declare function resolveProjectPath(cwd: string, relativePath: string): string;
|
package/dist/fs-utils.js
CHANGED
|
@@ -27,6 +27,7 @@ export async function withDirectoryLock(lockPath, fn, options = {}) {
|
|
|
27
27
|
const staleAfterMs = options.staleAfterMs ?? 60_000;
|
|
28
28
|
await ensureDir(path.dirname(lockPath));
|
|
29
29
|
let acquired = false;
|
|
30
|
+
let lastError = null;
|
|
30
31
|
for (let attempt = 0; attempt < retries; attempt += 1) {
|
|
31
32
|
try {
|
|
32
33
|
await fs.mkdir(lockPath);
|
|
@@ -34,6 +35,7 @@ export async function withDirectoryLock(lockPath, fn, options = {}) {
|
|
|
34
35
|
break;
|
|
35
36
|
}
|
|
36
37
|
catch (error) {
|
|
38
|
+
lastError = error;
|
|
37
39
|
const code = error?.code;
|
|
38
40
|
if (code !== "EEXIST") {
|
|
39
41
|
throw error;
|
|
@@ -52,21 +54,32 @@ export async function withDirectoryLock(lockPath, fn, options = {}) {
|
|
|
52
54
|
}
|
|
53
55
|
}
|
|
54
56
|
if (!acquired) {
|
|
55
|
-
|
|
57
|
+
const details = lastError instanceof Error ? lastError.message : String(lastError);
|
|
58
|
+
throw new Error(`Failed to acquire lock: ${lockPath} (attempts=${retries}, retryDelayMs=${retryDelayMs}, staleAfterMs=${staleAfterMs}, lastError=${details})`);
|
|
56
59
|
}
|
|
57
60
|
try {
|
|
58
61
|
return await fn();
|
|
59
62
|
}
|
|
60
63
|
finally {
|
|
61
|
-
await fs.rm(lockPath, { recursive: true, force: true }).catch(() => {
|
|
64
|
+
await fs.rm(lockPath, { recursive: true, force: true }).catch((cleanupError) => {
|
|
65
|
+
// Lock cleanup failure should not shadow the original operation result,
|
|
66
|
+
// but keep a diagnostic breadcrumb for flaky FS environments.
|
|
67
|
+
const details = cleanupError instanceof Error ? cleanupError.message : String(cleanupError);
|
|
68
|
+
// eslint-disable-next-line no-console
|
|
69
|
+
console.warn(`cclaw lock cleanup failed for ${lockPath}: ${details}`);
|
|
70
|
+
});
|
|
62
71
|
}
|
|
63
72
|
}
|
|
64
|
-
export async function writeFileSafe(filePath, content) {
|
|
73
|
+
export async function writeFileSafe(filePath, content, options = {}) {
|
|
65
74
|
await ensureDir(path.dirname(filePath));
|
|
66
75
|
const tempPath = path.join(path.dirname(filePath), `.${path.basename(filePath)}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`);
|
|
67
|
-
|
|
76
|
+
const targetMode = options.mode;
|
|
77
|
+
await fs.writeFile(tempPath, content, { encoding: "utf8", ...(targetMode !== undefined ? { mode: targetMode } : {}) });
|
|
68
78
|
try {
|
|
69
79
|
await fs.rename(tempPath, filePath);
|
|
80
|
+
if (targetMode !== undefined) {
|
|
81
|
+
await fs.chmod(filePath, targetMode).catch(() => undefined);
|
|
82
|
+
}
|
|
70
83
|
}
|
|
71
84
|
catch (error) {
|
|
72
85
|
const code = error?.code;
|
|
@@ -78,6 +91,9 @@ export async function writeFileSafe(filePath, content) {
|
|
|
78
91
|
if (code === "EXDEV") {
|
|
79
92
|
try {
|
|
80
93
|
await fs.copyFile(tempPath, filePath);
|
|
94
|
+
if (targetMode !== undefined) {
|
|
95
|
+
await fs.chmod(filePath, targetMode).catch(() => undefined);
|
|
96
|
+
}
|
|
81
97
|
}
|
|
82
98
|
finally {
|
|
83
99
|
await fs.unlink(tempPath).catch(() => undefined);
|
package/dist/gate-evidence.d.ts
CHANGED
|
@@ -41,6 +41,8 @@ export interface ReconciliationNotice {
|
|
|
41
41
|
export interface ReconciliationNoticesPayload {
|
|
42
42
|
schemaVersion: number;
|
|
43
43
|
notices: ReconciliationNotice[];
|
|
44
|
+
parseOk: boolean;
|
|
45
|
+
schemaOk: boolean;
|
|
44
46
|
}
|
|
45
47
|
export interface ReconciliationNoticeBuckets {
|
|
46
48
|
activeBlocked: ReconciliationNotice[];
|
package/dist/gate-evidence.js
CHANGED
|
@@ -72,7 +72,9 @@ function reconciliationNoticesPath(projectRoot) {
|
|
|
72
72
|
function defaultReconciliationNoticesPayload() {
|
|
73
73
|
return {
|
|
74
74
|
schemaVersion: RECONCILIATION_NOTICES_SCHEMA_VERSION,
|
|
75
|
-
notices: []
|
|
75
|
+
notices: [],
|
|
76
|
+
parseOk: true,
|
|
77
|
+
schemaOk: true
|
|
76
78
|
};
|
|
77
79
|
}
|
|
78
80
|
function sanitizeReconciliationNotice(raw) {
|
|
@@ -104,6 +106,7 @@ export async function readReconciliationNotices(projectRoot) {
|
|
|
104
106
|
}
|
|
105
107
|
try {
|
|
106
108
|
const raw = JSON.parse(await fs.readFile(filePath, "utf8"));
|
|
109
|
+
const schemaOk = raw.schemaVersion === RECONCILIATION_NOTICES_SCHEMA_VERSION;
|
|
107
110
|
const notices = Array.isArray(raw.notices)
|
|
108
111
|
? raw.notices
|
|
109
112
|
.map((value) => sanitizeReconciliationNotice(value))
|
|
@@ -111,11 +114,17 @@ export async function readReconciliationNotices(projectRoot) {
|
|
|
111
114
|
: [];
|
|
112
115
|
return {
|
|
113
116
|
schemaVersion: RECONCILIATION_NOTICES_SCHEMA_VERSION,
|
|
114
|
-
notices
|
|
117
|
+
notices,
|
|
118
|
+
parseOk: true,
|
|
119
|
+
schemaOk
|
|
115
120
|
};
|
|
116
121
|
}
|
|
117
122
|
catch {
|
|
118
|
-
return
|
|
123
|
+
return {
|
|
124
|
+
...defaultReconciliationNoticesPayload(),
|
|
125
|
+
parseOk: false,
|
|
126
|
+
schemaOk: false
|
|
127
|
+
};
|
|
119
128
|
}
|
|
120
129
|
}
|
|
121
130
|
async function writeReconciliationNotices(projectRoot, payload) {
|
|
@@ -124,7 +133,7 @@ async function writeReconciliationNotices(projectRoot, payload) {
|
|
|
124
133
|
await writeFileSafe(filePath, `${JSON.stringify({
|
|
125
134
|
schemaVersion: RECONCILIATION_NOTICES_SCHEMA_VERSION,
|
|
126
135
|
notices: payload.notices
|
|
127
|
-
}, null, 2)}\n
|
|
136
|
+
}, null, 2)}\n`, { mode: 0o600 });
|
|
128
137
|
}
|
|
129
138
|
export function classifyReconciliationNotices(flowState, notices) {
|
|
130
139
|
const activeBlocked = [];
|