cclaw-cli 0.48.0 → 0.48.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/artifact-linter.d.ts +2 -2
- package/dist/artifact-linter.js +4 -10
- package/dist/constants.d.ts +6 -0
- package/dist/constants.js +11 -0
- package/dist/content/core-agents.d.ts +53 -1
- package/dist/content/core-agents.js +6 -0
- package/dist/content/observe.js +22 -1
- package/dist/content/opencode-plugin.js +5 -1
- package/dist/content/stage-schema.js +2 -0
- package/dist/content/stages/ship.js +2 -5
- package/dist/content/templates.js +13 -15
- package/dist/content/utility-skills.d.ts +7 -1
- package/dist/content/utility-skills.js +5 -0
- package/dist/delegation.d.ts +15 -1
- package/dist/delegation.js +24 -8
- package/dist/doctor.js +101 -18
- package/dist/feature-system.d.ts +11 -4
- package/dist/feature-system.js +52 -8
- package/dist/flow-state.d.ts +3 -1
- package/dist/flow-state.js +34 -3
- package/dist/fs-utils.d.ts +9 -0
- package/dist/fs-utils.js +46 -3
- package/dist/gate-evidence.d.ts +2 -0
- package/dist/gate-evidence.js +13 -4
- package/dist/gitignore.js +6 -3
- package/dist/harness-adapters.js +11 -1
- package/dist/install.js +41 -5
- package/dist/internal/advance-stage.js +45 -8
- package/dist/knowledge-store.js +2 -2
- package/dist/retro-gate.js +23 -14
- package/dist/run-archive.js +164 -93
- package/dist/run-persistence.d.ts +8 -1
- package/dist/run-persistence.js +13 -5
- package/dist/tdd-cycle.js +6 -1
- package/package.json +1 -1
package/dist/doctor.js
CHANGED
|
@@ -3,18 +3,18 @@ import path from "node:path";
|
|
|
3
3
|
import { execFile } from "node:child_process";
|
|
4
4
|
import { pathToFileURL } from "node:url";
|
|
5
5
|
import { promisify } from "node:util";
|
|
6
|
-
import { REQUIRED_DIRS, RUNTIME_ROOT } from "./constants.js";
|
|
6
|
+
import { REQUIRED_DIRS, RUNTIME_ROOT, UTILITY_COMMANDS } from "./constants.js";
|
|
7
7
|
import { CCLAW_AGENTS } from "./content/core-agents.js";
|
|
8
8
|
import { detectAdvancedKeys, 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,25 @@ 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
|
+
}
|
|
193
212
|
function normalizeOpenCodePluginEntry(entry) {
|
|
194
213
|
if (typeof entry === "string" && entry.trim().length > 0)
|
|
195
214
|
return entry.trim();
|
|
@@ -212,12 +231,16 @@ async function opencodeRegistrationCheck(projectRoot) {
|
|
|
212
231
|
path.join(projectRoot, ".opencode/opencode.json"),
|
|
213
232
|
path.join(projectRoot, ".opencode/opencode.jsonc")
|
|
214
233
|
];
|
|
234
|
+
const mismatches = [];
|
|
235
|
+
let foundAnyConfig = false;
|
|
215
236
|
for (const configPath of candidates) {
|
|
216
237
|
if (!(await exists(configPath))) {
|
|
217
238
|
continue;
|
|
218
239
|
}
|
|
240
|
+
foundAnyConfig = true;
|
|
219
241
|
const parsed = await readHookDocument(configPath);
|
|
220
242
|
if (!parsed) {
|
|
243
|
+
mismatches.push(`${path.relative(projectRoot, configPath)} is unreadable or invalid JSON`);
|
|
221
244
|
continue;
|
|
222
245
|
}
|
|
223
246
|
const plugins = Array.isArray(parsed.plugin) ? parsed.plugin : [];
|
|
@@ -225,7 +248,10 @@ async function opencodeRegistrationCheck(projectRoot) {
|
|
|
225
248
|
if (registered) {
|
|
226
249
|
return { ok: true, details: `${path.relative(projectRoot, configPath)} registers ${expected}` };
|
|
227
250
|
}
|
|
228
|
-
|
|
251
|
+
mismatches.push(`${path.relative(projectRoot, configPath)} missing plugin ${expected}`);
|
|
252
|
+
}
|
|
253
|
+
if (foundAnyConfig) {
|
|
254
|
+
return { ok: false, details: mismatches.join(" | ") };
|
|
229
255
|
}
|
|
230
256
|
return { ok: false, details: `No opencode.json/opencode.jsonc found with plugin ${expected}` };
|
|
231
257
|
}
|
|
@@ -244,6 +270,12 @@ async function opencodePluginRuntimeShapeCheck(projectRoot) {
|
|
|
244
270
|
};
|
|
245
271
|
}
|
|
246
272
|
const plugin = imported.default({ directory: projectRoot });
|
|
273
|
+
if (!plugin || typeof plugin !== "object" || Array.isArray(plugin)) {
|
|
274
|
+
return {
|
|
275
|
+
ok: false,
|
|
276
|
+
details: `${path.relative(projectRoot, pluginPath)} factory must return a plugin object`
|
|
277
|
+
};
|
|
278
|
+
}
|
|
247
279
|
const requiredHandlers = [
|
|
248
280
|
"event",
|
|
249
281
|
"tool.execute.before",
|
|
@@ -257,7 +289,6 @@ async function opencodePluginRuntimeShapeCheck(projectRoot) {
|
|
|
257
289
|
details: `${path.relative(projectRoot, pluginPath)} missing runtime handlers: ${missing.join(", ")}`
|
|
258
290
|
};
|
|
259
291
|
}
|
|
260
|
-
await plugin.event({ event: { type: "session.updated", data: {} } });
|
|
261
292
|
return {
|
|
262
293
|
ok: true,
|
|
263
294
|
details: `${path.relative(projectRoot, pluginPath)} exports compatible runtime handler shape`
|
|
@@ -547,8 +578,8 @@ export async function doctorChecks(projectRoot, options = {}) {
|
|
|
547
578
|
ok: agentsBlockOk,
|
|
548
579
|
details: `${agentsFile} must contain the managed cclaw marker block with routing, verification, and minimal detail pointer`
|
|
549
580
|
});
|
|
550
|
-
// Utility commands
|
|
551
|
-
for (const cmd of
|
|
581
|
+
// Utility commands — keep in sync with UTILITY_COMMANDS (src/constants.ts)
|
|
582
|
+
for (const cmd of UTILITY_COMMANDS) {
|
|
552
583
|
const cmdPath = path.join(projectRoot, RUNTIME_ROOT, "commands", `${cmd}.md`);
|
|
553
584
|
checks.push({
|
|
554
585
|
name: `utility_command:${cmd}`,
|
|
@@ -815,7 +846,7 @@ export async function doctorChecks(projectRoot, options = {}) {
|
|
|
815
846
|
});
|
|
816
847
|
checks.push({
|
|
817
848
|
name: `shim:codex:${skillName}:frontmatter`,
|
|
818
|
-
ok,
|
|
849
|
+
ok: frontmatterOk,
|
|
819
850
|
details: frontmatterOk
|
|
820
851
|
? `${skillPath} has \`name: ${skillName}\` frontmatter`
|
|
821
852
|
: ok
|
|
@@ -1289,10 +1320,28 @@ export async function doctorChecks(projectRoot, options = {}) {
|
|
|
1289
1320
|
details: modePath
|
|
1290
1321
|
});
|
|
1291
1322
|
}
|
|
1292
|
-
await ensureFeatureSystem(projectRoot);
|
|
1293
|
-
const activeFeature = await readActiveFeature(projectRoot);
|
|
1294
|
-
let flowState =
|
|
1295
|
-
|
|
1323
|
+
await ensureFeatureSystem(projectRoot, { repair: false });
|
|
1324
|
+
const activeFeature = await readActiveFeature(projectRoot, { repair: false });
|
|
1325
|
+
let flowState = createInitialFlowState();
|
|
1326
|
+
let flowStateCorruptError = null;
|
|
1327
|
+
try {
|
|
1328
|
+
flowState = await readFlowState(projectRoot, { repairFeatureSystem: false });
|
|
1329
|
+
}
|
|
1330
|
+
catch (error) {
|
|
1331
|
+
if (error instanceof CorruptFlowStateError) {
|
|
1332
|
+
flowStateCorruptError = error;
|
|
1333
|
+
checks.push({
|
|
1334
|
+
name: "flow_state:readable",
|
|
1335
|
+
ok: false,
|
|
1336
|
+
severity: "error",
|
|
1337
|
+
details: error.message
|
|
1338
|
+
});
|
|
1339
|
+
}
|
|
1340
|
+
else {
|
|
1341
|
+
throw error;
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
if (options.reconcileCurrentStageGates === true && !flowStateCorruptError) {
|
|
1296
1345
|
const reconciliation = await reconcileAndWriteCurrentStageGateCatalog(projectRoot);
|
|
1297
1346
|
if (reconciliation.wrote) {
|
|
1298
1347
|
flowState = {
|
|
@@ -1311,6 +1360,13 @@ export async function doctorChecks(projectRoot, options = {}) {
|
|
|
1311
1360
|
: `no gate reconciliation changes needed for stage "${reconciliation.stage}"`
|
|
1312
1361
|
});
|
|
1313
1362
|
}
|
|
1363
|
+
else if (options.reconcileCurrentStageGates === true && flowStateCorruptError) {
|
|
1364
|
+
checks.push({
|
|
1365
|
+
name: "gates:reconcile:writeback",
|
|
1366
|
+
ok: false,
|
|
1367
|
+
details: "skipped gate reconciliation because flow-state.json is corrupt"
|
|
1368
|
+
});
|
|
1369
|
+
}
|
|
1314
1370
|
const activeRunId = typeof flowState.activeRunId === "string" ? flowState.activeRunId.trim() : "";
|
|
1315
1371
|
checks.push({
|
|
1316
1372
|
name: "flow_state:active_run_id",
|
|
@@ -1318,6 +1374,15 @@ export async function doctorChecks(projectRoot, options = {}) {
|
|
|
1318
1374
|
details: `${RUNTIME_ROOT}/state/flow-state.json must include activeRunId`
|
|
1319
1375
|
});
|
|
1320
1376
|
const reconciliationNotices = await readReconciliationNotices(projectRoot);
|
|
1377
|
+
checks.push({
|
|
1378
|
+
name: "state:reconciliation_notices_parse",
|
|
1379
|
+
ok: reconciliationNotices.parseOk && reconciliationNotices.schemaOk,
|
|
1380
|
+
details: !reconciliationNotices.parseOk
|
|
1381
|
+
? `unable to parse ${RECONCILIATION_NOTICES_REL_PATH}; reset with \`cclaw sync\` or repair JSON by hand`
|
|
1382
|
+
: !reconciliationNotices.schemaOk
|
|
1383
|
+
? `${RECONCILIATION_NOTICES_REL_PATH} schemaVersion mismatch; expected ${reconciliationNotices.schemaVersion}`
|
|
1384
|
+
: `${RECONCILIATION_NOTICES_REL_PATH} parsed successfully`
|
|
1385
|
+
});
|
|
1321
1386
|
const noticeBuckets = classifyReconciliationNotices(flowState, reconciliationNotices.notices);
|
|
1322
1387
|
const formatNoticeList = (items) => items
|
|
1323
1388
|
.slice(0, 8)
|
|
@@ -1425,22 +1490,38 @@ export async function doctorChecks(projectRoot, options = {}) {
|
|
|
1425
1490
|
? "no TODO/TBD/FIXME placeholder markers found in active artifacts"
|
|
1426
1491
|
: `warning: placeholder markers detected in active artifacts (${artifactPlaceholderHits.join(", ")}). Clear before marking completion.`
|
|
1427
1492
|
});
|
|
1428
|
-
const
|
|
1429
|
-
const
|
|
1493
|
+
const activeMetaStatus = await readJsonObjectStatus(activeFeatureMetaPath(projectRoot));
|
|
1494
|
+
const worktreeRegistryStatus = await readJsonObjectStatus(worktreeRegistryPath(projectRoot));
|
|
1495
|
+
const features = await listFeatures(projectRoot, { repair: false });
|
|
1496
|
+
const worktreeRegistry = await readFeatureWorktreeRegistry(projectRoot, { repair: false });
|
|
1430
1497
|
const activeFeatureEntry = worktreeRegistry.entries.find((entry) => entry.featureId === activeFeature);
|
|
1431
1498
|
const activeFeatureWorkspacePath = activeFeatureEntry
|
|
1432
1499
|
? resolveFeatureWorkspacePath(projectRoot, activeFeatureEntry)
|
|
1433
1500
|
: "";
|
|
1434
1501
|
checks.push({
|
|
1435
1502
|
name: "state:active_feature_meta",
|
|
1436
|
-
ok:
|
|
1503
|
+
ok: activeMetaStatus.exists,
|
|
1437
1504
|
details: `${RUNTIME_ROOT}/state/active-feature.json must exist`
|
|
1438
1505
|
});
|
|
1506
|
+
checks.push({
|
|
1507
|
+
name: "state:active_feature_meta_valid_json",
|
|
1508
|
+
ok: activeMetaStatus.ok,
|
|
1509
|
+
details: activeMetaStatus.ok
|
|
1510
|
+
? `${RUNTIME_ROOT}/state/active-feature.json parsed successfully`
|
|
1511
|
+
: `${RUNTIME_ROOT}/state/active-feature.json is invalid: ${activeMetaStatus.error ?? "unknown error"}`
|
|
1512
|
+
});
|
|
1439
1513
|
checks.push({
|
|
1440
1514
|
name: "state:worktree_registry_exists",
|
|
1441
|
-
ok:
|
|
1515
|
+
ok: worktreeRegistryStatus.exists,
|
|
1442
1516
|
details: `${RUNTIME_ROOT}/state/worktrees.json must exist and track feature->worktree mapping`
|
|
1443
1517
|
});
|
|
1518
|
+
checks.push({
|
|
1519
|
+
name: "state:worktree_registry_valid_json",
|
|
1520
|
+
ok: worktreeRegistryStatus.ok,
|
|
1521
|
+
details: worktreeRegistryStatus.ok
|
|
1522
|
+
? `${RUNTIME_ROOT}/state/worktrees.json parsed successfully`
|
|
1523
|
+
: `${RUNTIME_ROOT}/state/worktrees.json is invalid: ${worktreeRegistryStatus.error ?? "unknown error"}`
|
|
1524
|
+
});
|
|
1444
1525
|
checks.push({
|
|
1445
1526
|
name: "state:active_feature_exists",
|
|
1446
1527
|
ok: features.includes(activeFeature),
|
|
@@ -1590,7 +1671,9 @@ export async function doctorChecks(projectRoot, options = {}) {
|
|
|
1590
1671
|
ok: await exists(path.join(projectRoot, RUNTIME_ROOT, "runs")),
|
|
1591
1672
|
details: `${RUNTIME_ROOT}/runs must exist for archived feature snapshots`
|
|
1592
1673
|
});
|
|
1593
|
-
const delegation = await checkMandatoryDelegations(projectRoot, flowState.currentStage
|
|
1674
|
+
const delegation = await checkMandatoryDelegations(projectRoot, flowState.currentStage, {
|
|
1675
|
+
repairFeatureSystem: false
|
|
1676
|
+
});
|
|
1594
1677
|
const missingEvidenceNote = delegation.missingEvidence && delegation.missingEvidence.length > 0
|
|
1595
1678
|
? ` (role-switch rows without evidenceRefs: ${delegation.missingEvidence.join(", ")})`
|
|
1596
1679
|
: "";
|
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
|
@@ -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;
|
|
97
|
-
export declare function
|
|
98
|
+
export declare function getAvailableTransitions(from: FlowStage, track?: FlowTrack): TransitionRule[];
|
|
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,7 +100,35 @@ 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
|
}
|
|
100
|
-
export function
|
|
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
|
+
}
|
|
117
|
+
export function getTransitionGuards(from, to, track = "standard") {
|
|
118
|
+
// Natural forward edge on this track: derive guards fresh from the
|
|
119
|
+
// track-specific gate schema. `TRANSITION_RULES` collapses shared edges
|
|
120
|
+
// across tracks (first-registered wins), so reading guards directly
|
|
121
|
+
// from the track-aware schema avoids silently dropping gates that only
|
|
122
|
+
// the current track requires (e.g. `tdd_traceable_to_plan` on standard
|
|
123
|
+
// gets lost if quick was registered first).
|
|
124
|
+
const ordered = TRACK_STAGES[track];
|
|
125
|
+
const fromIdx = ordered.indexOf(from);
|
|
126
|
+
if (fromIdx >= 0 && ordered[fromIdx + 1] === to) {
|
|
127
|
+
return stageGateIds(from, track);
|
|
128
|
+
}
|
|
129
|
+
// Non-neighbour edges (e.g. `review -> tdd` with `review_verdict_blocked`)
|
|
130
|
+
// carry special guards not derivable from a stage's gate catalog; fall
|
|
131
|
+
// back to the pre-computed rule table.
|
|
101
132
|
const match = TRANSITION_RULES.find((rule) => rule.from === from && rule.to === to);
|
|
102
133
|
return match ? [...match.guards] : [];
|
|
103
134
|
}
|
package/dist/fs-utils.d.ts
CHANGED
|
@@ -1,4 +1,13 @@
|
|
|
1
1
|
export declare function ensureDir(dirPath: string): Promise<void>;
|
|
2
|
+
/**
|
|
3
|
+
* Strip a leading UTF-8 BOM (U+FEFF) if present. Many editors (VS Code on
|
|
4
|
+
* Windows, Notepad, some CI tools) silently prepend a BOM when saving
|
|
5
|
+
* UTF-8; when the file is then split on `\n` the first line keeps the
|
|
6
|
+
* invisible BOM and `JSON.parse` rejects it, which caused the first
|
|
7
|
+
* knowledge.jsonl entry to be silently dropped on load. Treat BOM as a
|
|
8
|
+
* no-op at read time so the rest of the pipeline sees clean UTF-8.
|
|
9
|
+
*/
|
|
10
|
+
export declare function stripBom(text: string): string;
|
|
2
11
|
export interface DirectoryLockOptions {
|
|
3
12
|
retries?: number;
|
|
4
13
|
retryDelayMs?: number;
|
package/dist/fs-utils.js
CHANGED
|
@@ -3,6 +3,17 @@ import path from "node:path";
|
|
|
3
3
|
export async function ensureDir(dirPath) {
|
|
4
4
|
await fs.mkdir(dirPath, { recursive: true });
|
|
5
5
|
}
|
|
6
|
+
/**
|
|
7
|
+
* Strip a leading UTF-8 BOM (U+FEFF) if present. Many editors (VS Code on
|
|
8
|
+
* Windows, Notepad, some CI tools) silently prepend a BOM when saving
|
|
9
|
+
* UTF-8; when the file is then split on `\n` the first line keeps the
|
|
10
|
+
* invisible BOM and `JSON.parse` rejects it, which caused the first
|
|
11
|
+
* knowledge.jsonl entry to be silently dropped on load. Treat BOM as a
|
|
12
|
+
* no-op at read time so the rest of the pipeline sees clean UTF-8.
|
|
13
|
+
*/
|
|
14
|
+
export function stripBom(text) {
|
|
15
|
+
return text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;
|
|
16
|
+
}
|
|
6
17
|
function sleep(ms) {
|
|
7
18
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
8
19
|
}
|
|
@@ -16,6 +27,7 @@ export async function withDirectoryLock(lockPath, fn, options = {}) {
|
|
|
16
27
|
const staleAfterMs = options.staleAfterMs ?? 60_000;
|
|
17
28
|
await ensureDir(path.dirname(lockPath));
|
|
18
29
|
let acquired = false;
|
|
30
|
+
let lastError = null;
|
|
19
31
|
for (let attempt = 0; attempt < retries; attempt += 1) {
|
|
20
32
|
try {
|
|
21
33
|
await fs.mkdir(lockPath);
|
|
@@ -23,6 +35,7 @@ export async function withDirectoryLock(lockPath, fn, options = {}) {
|
|
|
23
35
|
break;
|
|
24
36
|
}
|
|
25
37
|
catch (error) {
|
|
38
|
+
lastError = error;
|
|
26
39
|
const code = error?.code;
|
|
27
40
|
if (code !== "EEXIST") {
|
|
28
41
|
throw error;
|
|
@@ -41,20 +54,50 @@ export async function withDirectoryLock(lockPath, fn, options = {}) {
|
|
|
41
54
|
}
|
|
42
55
|
}
|
|
43
56
|
if (!acquired) {
|
|
44
|
-
|
|
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})`);
|
|
45
59
|
}
|
|
46
60
|
try {
|
|
47
61
|
return await fn();
|
|
48
62
|
}
|
|
49
63
|
finally {
|
|
50
|
-
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
|
+
});
|
|
51
71
|
}
|
|
52
72
|
}
|
|
53
73
|
export async function writeFileSafe(filePath, content) {
|
|
54
74
|
await ensureDir(path.dirname(filePath));
|
|
55
75
|
const tempPath = path.join(path.dirname(filePath), `.${path.basename(filePath)}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`);
|
|
56
76
|
await fs.writeFile(tempPath, content, "utf8");
|
|
57
|
-
|
|
77
|
+
try {
|
|
78
|
+
await fs.rename(tempPath, filePath);
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
const code = error?.code;
|
|
82
|
+
// `rename` fails with EXDEV when the temp file and target live on
|
|
83
|
+
// different filesystems (container bind mounts, tmpfs + rootfs,
|
|
84
|
+
// cross-volume setups). Fall back to copy + unlink so atomic writes
|
|
85
|
+
// still work — copyFile is not fully atomic but is the best we can
|
|
86
|
+
// do across devices, and we remove the temp even if copy fails.
|
|
87
|
+
if (code === "EXDEV") {
|
|
88
|
+
try {
|
|
89
|
+
await fs.copyFile(tempPath, filePath);
|
|
90
|
+
}
|
|
91
|
+
finally {
|
|
92
|
+
await fs.unlink(tempPath).catch(() => undefined);
|
|
93
|
+
}
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
// Other errors: try to clean up the temp to avoid littering the
|
|
97
|
+
// directory with orphaned `.tmp-<pid>-*` files, then rethrow.
|
|
98
|
+
await fs.unlink(tempPath).catch(() => undefined);
|
|
99
|
+
throw error;
|
|
100
|
+
}
|
|
58
101
|
}
|
|
59
102
|
export async function exists(filePath) {
|
|
60
103
|
try {
|
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) {
|
|
@@ -212,7 +221,7 @@ export async function verifyCurrentStageGateEvidence(projectRoot, flowState) {
|
|
|
212
221
|
const artifactPresent = await currentStageArtifactExists(projectRoot, stage, flowState.track);
|
|
213
222
|
const shouldValidateArtifact = artifactPresent || catalog.passed.length > 0 || flowState.completedStages.includes(stage);
|
|
214
223
|
if (shouldValidateArtifact) {
|
|
215
|
-
const lint = await lintArtifact(projectRoot, stage);
|
|
224
|
+
const lint = await lintArtifact(projectRoot, stage, flowState.track);
|
|
216
225
|
if (!lint.passed) {
|
|
217
226
|
const failedRequired = lint.findings
|
|
218
227
|
.filter((finding) => finding.required && !finding.found)
|
package/dist/gitignore.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { REQUIRED_GITIGNORE_PATTERNS } from "./constants.js";
|
|
4
|
-
import { exists } from "./fs-utils.js";
|
|
4
|
+
import { exists, writeFileSafe } from "./fs-utils.js";
|
|
5
5
|
export async function ensureGitignore(projectRoot) {
|
|
6
6
|
const gitignorePath = path.join(projectRoot, ".gitignore");
|
|
7
7
|
const currentContent = (await exists(gitignorePath))
|
|
@@ -15,7 +15,10 @@ export async function ensureGitignore(projectRoot) {
|
|
|
15
15
|
}
|
|
16
16
|
const base = lines.join("\n").replace(/\s+$/u, "");
|
|
17
17
|
const suffix = `${base.length > 0 ? "\n" : ""}${missing.join("\n")}\n`;
|
|
18
|
-
|
|
18
|
+
// `writeFileSafe` performs a tmp-file + rename so a crash mid-write
|
|
19
|
+
// cannot leave `.gitignore` in a half-written state; the previous
|
|
20
|
+
// direct `fs.writeFile` could truncate the file on SIGKILL.
|
|
21
|
+
await writeFileSafe(gitignorePath, `${base}${suffix}`);
|
|
19
22
|
}
|
|
20
23
|
export async function removeGitignorePatterns(projectRoot) {
|
|
21
24
|
const gitignorePath = path.join(projectRoot, ".gitignore");
|
|
@@ -30,7 +33,7 @@ export async function removeGitignorePatterns(projectRoot) {
|
|
|
30
33
|
await fs.rm(gitignorePath, { force: true });
|
|
31
34
|
}
|
|
32
35
|
else {
|
|
33
|
-
await
|
|
36
|
+
await writeFileSafe(gitignorePath, `${result}\n`);
|
|
34
37
|
}
|
|
35
38
|
}
|
|
36
39
|
export async function gitignoreHasRequiredPatterns(projectRoot) {
|
package/dist/harness-adapters.js
CHANGED
|
@@ -54,7 +54,12 @@ const LEGACY_CODEX_SKILL_NAMES = [
|
|
|
54
54
|
"cclaw-cc-next",
|
|
55
55
|
"cclaw-cc-view",
|
|
56
56
|
"cclaw-cc-ops",
|
|
57
|
-
"cclaw-cc-ideate"
|
|
57
|
+
"cclaw-cc-ideate",
|
|
58
|
+
// Pre-v0.40 installed `/cc-learn` as a top-level skill before it was
|
|
59
|
+
// folded into `/cc-ops`. Without this entry the orphan stays behind
|
|
60
|
+
// after upgrade and Codex lists both the new in-thread workflow and
|
|
61
|
+
// the legacy slash command.
|
|
62
|
+
"cclaw-cc-learn"
|
|
58
63
|
];
|
|
59
64
|
/**
|
|
60
65
|
* Shims that older cclaw versions installed as top-level slash commands but
|
|
@@ -417,6 +422,11 @@ what the hook surface does and does not cover.
|
|
|
417
422
|
are **not** gated by hooks — read
|
|
418
423
|
\`.cclaw/references/harnesses/codex-playbook.md\` for what cclaw
|
|
419
424
|
substitutes with in-turn agent steps for those call classes.
|
|
425
|
+
- Codex's \`SessionStart\` matcher only supports \`startup|resume\`. Claude
|
|
426
|
+
and Cursor also fire on \`clear\` and \`compact\`, so mid-session
|
|
427
|
+
context resets there re-inject cclaw's bootstrap automatically. In
|
|
428
|
+
Codex you must re-announce the active stage yourself after any
|
|
429
|
+
\`/clear\` or compaction — the skill does not reload implicitly.
|
|
420
430
|
`;
|
|
421
431
|
}
|
|
422
432
|
function codexSkillMarkdown(command, skillName, skillFolder, commandFile) {
|