cclaw-cli 0.51.30 → 0.55.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.
Files changed (142) hide show
  1. package/README.md +22 -16
  2. package/dist/artifact-linter/brainstorm.d.ts +2 -0
  3. package/dist/artifact-linter/brainstorm.js +245 -0
  4. package/dist/artifact-linter/design.d.ts +2 -0
  5. package/dist/artifact-linter/design.js +323 -0
  6. package/dist/artifact-linter/plan.d.ts +2 -0
  7. package/dist/artifact-linter/plan.js +162 -0
  8. package/dist/artifact-linter/review-army.d.ts +24 -0
  9. package/dist/artifact-linter/review-army.js +365 -0
  10. package/dist/artifact-linter/review.d.ts +2 -0
  11. package/dist/artifact-linter/review.js +65 -0
  12. package/dist/artifact-linter/scope.d.ts +2 -0
  13. package/dist/artifact-linter/scope.js +115 -0
  14. package/dist/artifact-linter/shared.d.ts +246 -0
  15. package/dist/artifact-linter/shared.js +1488 -0
  16. package/dist/artifact-linter/ship.d.ts +2 -0
  17. package/dist/artifact-linter/ship.js +46 -0
  18. package/dist/artifact-linter/spec.d.ts +2 -0
  19. package/dist/artifact-linter/spec.js +108 -0
  20. package/dist/artifact-linter/tdd.d.ts +2 -0
  21. package/dist/artifact-linter/tdd.js +124 -0
  22. package/dist/artifact-linter.d.ts +4 -76
  23. package/dist/artifact-linter.js +56 -2949
  24. package/dist/cli.d.ts +1 -6
  25. package/dist/cli.js +4 -159
  26. package/dist/codex-feature-flag.d.ts +1 -1
  27. package/dist/codex-feature-flag.js +1 -1
  28. package/dist/config.d.ts +3 -2
  29. package/dist/config.js +67 -3
  30. package/dist/constants.d.ts +1 -7
  31. package/dist/constants.js +9 -15
  32. package/dist/content/cancel-command.js +2 -2
  33. package/dist/content/closeout-guidance.js +10 -7
  34. package/dist/content/core-agents.d.ts +18 -0
  35. package/dist/content/core-agents.js +46 -2
  36. package/dist/content/decision-protocol.d.ts +1 -1
  37. package/dist/content/decision-protocol.js +1 -1
  38. package/dist/content/examples.js +6 -6
  39. package/dist/content/harness-doc.js +20 -2
  40. package/dist/content/hook-inline-snippets.d.ts +17 -4
  41. package/dist/content/hook-inline-snippets.js +218 -5
  42. package/dist/content/hook-manifest.d.ts +2 -2
  43. package/dist/content/hook-manifest.js +2 -2
  44. package/dist/content/hooks.d.ts +1 -0
  45. package/dist/content/hooks.js +32 -137
  46. package/dist/content/idea-command.d.ts +8 -0
  47. package/dist/content/{ideate-command.js → idea-command.js} +57 -50
  48. package/dist/content/idea-frames.d.ts +31 -0
  49. package/dist/content/{ideate-frames.js → idea-frames.js} +9 -9
  50. package/dist/content/idea-ranking.d.ts +25 -0
  51. package/dist/content/{ideate-ranking.js → idea-ranking.js} +5 -5
  52. package/dist/content/iron-laws.d.ts +0 -1
  53. package/dist/content/iron-laws.js +31 -16
  54. package/dist/content/learnings.js +1 -1
  55. package/dist/content/meta-skill.js +7 -7
  56. package/dist/content/node-hooks.d.ts +10 -0
  57. package/dist/content/node-hooks.js +43 -9
  58. package/dist/content/opencode-plugin.js +3 -3
  59. package/dist/content/skills.js +19 -7
  60. package/dist/content/stage-schema.js +44 -2
  61. package/dist/content/stages/_lint-metadata/index.js +26 -2
  62. package/dist/content/stages/brainstorm.js +13 -7
  63. package/dist/content/stages/design.js +16 -11
  64. package/dist/content/stages/plan.js +7 -4
  65. package/dist/content/stages/review.js +4 -4
  66. package/dist/content/stages/schema-types.d.ts +1 -1
  67. package/dist/content/stages/scope.js +15 -12
  68. package/dist/content/stages/ship.js +2 -2
  69. package/dist/content/stages/spec.js +9 -3
  70. package/dist/content/stages/tdd.js +14 -4
  71. package/dist/content/start-command.js +11 -10
  72. package/dist/content/status-command.js +3 -3
  73. package/dist/content/subagents.js +60 -6
  74. package/dist/content/templates.d.ts +1 -1
  75. package/dist/content/templates.js +102 -150
  76. package/dist/content/tree-command.js +2 -2
  77. package/dist/content/utility-skills.d.ts +2 -2
  78. package/dist/content/utility-skills.js +2 -2
  79. package/dist/content/view-command.js +4 -2
  80. package/dist/delegation.d.ts +2 -0
  81. package/dist/delegation.js +2 -1
  82. package/dist/early-loop.d.ts +66 -0
  83. package/dist/early-loop.js +275 -0
  84. package/dist/gate-evidence.d.ts +8 -0
  85. package/dist/gate-evidence.js +141 -5
  86. package/dist/harness-adapters.d.ts +2 -2
  87. package/dist/harness-adapters.js +47 -18
  88. package/dist/install.js +153 -29
  89. package/dist/internal/advance-stage/advance.d.ts +50 -0
  90. package/dist/internal/advance-stage/advance.js +480 -0
  91. package/dist/internal/advance-stage/cancel-run.d.ts +8 -0
  92. package/dist/internal/advance-stage/cancel-run.js +19 -0
  93. package/dist/internal/advance-stage/flow-state-coercion.d.ts +3 -0
  94. package/dist/internal/advance-stage/flow-state-coercion.js +81 -0
  95. package/dist/internal/advance-stage/helpers.d.ts +14 -0
  96. package/dist/internal/advance-stage/helpers.js +145 -0
  97. package/dist/internal/advance-stage/hook.d.ts +8 -0
  98. package/dist/internal/advance-stage/hook.js +40 -0
  99. package/dist/internal/advance-stage/parsers.d.ts +54 -0
  100. package/dist/internal/advance-stage/parsers.js +307 -0
  101. package/dist/internal/advance-stage/review-loop.d.ts +7 -0
  102. package/dist/internal/advance-stage/review-loop.js +170 -0
  103. package/dist/internal/advance-stage/rewind.d.ts +14 -0
  104. package/dist/internal/advance-stage/rewind.js +108 -0
  105. package/dist/internal/advance-stage/start-flow.d.ts +11 -0
  106. package/dist/internal/advance-stage/start-flow.js +136 -0
  107. package/dist/internal/advance-stage/verify.d.ts +29 -0
  108. package/dist/internal/advance-stage/verify.js +225 -0
  109. package/dist/internal/advance-stage.js +21 -1470
  110. package/dist/internal/compound-readiness.d.ts +1 -1
  111. package/dist/internal/compound-readiness.js +2 -2
  112. package/dist/internal/early-loop-status.d.ts +7 -0
  113. package/dist/internal/early-loop-status.js +90 -0
  114. package/dist/internal/runtime-integrity.d.ts +7 -0
  115. package/dist/internal/runtime-integrity.js +288 -0
  116. package/dist/internal/tdd-red-evidence.js +1 -1
  117. package/dist/knowledge-store.d.ts +3 -8
  118. package/dist/knowledge-store.js +16 -29
  119. package/dist/managed-resources.js +24 -2
  120. package/dist/policy.js +4 -6
  121. package/dist/run-archive.d.ts +1 -1
  122. package/dist/run-archive.js +12 -12
  123. package/dist/run-persistence.js +111 -11
  124. package/dist/tdd-cycle.d.ts +3 -3
  125. package/dist/tdd-cycle.js +1 -1
  126. package/dist/types.d.ts +18 -10
  127. package/package.json +1 -1
  128. package/dist/content/ideate-command.d.ts +0 -8
  129. package/dist/content/ideate-frames.d.ts +0 -31
  130. package/dist/content/ideate-ranking.d.ts +0 -25
  131. package/dist/content/next-command.d.ts +0 -20
  132. package/dist/content/next-command.js +0 -298
  133. package/dist/content/seed-shelf.d.ts +0 -36
  134. package/dist/content/seed-shelf.js +0 -301
  135. package/dist/content/stage-common-guidance.d.ts +0 -1
  136. package/dist/content/stage-common-guidance.js +0 -106
  137. package/dist/doctor-registry.d.ts +0 -10
  138. package/dist/doctor-registry.js +0 -186
  139. package/dist/doctor.d.ts +0 -17
  140. package/dist/doctor.js +0 -2201
  141. package/dist/internal/hook-manifest.d.ts +0 -16
  142. package/dist/internal/hook-manifest.js +0 -77
@@ -0,0 +1,66 @@
1
+ export declare const EARLY_LOOP_STAGES: readonly ["brainstorm", "scope", "design"];
2
+ export type EarlyLoopStage = (typeof EARLY_LOOP_STAGES)[number];
3
+ export type EarlyLoopConcernSeverity = "critical" | "important" | "suggestion";
4
+ export interface EarlyLoopConcern {
5
+ id: string;
6
+ severity: EarlyLoopConcernSeverity;
7
+ locator: string;
8
+ summary: string;
9
+ firstSeenIteration: number;
10
+ lastSeenIteration: number;
11
+ resolvedAtIteration?: number;
12
+ }
13
+ export interface EarlyLoopStatus {
14
+ schemaVersion: 1;
15
+ stage: EarlyLoopStage;
16
+ runId: string;
17
+ iteration: number;
18
+ maxIterations: number;
19
+ openConcerns: EarlyLoopConcern[];
20
+ resolvedConcerns: EarlyLoopConcern[];
21
+ lastSeenConcernIds: string[];
22
+ convergenceTripped: boolean;
23
+ escalationReason?: string;
24
+ lastUpdatedAt: string;
25
+ }
26
+ export interface EarlyLoopLogConcern {
27
+ id: string;
28
+ severity: EarlyLoopConcernSeverity;
29
+ locator: string;
30
+ summary: string;
31
+ }
32
+ export interface EarlyLoopLogEntry {
33
+ ts: string;
34
+ runId: string;
35
+ stage: string;
36
+ iteration?: number;
37
+ concerns: EarlyLoopLogConcern[];
38
+ resolvedConcernIds: string[];
39
+ }
40
+ export interface EarlyLoopParseIssue {
41
+ lineNumber: number;
42
+ reason: string;
43
+ rawLine: string;
44
+ }
45
+ export interface ParseEarlyLoopLogOptions {
46
+ issues?: EarlyLoopParseIssue[];
47
+ strict?: boolean;
48
+ }
49
+ export interface DeriveEarlyLoopStatusOptions {
50
+ stage: EarlyLoopStage;
51
+ runId: string;
52
+ maxIterations?: number;
53
+ now?: Date;
54
+ }
55
+ export interface ComputeEarlyLoopStatusOptions {
56
+ maxIterations?: number;
57
+ now?: Date;
58
+ parseIssues?: EarlyLoopParseIssue[];
59
+ strictParse?: boolean;
60
+ }
61
+ export declare function isEarlyLoopStage(value: unknown): value is EarlyLoopStage;
62
+ export declare function normalizeEarlyLoopMaxIterations(value: number | undefined): number;
63
+ export declare function parseEarlyLoopLog(text: string, options?: ParseEarlyLoopLogOptions): EarlyLoopLogEntry[];
64
+ export declare function deriveEarlyLoopStatus(entries: EarlyLoopLogEntry[], options: DeriveEarlyLoopStatusOptions): EarlyLoopStatus;
65
+ export declare function computeEarlyLoopStatus(stage: EarlyLoopStage, runId: string, concernsLogPath: string, options?: ComputeEarlyLoopStatusOptions): Promise<EarlyLoopStatus>;
66
+ export declare function formatEarlyLoopStatusLine(status: EarlyLoopStatus): string;
@@ -0,0 +1,275 @@
1
+ import fs from "node:fs/promises";
2
+ import { DEFAULT_EARLY_LOOP_MAX_ITERATIONS } from "./config.js";
3
+ export const EARLY_LOOP_STAGES = ["brainstorm", "scope", "design"];
4
+ const CONCERN_ID_PREFIX = "C-";
5
+ function severityWeight(severity) {
6
+ if (severity === "critical")
7
+ return 3;
8
+ if (severity === "important")
9
+ return 2;
10
+ return 1;
11
+ }
12
+ function normalizeSeverity(value) {
13
+ if (value === "critical" || value === "important" || value === "suggestion") {
14
+ return value;
15
+ }
16
+ return "important";
17
+ }
18
+ function normalizeText(value, fallback) {
19
+ if (typeof value !== "string")
20
+ return fallback;
21
+ const trimmed = value.trim();
22
+ return trimmed.length > 0 ? trimmed : fallback;
23
+ }
24
+ function stableConcernFallbackId(locator, summary) {
25
+ const seed = `${locator}::${summary}`.trim().toLowerCase();
26
+ let hash = 0;
27
+ for (let index = 0; index < seed.length; index += 1) {
28
+ hash = (Math.imul(31, hash) + seed.charCodeAt(index)) >>> 0;
29
+ }
30
+ return `${CONCERN_ID_PREFIX}${hash.toString(16).padStart(8, "0")}`;
31
+ }
32
+ function normalizeConcernId(id, locator, summary) {
33
+ if (typeof id === "string") {
34
+ const trimmed = id.trim();
35
+ if (trimmed.length > 0) {
36
+ return trimmed;
37
+ }
38
+ }
39
+ return stableConcernFallbackId(locator, summary);
40
+ }
41
+ function normalizeConcerns(value) {
42
+ if (!Array.isArray(value))
43
+ return [];
44
+ const concerns = [];
45
+ for (const row of value) {
46
+ if (!row || typeof row !== "object" || Array.isArray(row))
47
+ continue;
48
+ const typed = row;
49
+ const locator = normalizeText(typed.locator, "unknown-location");
50
+ const summary = normalizeText(typed.summary, "missing-summary");
51
+ concerns.push({
52
+ id: normalizeConcernId(typed.id, locator, summary),
53
+ severity: normalizeSeverity(typed.severity),
54
+ locator,
55
+ summary
56
+ });
57
+ }
58
+ return concerns;
59
+ }
60
+ function normalizeResolvedConcernIds(value) {
61
+ if (!Array.isArray(value))
62
+ return [];
63
+ return value
64
+ .filter((entry) => typeof entry === "string")
65
+ .map((entry) => entry.trim())
66
+ .filter((entry) => entry.length > 0);
67
+ }
68
+ export function isEarlyLoopStage(value) {
69
+ return typeof value === "string" && EARLY_LOOP_STAGES.includes(value);
70
+ }
71
+ export function normalizeEarlyLoopMaxIterations(value) {
72
+ if (typeof value === "number" && Number.isInteger(value) && value >= 1) {
73
+ return value;
74
+ }
75
+ return DEFAULT_EARLY_LOOP_MAX_ITERATIONS;
76
+ }
77
+ export function parseEarlyLoopLog(text, options = {}) {
78
+ const strict = options.strict === true;
79
+ const issues = options.issues;
80
+ const normalized = text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;
81
+ const lines = normalized.split(/\r?\n/u);
82
+ const entries = [];
83
+ for (let index = 0; index < lines.length; index += 1) {
84
+ const raw = lines[index] ?? "";
85
+ const line = raw.trim();
86
+ if (line.length === 0)
87
+ continue;
88
+ const lineNumber = index + 1;
89
+ let parsed;
90
+ try {
91
+ parsed = JSON.parse(line);
92
+ }
93
+ catch (error) {
94
+ issues?.push({
95
+ lineNumber,
96
+ reason: `json-parse-failed: ${error instanceof Error ? error.message : String(error)}`,
97
+ rawLine: raw
98
+ });
99
+ continue;
100
+ }
101
+ const runId = normalizeText(parsed.runId, "");
102
+ const stage = normalizeText(parsed.stage, "");
103
+ const concerns = normalizeConcerns(parsed.concerns);
104
+ const resolvedConcernIds = normalizeResolvedConcernIds(parsed.resolvedConcernIds);
105
+ const iteration = typeof parsed.iteration === "number" &&
106
+ Number.isInteger(parsed.iteration) &&
107
+ parsed.iteration >= 1
108
+ ? parsed.iteration
109
+ : undefined;
110
+ if (strict) {
111
+ const missing = [];
112
+ if (runId.length === 0)
113
+ missing.push("runId");
114
+ if (stage.length === 0)
115
+ missing.push("stage");
116
+ if (concerns.length === 0 && resolvedConcernIds.length === 0) {
117
+ missing.push("concerns/resolvedConcernIds");
118
+ }
119
+ if (missing.length > 0) {
120
+ issues?.push({
121
+ lineNumber,
122
+ reason: `missing-required-fields: ${missing.join(",")}`,
123
+ rawLine: raw
124
+ });
125
+ continue;
126
+ }
127
+ }
128
+ entries.push({
129
+ ts: normalizeText(parsed.ts, ""),
130
+ runId: runId.length > 0 ? runId : "active",
131
+ stage: stage.length > 0 ? stage : "brainstorm",
132
+ iteration,
133
+ concerns,
134
+ resolvedConcernIds
135
+ });
136
+ }
137
+ return entries;
138
+ }
139
+ function sortConcerns(a, b) {
140
+ const severityDiff = severityWeight(b.severity) - severityWeight(a.severity);
141
+ if (severityDiff !== 0)
142
+ return severityDiff;
143
+ if (a.firstSeenIteration !== b.firstSeenIteration) {
144
+ return a.firstSeenIteration - b.firstSeenIteration;
145
+ }
146
+ if (a.lastSeenIteration !== b.lastSeenIteration) {
147
+ return a.lastSeenIteration - b.lastSeenIteration;
148
+ }
149
+ return a.id.localeCompare(b.id, "en");
150
+ }
151
+ export function deriveEarlyLoopStatus(entries, options) {
152
+ const maxIterations = normalizeEarlyLoopMaxIterations(options.maxIterations);
153
+ const concerns = new Map();
154
+ const filtered = entries.filter((entry) => entry.runId === options.runId && entry.stage === options.stage);
155
+ let previousConcernSnapshotKey = "";
156
+ let sameConcernStreak = 0;
157
+ let convergenceTripped = false;
158
+ let escalationReason;
159
+ let currentIteration = 0;
160
+ let lastSeenConcernIds = [];
161
+ for (const entry of filtered) {
162
+ currentIteration += 1;
163
+ const iteration = entry.iteration ?? currentIteration;
164
+ const seenThisIteration = new Set();
165
+ for (const concern of entry.concerns) {
166
+ seenThisIteration.add(concern.id);
167
+ const existing = concerns.get(concern.id);
168
+ if (!existing) {
169
+ concerns.set(concern.id, {
170
+ id: concern.id,
171
+ severity: concern.severity,
172
+ locator: concern.locator,
173
+ summary: concern.summary,
174
+ firstSeenIteration: iteration,
175
+ lastSeenIteration: iteration
176
+ });
177
+ continue;
178
+ }
179
+ existing.lastSeenIteration = iteration;
180
+ existing.locator = concern.locator;
181
+ existing.summary = concern.summary;
182
+ if (severityWeight(concern.severity) >= severityWeight(existing.severity)) {
183
+ existing.severity = concern.severity;
184
+ }
185
+ delete existing.resolvedAtIteration;
186
+ }
187
+ for (const concernId of entry.resolvedConcernIds) {
188
+ const existing = concerns.get(concernId);
189
+ if (!existing)
190
+ continue;
191
+ if (seenThisIteration.has(concernId))
192
+ continue;
193
+ if (existing.resolvedAtIteration === undefined) {
194
+ existing.resolvedAtIteration = iteration;
195
+ }
196
+ }
197
+ for (const concern of concerns.values()) {
198
+ if (concern.resolvedAtIteration !== undefined)
199
+ continue;
200
+ if (seenThisIteration.has(concern.id))
201
+ continue;
202
+ concern.resolvedAtIteration = iteration;
203
+ }
204
+ const openConcernIds = Array.from(concerns.values())
205
+ .filter((concern) => concern.resolvedAtIteration === undefined)
206
+ .map((concern) => concern.id)
207
+ .sort((a, b) => a.localeCompare(b, "en"));
208
+ lastSeenConcernIds = openConcernIds;
209
+ const snapshotKey = openConcernIds.join("|");
210
+ if (snapshotKey.length > 0 && snapshotKey === previousConcernSnapshotKey) {
211
+ sameConcernStreak += 1;
212
+ if (!convergenceTripped && sameConcernStreak >= 2) {
213
+ convergenceTripped = true;
214
+ escalationReason = `same concerns ${sameConcernStreak} iterations in a row`;
215
+ }
216
+ }
217
+ else {
218
+ sameConcernStreak = snapshotKey.length > 0 ? 1 : 0;
219
+ }
220
+ previousConcernSnapshotKey = snapshotKey;
221
+ }
222
+ const openConcerns = Array.from(concerns.values())
223
+ .filter((concern) => concern.resolvedAtIteration === undefined)
224
+ .sort(sortConcerns);
225
+ const resolvedConcerns = Array.from(concerns.values())
226
+ .filter((concern) => concern.resolvedAtIteration !== undefined)
227
+ .sort((a, b) => {
228
+ if (a.resolvedAtIteration !== b.resolvedAtIteration) {
229
+ return a.resolvedAtIteration - b.resolvedAtIteration;
230
+ }
231
+ return sortConcerns(a, b);
232
+ });
233
+ if (!convergenceTripped && openConcerns.length > 0 && currentIteration >= maxIterations) {
234
+ convergenceTripped = true;
235
+ escalationReason = `max iterations ${maxIterations} reached with ${openConcerns.length} open concern(s)`;
236
+ }
237
+ return {
238
+ schemaVersion: 1,
239
+ stage: options.stage,
240
+ runId: options.runId,
241
+ iteration: currentIteration,
242
+ maxIterations,
243
+ openConcerns,
244
+ resolvedConcerns,
245
+ lastSeenConcernIds,
246
+ convergenceTripped,
247
+ ...(escalationReason ? { escalationReason } : {}),
248
+ lastUpdatedAt: (options.now ?? new Date()).toISOString()
249
+ };
250
+ }
251
+ export async function computeEarlyLoopStatus(stage, runId, concernsLogPath, options = {}) {
252
+ let raw = "";
253
+ try {
254
+ raw = await fs.readFile(concernsLogPath, "utf8");
255
+ }
256
+ catch (error) {
257
+ if (error.code !== "ENOENT") {
258
+ throw error;
259
+ }
260
+ }
261
+ const parsed = parseEarlyLoopLog(raw, {
262
+ issues: options.parseIssues,
263
+ strict: options.strictParse
264
+ });
265
+ return deriveEarlyLoopStatus(parsed, {
266
+ stage,
267
+ runId,
268
+ maxIterations: options.maxIterations,
269
+ now: options.now
270
+ });
271
+ }
272
+ export function formatEarlyLoopStatusLine(status) {
273
+ const convergence = status.convergenceTripped ? "tripped" : "clear";
274
+ return `Early Loop: stage=${status.stage}, iter=${status.iteration}/${status.maxIterations}, open=${status.openConcerns.length}, convergence=${convergence}`;
275
+ }
@@ -30,6 +30,12 @@ export interface CompletedStagesClosureResult {
30
30
  }>;
31
31
  }
32
32
  export declare const RECONCILIATION_NOTICES_REL_PATH = ".cclaw/state/reconciliation-notices.json";
33
+ export type ReconciliationNoticeKind = "gate_demotion" | "closeout_substate_demotion";
34
+ export interface CloseoutSubstateDemotionPayload {
35
+ previous: string;
36
+ next: string;
37
+ reason: string;
38
+ }
33
39
  export interface ReconciliationNotice {
34
40
  id: string;
35
41
  runId: string;
@@ -37,6 +43,8 @@ export interface ReconciliationNotice {
37
43
  gateId: string;
38
44
  reason: string;
39
45
  demotedAt: string;
46
+ kind?: ReconciliationNoticeKind;
47
+ payload?: CloseoutSubstateDemotionPayload;
40
48
  }
41
49
  export interface ReconciliationNoticesPayload {
42
50
  schemaVersion: number;
@@ -2,10 +2,12 @@ import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { checkReviewSecurityNoChangeAttestation, checkReviewVerdictConsistency, extractMarkdownSectionBody, lintArtifact, validateReviewArmy } from "./artifact-linter.js";
4
4
  import { resolveArtifactPath } from "./artifact-paths.js";
5
+ import { readConfig } from "./config.js";
5
6
  import { RUNTIME_ROOT } from "./constants.js";
6
7
  import { stageSchema } from "./content/stage-schema.js";
7
8
  import { readDelegationLedger } from "./delegation.js";
8
9
  import { ensureDir, exists, writeFileSafe } from "./fs-utils.js";
10
+ import { computeEarlyLoopStatus, isEarlyLoopStage, normalizeEarlyLoopMaxIterations } from "./early-loop.js";
9
11
  import { detectPublicApiChanges } from "./internal/detect-public-api-changes.js";
10
12
  import { readFlowState, writeFlowState } from "./runs.js";
11
13
  import { parseTddCycleLog, validateTddCycleOrder } from "./tdd-cycle.js";
@@ -119,6 +121,92 @@ async function verifyDiscoveredCommandEvidence(projectRoot, stage, gateId, flowS
119
121
  return null;
120
122
  return `${stage} verification gate blocked (${gateId}): guard evidence must cite one discovered real test command: ${commands.join(", ")}.`;
121
123
  }
124
+ function toEarlyLoopGateSnapshot(value) {
125
+ if (!value || typeof value !== "object" || Array.isArray(value))
126
+ return null;
127
+ const typed = value;
128
+ const stage = typeof typed.stage === "string" ? typed.stage : "";
129
+ const runId = typeof typed.runId === "string" ? typed.runId : "active";
130
+ const iteration = typeof typed.iteration === "number" && Number.isInteger(typed.iteration) && typed.iteration >= 0
131
+ ? typed.iteration
132
+ : 0;
133
+ const maxIterations = normalizeEarlyLoopMaxIterations(typeof typed.maxIterations === "number" ? typed.maxIterations : undefined);
134
+ const openConcernIds = Array.isArray(typed.openConcerns)
135
+ ? typed.openConcerns
136
+ .flatMap((concern) => {
137
+ if (!concern || typeof concern !== "object" || Array.isArray(concern))
138
+ return [];
139
+ const id = concern.id;
140
+ return typeof id === "string" && id.trim().length > 0 ? [id.trim()] : [];
141
+ })
142
+ .sort((a, b) => a.localeCompare(b, "en"))
143
+ : [];
144
+ if (stage.length === 0)
145
+ return null;
146
+ return {
147
+ stage,
148
+ runId,
149
+ iteration,
150
+ maxIterations,
151
+ openConcernIds,
152
+ openConcernCount: openConcernIds.length,
153
+ convergenceTripped: typed.convergenceTripped === true,
154
+ escalationReason: typeof typed.escalationReason === "string" && typed.escalationReason.trim().length > 0
155
+ ? typed.escalationReason.trim()
156
+ : undefined
157
+ };
158
+ }
159
+ async function readEarlyLoopGateSnapshot(projectRoot, flowState) {
160
+ if (!isEarlyLoopStage(flowState.currentStage)) {
161
+ return { snapshot: null };
162
+ }
163
+ const stateDir = path.join(projectRoot, RUNTIME_ROOT, "state");
164
+ const statusPath = path.join(stateDir, "early-loop.json");
165
+ let onDisk = null;
166
+ if (await exists(statusPath)) {
167
+ try {
168
+ onDisk = toEarlyLoopGateSnapshot(JSON.parse(await fs.readFile(statusPath, "utf8")));
169
+ }
170
+ catch (error) {
171
+ const reason = error instanceof Error ? error.message : String(error);
172
+ return {
173
+ snapshot: null,
174
+ issue: `early loop gate blocked (early_loop_open_concerns): unable to parse ${statusPath} (${reason}). ` +
175
+ "Rebuild status with `cclaw internal early-loop-status --write`."
176
+ };
177
+ }
178
+ }
179
+ if (onDisk &&
180
+ onDisk.stage === flowState.currentStage &&
181
+ onDisk.runId === flowState.activeRunId) {
182
+ return { snapshot: onDisk };
183
+ }
184
+ try {
185
+ const config = await readConfig(projectRoot);
186
+ const computed = await computeEarlyLoopStatus(flowState.currentStage, flowState.activeRunId, path.join(stateDir, "early-loop-log.jsonl"), {
187
+ maxIterations: config.earlyLoop?.maxIterations
188
+ });
189
+ return {
190
+ snapshot: {
191
+ stage: computed.stage,
192
+ runId: computed.runId,
193
+ iteration: computed.iteration,
194
+ maxIterations: computed.maxIterations,
195
+ openConcernIds: computed.openConcerns.map((concern) => concern.id),
196
+ openConcernCount: computed.openConcerns.length,
197
+ convergenceTripped: computed.convergenceTripped,
198
+ escalationReason: computed.escalationReason
199
+ }
200
+ };
201
+ }
202
+ catch (error) {
203
+ const reason = error instanceof Error ? error.message : String(error);
204
+ return {
205
+ snapshot: null,
206
+ issue: `early loop gate blocked (early_loop_open_concerns): unable to compute status from early-loop-log.jsonl (${reason}).`
207
+ };
208
+ }
209
+ }
122
210
  const RECONCILIATION_NOTICES_FILE = "reconciliation-notices.json";
123
211
  const RECONCILIATION_NOTICES_SCHEMA_VERSION = 1;
124
212
  const DESIGN_RESEARCH_REQUIRED_SECTIONS = [
@@ -156,13 +244,31 @@ function sanitizeReconciliationNotice(raw) {
156
244
  typeof typed.demotedAt !== "string") {
157
245
  return null;
158
246
  }
247
+ const kind = typed.kind === "closeout_substate_demotion"
248
+ ? "closeout_substate_demotion"
249
+ : "gate_demotion";
250
+ let payload;
251
+ if (kind === "closeout_substate_demotion" && typed.payload && typeof typed.payload === "object" && !Array.isArray(typed.payload)) {
252
+ const payloadTyped = typed.payload;
253
+ if (typeof payloadTyped.previous === "string" &&
254
+ typeof payloadTyped.next === "string" &&
255
+ typeof payloadTyped.reason === "string") {
256
+ payload = {
257
+ previous: payloadTyped.previous,
258
+ next: payloadTyped.next,
259
+ reason: payloadTyped.reason
260
+ };
261
+ }
262
+ }
159
263
  return {
160
264
  id: typed.id,
161
265
  runId: typed.runId,
162
266
  stage: typed.stage,
163
267
  gateId: typed.gateId,
164
268
  reason: typed.reason,
165
- demotedAt: typed.demotedAt
269
+ demotedAt: typed.demotedAt,
270
+ kind,
271
+ payload
166
272
  };
167
273
  }
168
274
  export async function readReconciliationNotices(projectRoot) {
@@ -211,6 +317,9 @@ export function classifyReconciliationNotices(flowState, notices) {
211
317
  staleRun.push(notice);
212
318
  continue;
213
319
  }
320
+ if (notice.kind === "closeout_substate_demotion") {
321
+ continue;
322
+ }
214
323
  const stageCatalog = flowState.stageGateCatalog[notice.stage];
215
324
  const blocked = stageCatalog.blocked.includes(notice.gateId);
216
325
  if (!blocked) {
@@ -239,6 +348,7 @@ export async function verifyCurrentStageGateEvidence(projectRoot, flowState) {
239
348
  const recommendedSet = new Set(recommended);
240
349
  const allowedSet = new Set([...required, ...recommended]);
241
350
  const issues = [];
351
+ const softNotices = [];
242
352
  const catalogRequired = unique(catalog.required);
243
353
  const catalogRecommended = unique(catalog.recommended ?? []);
244
354
  const catalogConditional = unique(catalog.conditional ?? []);
@@ -439,8 +549,33 @@ export async function verifyCurrentStageGateEvidence(projectRoot, flowState) {
439
549
  }
440
550
  }
441
551
  }
552
+ if (isEarlyLoopStage(stage)) {
553
+ const { snapshot, issue } = await readEarlyLoopGateSnapshot(projectRoot, flowState);
554
+ if (issue) {
555
+ issues.push(issue);
556
+ }
557
+ else if (snapshot && snapshot.openConcernCount > 0) {
558
+ const concernTail = snapshot.openConcernIds.length > 3
559
+ ? `, +${snapshot.openConcernIds.length - 3} more`
560
+ : "";
561
+ const concernSample = snapshot.openConcernIds.slice(0, 3).join(", ");
562
+ if (snapshot.convergenceTripped) {
563
+ const reason = snapshot.escalationReason ?? "convergence guard tripped";
564
+ softNotices.push(`early loop escalation notice (early_loop_open_concerns): ${reason}; ` +
565
+ `open concerns remain (${concernSample}${concernTail}). Request explicit human override before advancing.`);
566
+ }
567
+ else {
568
+ issues.push(`early loop gate blocked (early_loop_open_concerns): ` +
569
+ `${snapshot.openConcernCount} open concern(s) remain after iteration ` +
570
+ `${snapshot.iteration}/${snapshot.maxIterations} (${concernSample}${concernTail}).`);
571
+ }
572
+ }
573
+ }
442
574
  const missingRequired = required.filter((gateId) => !passedSet.has(gateId));
443
- const missingRecommended = recommended.filter((gateId) => !passedSet.has(gateId));
575
+ const missingRecommended = [
576
+ ...recommended.filter((gateId) => !passedSet.has(gateId)),
577
+ ...softNotices
578
+ ];
444
579
  const missingTriggeredConditional = [];
445
580
  const blockingBlocked = catalog.blocked.filter((gateId) => requiredSet.has(gateId));
446
581
  const complete = missingRequired.length === 0 && blockingBlocked.length === 0;
@@ -617,9 +752,9 @@ export async function reconcileAndWriteCurrentStageGateCatalog(projectRoot) {
617
752
  noticesChanged = true;
618
753
  }
619
754
  if (reconciliation.demotedGateIds.length > 0) {
620
- const existing = new Set(noticesPayload.notices.map((notice) => `${notice.runId}:${notice.stage}:${notice.gateId}`));
755
+ const existing = new Set(noticesPayload.notices.map((notice) => `${notice.runId}:${notice.stage}:${notice.gateId}:${notice.kind ?? "gate_demotion"}`));
621
756
  for (const gateId of reconciliation.demotedGateIds) {
622
- const dedupeKey = `${effectiveState.activeRunId}:${reconciliation.stage}:${gateId}`;
757
+ const dedupeKey = `${effectiveState.activeRunId}:${reconciliation.stage}:${gateId}:gate_demotion`;
623
758
  if (existing.has(dedupeKey)) {
624
759
  continue;
625
760
  }
@@ -630,7 +765,8 @@ export async function reconcileAndWriteCurrentStageGateCatalog(projectRoot) {
630
765
  stage: reconciliation.stage,
631
766
  gateId,
632
767
  reason: "demoted from passed to blocked during gate reconciliation (missing evidence)",
633
- demotedAt: ts
768
+ demotedAt: ts,
769
+ kind: "gate_demotion"
634
770
  });
635
771
  existing.add(dedupeKey);
636
772
  noticesChanged = true;
@@ -32,7 +32,7 @@ export type SubagentFallback =
32
32
  * directories under a skills root (Codex CLI ≥0.89, Jan 2026). cclaw
33
33
  * writes `<commandDir>/<skillName>/SKILL.md` and the agent invokes it
34
34
  * either via `/use <skillName>` or via automatic description matching
35
- * when the user's text mentions `/cc`, `/cc-ideate`, or `/cc-cancel`.
35
+ * when the user's text mentions `/cc`, `/cc-idea`, or `/cc-cancel`.
36
36
  */
37
37
  export type ShimKind = "command" | "skill";
38
38
  export interface HarnessAdapter {
@@ -47,7 +47,7 @@ export interface HarnessAdapter {
47
47
  * Root directory where cclaw writes `/cc*` entry points.
48
48
  *
49
49
  * - For `shimKind: "command"` this is the directory containing flat
50
- * markdown files (`<commandDir>/cc.md`, `<commandDir>/cc-ideate.md`, …).
50
+ * markdown files (`<commandDir>/cc.md`, `<commandDir>/cc-idea.md`, …).
51
51
  * - For `shimKind: "skill"` this is the skills root that contains
52
52
  * per-skill subdirectories (`<commandDir>/<skillName>/SKILL.md`).
53
53
  */