@cleocode/core 2026.6.14 → 2026.6.17
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/check/pr-gate.d.ts +6 -2
- package/dist/check/pr-gate.d.ts.map +1 -1
- package/dist/check/pr-gate.js +14 -32
- package/dist/check/pr-gate.js.map +1 -1
- package/dist/config/config-repair.d.ts +70 -0
- package/dist/config/config-repair.d.ts.map +1 -0
- package/dist/config/config-repair.js +248 -0
- package/dist/config/config-repair.js.map +1 -0
- package/dist/config/registry.d.ts.map +1 -1
- package/dist/config/registry.js +4 -4
- package/dist/config/registry.js.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +11 -10
- package/dist/config.js.map +1 -1
- package/dist/docs/export-document.js +58 -16
- package/dist/docs/export-document.js.map +2 -2
- package/dist/gc/cleanup.d.ts +23 -8
- package/dist/gc/cleanup.d.ts.map +1 -1
- package/dist/gc/cleanup.js +287 -22
- package/dist/gc/cleanup.js.map +1 -1
- package/dist/gc/janitor.d.ts +149 -0
- package/dist/gc/janitor.d.ts.map +1 -0
- package/dist/gc/janitor.js +760 -0
- package/dist/gc/janitor.js.map +1 -0
- package/dist/internal.d.ts +1 -0
- package/dist/internal.d.ts.map +1 -1
- package/dist/internal.js +3 -0
- package/dist/internal.js.map +1 -1
- package/dist/llm/cli-ops.d.ts +31 -4
- package/dist/llm/cli-ops.d.ts.map +1 -1
- package/dist/llm/cli-ops.js +110 -26
- package/dist/llm/cli-ops.js.map +1 -1
- package/dist/llm/credential-pool.d.ts +62 -0
- package/dist/llm/credential-pool.d.ts.map +1 -1
- package/dist/llm/credential-pool.js +161 -0
- package/dist/llm/credential-pool.js.map +1 -1
- package/dist/llm/cross-provider-selector.d.ts +254 -0
- package/dist/llm/cross-provider-selector.d.ts.map +1 -0
- package/dist/llm/cross-provider-selector.js +635 -0
- package/dist/llm/cross-provider-selector.js.map +1 -0
- package/dist/llm/index.d.ts +2 -0
- package/dist/llm/index.d.ts.map +1 -1
- package/dist/llm/index.js +1 -0
- package/dist/llm/index.js.map +1 -1
- package/dist/llm/local-model-fit.d.ts +260 -0
- package/dist/llm/local-model-fit.d.ts.map +1 -0
- package/dist/llm/local-model-fit.js +563 -0
- package/dist/llm/local-model-fit.js.map +1 -0
- package/dist/llm/plugin-facade.js +883 -231
- package/dist/llm/plugin-facade.js.map +3 -3
- package/dist/llm/provider-registry/builtin/ollama.d.ts +18 -0
- package/dist/llm/provider-registry/builtin/ollama.d.ts.map +1 -1
- package/dist/llm/provider-registry/builtin/ollama.js +20 -2
- package/dist/llm/provider-registry/builtin/ollama.js.map +1 -1
- package/dist/llm/role-resolver.d.ts.map +1 -1
- package/dist/llm/role-resolver.js +45 -10
- package/dist/llm/role-resolver.js.map +1 -1
- package/dist/llm/transports/codex-responses.d.ts +111 -88
- package/dist/llm/transports/codex-responses.d.ts.map +1 -1
- package/dist/llm/transports/codex-responses.js +456 -258
- package/dist/llm/transports/codex-responses.js.map +1 -1
- package/dist/release/index.d.ts +2 -2
- package/dist/release/index.d.ts.map +1 -1
- package/dist/release/index.js +1 -1
- package/dist/release/index.js.map +1 -1
- package/dist/release/reconcile.d.ts +97 -0
- package/dist/release/reconcile.d.ts.map +1 -1
- package/dist/release/reconcile.js +412 -5
- package/dist/release/reconcile.js.map +1 -1
- package/dist/resources/backend.d.ts +155 -0
- package/dist/resources/backend.d.ts.map +1 -0
- package/dist/resources/backend.js +23 -0
- package/dist/resources/backend.js.map +1 -0
- package/dist/resources/linux-backend.d.ts +167 -0
- package/dist/resources/linux-backend.d.ts.map +1 -0
- package/dist/resources/linux-backend.js +259 -0
- package/dist/resources/linux-backend.js.map +1 -0
- package/dist/resources/monitor.d.ts +230 -0
- package/dist/resources/monitor.d.ts.map +1 -0
- package/dist/resources/monitor.js +255 -0
- package/dist/resources/monitor.js.map +1 -0
- package/dist/resources/spawn-wrapper.d.ts +265 -0
- package/dist/resources/spawn-wrapper.d.ts.map +1 -0
- package/dist/resources/spawn-wrapper.js +304 -0
- package/dist/resources/spawn-wrapper.js.map +1 -0
- package/dist/selfimprove/budget.d.ts +11 -2
- package/dist/selfimprove/budget.d.ts.map +1 -1
- package/dist/selfimprove/budget.js +11 -2
- package/dist/selfimprove/budget.js.map +1 -1
- package/dist/selfimprove/fix-gen-context.d.ts +172 -0
- package/dist/selfimprove/fix-gen-context.d.ts.map +1 -0
- package/dist/selfimprove/fix-gen-context.js +218 -0
- package/dist/selfimprove/fix-gen-context.js.map +1 -0
- package/dist/selfimprove/fix-gen.d.ts +285 -0
- package/dist/selfimprove/fix-gen.d.ts.map +1 -0
- package/dist/selfimprove/fix-gen.js +346 -0
- package/dist/selfimprove/fix-gen.js.map +1 -0
- package/dist/selfimprove/op-source-map.d.ts +92 -0
- package/dist/selfimprove/op-source-map.d.ts.map +1 -0
- package/dist/selfimprove/op-source-map.js +187 -0
- package/dist/selfimprove/op-source-map.js.map +1 -0
- package/dist/selfimprove/probe-helper.d.ts +61 -0
- package/dist/selfimprove/probe-helper.d.ts.map +1 -0
- package/dist/selfimprove/probe-helper.js +64 -0
- package/dist/selfimprove/probe-helper.js.map +1 -0
- package/dist/selfimprove/run-loop.d.ts +44 -4
- package/dist/selfimprove/run-loop.d.ts.map +1 -1
- package/dist/selfimprove/run-loop.js +73 -6
- package/dist/selfimprove/run-loop.js.map +1 -1
- package/dist/selfimprove/scenarios/seeded-code-regression/golden.json +18 -0
- package/dist/selfimprove/scenarios/seeded-code-regression/scenario.json +12 -0
- package/dist/sentient/cross-project-hygiene.d.ts +11 -2
- package/dist/sentient/cross-project-hygiene.d.ts.map +1 -1
- package/dist/sentient/cross-project-hygiene.js +73 -10
- package/dist/sentient/cross-project-hygiene.js.map +1 -1
- package/dist/sentient/daemon.d.ts.map +1 -1
- package/dist/sentient/daemon.js +7 -2
- package/dist/sentient/daemon.js.map +1 -1
- package/dist/sentient/worktree-dispatch.d.ts.map +1 -1
- package/dist/sentient/worktree-dispatch.js +8 -1
- package/dist/sentient/worktree-dispatch.js.map +1 -1
- package/dist/setup/index.d.ts +1 -0
- package/dist/setup/index.d.ts.map +1 -1
- package/dist/setup/index.js +1 -0
- package/dist/setup/index.js.map +1 -1
- package/dist/setup/sections/models-roles.d.ts +32 -1
- package/dist/setup/sections/models-roles.d.ts.map +1 -1
- package/dist/setup/sections/models-roles.js +118 -19
- package/dist/setup/sections/models-roles.js.map +1 -1
- package/dist/setup/whoami-summary.d.ts +34 -0
- package/dist/setup/whoami-summary.d.ts.map +1 -0
- package/dist/setup/whoami-summary.js +106 -0
- package/dist/setup/whoami-summary.js.map +1 -0
- package/dist/spawn/branch-lock.d.ts.map +1 -1
- package/dist/spawn/branch-lock.js +7 -1
- package/dist/spawn/branch-lock.js.map +1 -1
- package/dist/store/attachment-repair.d.ts +110 -0
- package/dist/store/attachment-repair.d.ts.map +1 -0
- package/dist/store/attachment-repair.js +326 -0
- package/dist/store/attachment-repair.js.map +1 -0
- package/dist/store/attachment-store.d.ts.map +1 -1
- package/dist/store/attachment-store.js +24 -7
- package/dist/store/attachment-store.js.map +1 -1
- package/dist/tasks/evidence.d.ts +34 -2
- package/dist/tasks/evidence.d.ts.map +1 -1
- package/dist/tasks/evidence.js +293 -35
- package/dist/tasks/evidence.js.map +1 -1
- package/dist/verification/verify-tools.d.ts.map +1 -1
- package/dist/verification/verify-tools.js +5 -6
- package/dist/verification/verify-tools.js.map +1 -1
- package/package.json +22 -12
- package/templates/workflows/release-prepare.yml.tmpl +48 -2
|
@@ -39,13 +39,351 @@ import { mkdir } from 'node:fs/promises';
|
|
|
39
39
|
import { join, resolve } from 'node:path';
|
|
40
40
|
import { promisify } from 'node:util';
|
|
41
41
|
import { safeParseReleasePlan } from '@cleocode/contracts';
|
|
42
|
-
import { eq } from 'drizzle-orm';
|
|
42
|
+
import { eq, inArray } from 'drizzle-orm';
|
|
43
43
|
import { engineError, engineSuccess } from '../engine-result.js';
|
|
44
44
|
import { getLogger } from '../logger.js';
|
|
45
45
|
import { generateProjectHash } from '../nexus/hash.js';
|
|
46
46
|
import { getProjectRoot } from '../paths.js';
|
|
47
47
|
import { getDb, getNativeDb } from '../store/sqlite.js';
|
|
48
48
|
import * as schema from '../store/tasks-schema.js';
|
|
49
|
+
// ─── Tag-reconcile plan synthesis (T11977 · DHQ-080) ─────────────────────────
|
|
50
|
+
/** Provenance origin label written into plan.meta when synthesis fires. */
|
|
51
|
+
const TAG_RECONCILE_ORIGIN = 'tag-reconcile-synthesized';
|
|
52
|
+
/** T#### regex re-used across synthesis helpers. */
|
|
53
|
+
const SYNTH_TASK_TOKEN_RE = /\bT\d{1,5}\b/g;
|
|
54
|
+
/** Conventional-commit prefix detector (mirrors backfill.ts equivalent). */
|
|
55
|
+
const SYNTH_CC_RE = /^(feat|fix|chore|docs|refactor|test|perf|build|ci|revert|breaking|style)(?:\(([^)]+)\))?!?:\s/i;
|
|
56
|
+
/**
|
|
57
|
+
* Infer the previous git tag by listing all tags sorted by creator-date
|
|
58
|
+
* (annotated + lightweight, oldest-first) and returning the entry
|
|
59
|
+
* immediately before `version`. Returns `null` when `version` is the
|
|
60
|
+
* oldest tag or no tags exist.
|
|
61
|
+
*/
|
|
62
|
+
function inferPrevTag(version, projectRoot) {
|
|
63
|
+
try {
|
|
64
|
+
const raw = execFileSync('git', ['tag', '--list', '--sort=creatordate'], {
|
|
65
|
+
cwd: projectRoot,
|
|
66
|
+
encoding: 'utf-8',
|
|
67
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
68
|
+
timeout: SUBPROCESS_TIMEOUT_MS,
|
|
69
|
+
}).trim();
|
|
70
|
+
const tags = raw
|
|
71
|
+
.split('\n')
|
|
72
|
+
.map((t) => t.trim())
|
|
73
|
+
.filter((t) => t.length > 0);
|
|
74
|
+
const idx = tags.indexOf(version);
|
|
75
|
+
if (idx <= 0)
|
|
76
|
+
return null;
|
|
77
|
+
return tags[idx - 1] ?? null;
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Parse the CHANGELOG.md section for `version` and extract:
|
|
85
|
+
* - Every T#### token mentioned in the section body.
|
|
86
|
+
* - The section text (for task summary extraction).
|
|
87
|
+
*
|
|
88
|
+
* Canonical header format (ADR-028 §2.5): `## [VERSION]` (no `v` prefix).
|
|
89
|
+
* We strip the leading `v` before matching so both forms are accepted.
|
|
90
|
+
*
|
|
91
|
+
* Returns `{ found: false }` when CHANGELOG.md is missing or has no matching
|
|
92
|
+
* section — synthesis continues without CHANGELOG data (non-fatal).
|
|
93
|
+
*/
|
|
94
|
+
function parseChangelogSection(version, projectRoot) {
|
|
95
|
+
const changelogPath = join(projectRoot, 'CHANGELOG.md');
|
|
96
|
+
if (!existsSync(changelogPath))
|
|
97
|
+
return { found: false };
|
|
98
|
+
let contents;
|
|
99
|
+
try {
|
|
100
|
+
contents = readFileSync(changelogPath, 'utf-8');
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
return { found: false };
|
|
104
|
+
}
|
|
105
|
+
// Normalise version to no-v form (ADR-028 §2.5).
|
|
106
|
+
const normalized = version.startsWith('v') ? version.slice(1) : version;
|
|
107
|
+
const headerPattern = `## [${normalized}]`;
|
|
108
|
+
const startIdx = contents.indexOf(headerPattern);
|
|
109
|
+
if (startIdx < 0)
|
|
110
|
+
return { found: false };
|
|
111
|
+
// Slice from the header line to the next `## [` section header (exclusive).
|
|
112
|
+
const bodyStart = startIdx + headerPattern.length;
|
|
113
|
+
const nextSectionIdx = contents.indexOf('\n## [', bodyStart);
|
|
114
|
+
const sectionText = nextSectionIdx >= 0 ? contents.slice(bodyStart, nextSectionIdx) : contents.slice(bodyStart);
|
|
115
|
+
// Extract all T#### tokens from the section body.
|
|
116
|
+
SYNTH_TASK_TOKEN_RE.lastIndex = 0;
|
|
117
|
+
const tokens = new Set();
|
|
118
|
+
let m = SYNTH_TASK_TOKEN_RE.exec(sectionText);
|
|
119
|
+
while (m !== null) {
|
|
120
|
+
tokens.add(m[0]);
|
|
121
|
+
m = SYNTH_TASK_TOKEN_RE.exec(sectionText);
|
|
122
|
+
}
|
|
123
|
+
return { found: true, taskIds: Array.from(tokens), sectionText };
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Extract unique T#### tokens from the `prevTag..tag` git log (subjects +
|
|
127
|
+
* bodies), plus PR numbers from merge-commit subjects.
|
|
128
|
+
*
|
|
129
|
+
* Returns empty arrays on git failure (non-fatal — synthesis continues).
|
|
130
|
+
*/
|
|
131
|
+
function extractTokensFromGitLog(prevTag, tag, projectRoot) {
|
|
132
|
+
try {
|
|
133
|
+
const range = prevTag ? `${prevTag}..${tag}` : tag;
|
|
134
|
+
const logOut = execFileSync('git', ['log', '--pretty=format:%s%n%b%n---COMMIT---', range], {
|
|
135
|
+
cwd: projectRoot,
|
|
136
|
+
encoding: 'utf-8',
|
|
137
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
138
|
+
timeout: SUBPROCESS_TIMEOUT_MS,
|
|
139
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
140
|
+
}).trim();
|
|
141
|
+
SYNTH_TASK_TOKEN_RE.lastIndex = 0;
|
|
142
|
+
const taskIds = new Set();
|
|
143
|
+
let m = SYNTH_TASK_TOKEN_RE.exec(logOut);
|
|
144
|
+
while (m !== null) {
|
|
145
|
+
taskIds.add(m[0]);
|
|
146
|
+
m = SYNTH_TASK_TOKEN_RE.exec(logOut);
|
|
147
|
+
}
|
|
148
|
+
// Extract PR numbers from merge-commit subjects: "Merge pull request #NNN"
|
|
149
|
+
const prNumbers = new Set();
|
|
150
|
+
const PR_RE = /Merge pull request #(\d+)/g;
|
|
151
|
+
let pm = PR_RE.exec(logOut);
|
|
152
|
+
while (pm !== null) {
|
|
153
|
+
const n = Number.parseInt(pm[1], 10);
|
|
154
|
+
if (Number.isFinite(n) && n > 0)
|
|
155
|
+
prNumbers.add(n);
|
|
156
|
+
pm = PR_RE.exec(logOut);
|
|
157
|
+
}
|
|
158
|
+
return { taskIds: Array.from(taskIds), prNumbers: Array.from(prNumbers) };
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
return { taskIds: [], prNumbers: [] };
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Parse the CC type from a commit subject and map to a task kind.
|
|
166
|
+
* Mirrors the equivalent in `backfill.ts`.
|
|
167
|
+
*
|
|
168
|
+
* @internal
|
|
169
|
+
*/
|
|
170
|
+
function synthCcToTaskKind(subject) {
|
|
171
|
+
const m = subject.match(SYNTH_CC_RE);
|
|
172
|
+
const raw = m?.[1]?.toLowerCase() ?? '';
|
|
173
|
+
switch (raw) {
|
|
174
|
+
case 'feat':
|
|
175
|
+
return 'feat';
|
|
176
|
+
case 'fix':
|
|
177
|
+
return 'fix';
|
|
178
|
+
case 'docs':
|
|
179
|
+
return 'docs';
|
|
180
|
+
case 'refactor':
|
|
181
|
+
return 'refactor';
|
|
182
|
+
case 'test':
|
|
183
|
+
return 'test';
|
|
184
|
+
case 'perf':
|
|
185
|
+
return 'perf';
|
|
186
|
+
case 'revert':
|
|
187
|
+
return 'revert';
|
|
188
|
+
case 'breaking':
|
|
189
|
+
return 'breaking';
|
|
190
|
+
default:
|
|
191
|
+
return 'chore';
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Synthesise a minimal {@link ReleasePlan} for a tag-driven release that has
|
|
196
|
+
* no `.cleo/release/<version>.plan.json` on disk (DHQ-080 · T11977).
|
|
197
|
+
*
|
|
198
|
+
* Derivation order (highest-fidelity source wins):
|
|
199
|
+
* (a) CHANGELOG.md section for `version` — task-ID tokens + summaries.
|
|
200
|
+
* (b) Merged PRs between prevTag..tag (`git log --merges`) — T#### from titles.
|
|
201
|
+
* (c) All commits between prevTag..tag — T#### from subjects/bodies.
|
|
202
|
+
*
|
|
203
|
+
* The synthesised plan carries `meta.origin = 'tag-reconcile-synthesized'`
|
|
204
|
+
* so honest plans (from `cleo release plan`) remain distinguishable. Fields
|
|
205
|
+
* that cannot be derived without a prior `cleo release plan` run (evidenceAtoms,
|
|
206
|
+
* gates, prUrl, mergeCommitSha, workflowRunUrl) are left empty/null.
|
|
207
|
+
*
|
|
208
|
+
* Does NOT write to disk — the caller decides whether to persist or dry-run.
|
|
209
|
+
*
|
|
210
|
+
* @param version — Version tag to synthesise a plan for (e.g. `v2026.6.14`).
|
|
211
|
+
* @param projectRoot — Absolute project root.
|
|
212
|
+
* @returns `SynthesizedPlanReport` with the in-memory plan + derivation metadata.
|
|
213
|
+
*
|
|
214
|
+
* @task T11977
|
|
215
|
+
*/
|
|
216
|
+
export async function synthesizePlanForReconcile(version, projectRoot) {
|
|
217
|
+
const prevTag = inferPrevTag(version, projectRoot);
|
|
218
|
+
// (a) CHANGELOG.md section
|
|
219
|
+
const changelogResult = parseChangelogSection(version, projectRoot);
|
|
220
|
+
const changelogTaskIds = changelogResult.found ? changelogResult.taskIds : [];
|
|
221
|
+
// (b+c) Git log tokens + PR numbers
|
|
222
|
+
const { taskIds: commitTaskIds, prNumbers: discoveredPrNumbers } = extractTokensFromGitLog(prevTag, version, projectRoot);
|
|
223
|
+
// Union the sources; changelog wins position (highest fidelity).
|
|
224
|
+
const allTokens = new Set([...changelogTaskIds, ...commitTaskIds]);
|
|
225
|
+
// Intersect with valid task IDs in the runtime store (do not fabricate rows).
|
|
226
|
+
// Use inArray to avoid a full table scan (DHQ-065 anti-pattern guard).
|
|
227
|
+
const db = await getDb(projectRoot);
|
|
228
|
+
const validIds = new Set();
|
|
229
|
+
if (allTokens.size > 0) {
|
|
230
|
+
const tokenList = Array.from(allTokens);
|
|
231
|
+
const rows = await db
|
|
232
|
+
.select({ id: schema.tasks.id })
|
|
233
|
+
.from(schema.tasks)
|
|
234
|
+
.where(inArray(schema.tasks.id, tokenList))
|
|
235
|
+
.all();
|
|
236
|
+
for (const r of rows) {
|
|
237
|
+
if (typeof r.id === 'string') {
|
|
238
|
+
validIds.add(r.id);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
// Derive task kind heuristic from the CHANGELOG section or first git log subject.
|
|
243
|
+
let seedKind = 'chore';
|
|
244
|
+
if (changelogResult.found) {
|
|
245
|
+
// Scan changelog section for a first `###` heading to derive kind.
|
|
246
|
+
const firstHeading = changelogResult.sectionText.split('\n').find((l) => l.startsWith('### '));
|
|
247
|
+
if (firstHeading?.toLowerCase().includes('added'))
|
|
248
|
+
seedKind = 'feat';
|
|
249
|
+
else if (firstHeading?.toLowerCase().includes('fix'))
|
|
250
|
+
seedKind = 'fix';
|
|
251
|
+
}
|
|
252
|
+
else {
|
|
253
|
+
// Fall back to first commit subject CC type.
|
|
254
|
+
try {
|
|
255
|
+
const range = prevTag ? `${prevTag}..${version}` : version;
|
|
256
|
+
const firstSubject = execFileSync('git', ['log', '--pretty=format:%s', '--reverse', range], {
|
|
257
|
+
cwd: projectRoot,
|
|
258
|
+
encoding: 'utf-8',
|
|
259
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
260
|
+
timeout: SUBPROCESS_TIMEOUT_MS,
|
|
261
|
+
})
|
|
262
|
+
.trim()
|
|
263
|
+
.split('\n')[0];
|
|
264
|
+
if (firstSubject)
|
|
265
|
+
seedKind = synthCcToTaskKind(firstSubject);
|
|
266
|
+
}
|
|
267
|
+
catch {
|
|
268
|
+
// non-fatal
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
const epicId = validIds.size > 0
|
|
272
|
+
? Array.from(validIds)[0]
|
|
273
|
+
: `SYNTH-${version.replace(/[^A-Z0-9]/gi, '')}`;
|
|
274
|
+
// Build per-task entries. Each task gets a summary from the CHANGELOG section
|
|
275
|
+
// when available; otherwise a generic backfill note.
|
|
276
|
+
const taskEntries = validIds.size > 0
|
|
277
|
+
? Array.from(validIds).map((id) => {
|
|
278
|
+
// Try to extract a summary line for this task from the CHANGELOG section.
|
|
279
|
+
let summary = `${id} — synthesized from ${version}`;
|
|
280
|
+
if (changelogResult.found) {
|
|
281
|
+
// Look for a bullet line containing the task ID.
|
|
282
|
+
const summaryLine = changelogResult.sectionText
|
|
283
|
+
.split('\n')
|
|
284
|
+
.find((l) => l.includes(`(${id}`) || l.includes(`${id}/`) || l.includes(` ${id} `));
|
|
285
|
+
if (summaryLine) {
|
|
286
|
+
// Strip leading `-` or `*` and trailing refs like `_(T#/PR#)_`.
|
|
287
|
+
summary = summaryLine
|
|
288
|
+
.replace(/^\s*[-*]\s*/, '')
|
|
289
|
+
.replace(/\s*_\(.*\)_\s*$/, '')
|
|
290
|
+
.trim();
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
return {
|
|
294
|
+
id,
|
|
295
|
+
kind: seedKind,
|
|
296
|
+
impact: 'patch',
|
|
297
|
+
userFacingSummary: summary,
|
|
298
|
+
evidenceAtoms: [`note:synthesized-from-tag-${version}`],
|
|
299
|
+
epicAncestor: epicId,
|
|
300
|
+
};
|
|
301
|
+
})
|
|
302
|
+
: [
|
|
303
|
+
{
|
|
304
|
+
id: `SYNTH-${version.replace(/[^A-Z0-9]/gi, '')}-PLACEHOLDER`,
|
|
305
|
+
kind: 'chore',
|
|
306
|
+
impact: 'patch',
|
|
307
|
+
userFacingSummary: `Tag-driven release ${version} (no task tokens resolved)`,
|
|
308
|
+
evidenceAtoms: [`note:synthesized-from-tag-${version}`],
|
|
309
|
+
epicAncestor: epicId,
|
|
310
|
+
},
|
|
311
|
+
];
|
|
312
|
+
// Bucket task IDs into changelog sections.
|
|
313
|
+
const changelog = {
|
|
314
|
+
features: [],
|
|
315
|
+
fixes: [],
|
|
316
|
+
chores: [],
|
|
317
|
+
breaking: [],
|
|
318
|
+
};
|
|
319
|
+
for (const t of taskEntries) {
|
|
320
|
+
if (t.kind === 'feat')
|
|
321
|
+
changelog.features.push(t.id);
|
|
322
|
+
else if (t.kind === 'fix' || t.kind === 'hotfix')
|
|
323
|
+
changelog.fixes.push(t.id);
|
|
324
|
+
else if (t.kind === 'breaking' || t.kind === 'revert')
|
|
325
|
+
changelog.breaking.push(t.id);
|
|
326
|
+
else
|
|
327
|
+
changelog.chores.push(t.id);
|
|
328
|
+
}
|
|
329
|
+
const nowIso = new Date().toISOString();
|
|
330
|
+
const plan = {
|
|
331
|
+
$schema: 'https://cleocode.io/schemas/release-plan/v1.json',
|
|
332
|
+
version,
|
|
333
|
+
resolvedVersion: version,
|
|
334
|
+
suffixApplied: false,
|
|
335
|
+
scheme: 'calver',
|
|
336
|
+
channel: 'latest',
|
|
337
|
+
epicId,
|
|
338
|
+
releaseKind: 'regular',
|
|
339
|
+
createdAt: nowIso,
|
|
340
|
+
createdBy: TAG_RECONCILE_ORIGIN,
|
|
341
|
+
previousVersion: prevTag,
|
|
342
|
+
previousTag: prevTag,
|
|
343
|
+
previousShippedAt: null,
|
|
344
|
+
tasks: taskEntries,
|
|
345
|
+
changelog,
|
|
346
|
+
gates: [],
|
|
347
|
+
platformMatrix: [
|
|
348
|
+
{
|
|
349
|
+
platform: 'any',
|
|
350
|
+
publisher: 'npm',
|
|
351
|
+
package: '@cleocode/cleo',
|
|
352
|
+
smoke: false,
|
|
353
|
+
},
|
|
354
|
+
],
|
|
355
|
+
preflightSummary: {
|
|
356
|
+
esbuildExternalsDrift: false,
|
|
357
|
+
lockfileDrift: false,
|
|
358
|
+
epicCompletenessClean: true,
|
|
359
|
+
doubleListingClean: true,
|
|
360
|
+
preflightWarnings: [TAG_RECONCILE_ORIGIN],
|
|
361
|
+
},
|
|
362
|
+
workflowRunUrl: null,
|
|
363
|
+
prUrl: null,
|
|
364
|
+
mergeCommitSha: null,
|
|
365
|
+
status: 'published',
|
|
366
|
+
meta: {
|
|
367
|
+
firstEverRelease: prevTag === null,
|
|
368
|
+
origin: TAG_RECONCILE_ORIGIN,
|
|
369
|
+
synthesizedAt: nowIso,
|
|
370
|
+
changelogSectionFound: changelogResult.found,
|
|
371
|
+
discoveredPrCount: discoveredPrNumbers.length,
|
|
372
|
+
},
|
|
373
|
+
};
|
|
374
|
+
const prTaskIds = [];
|
|
375
|
+
// PR task IDs are derived at reconcile time from the actual fetched PRs.
|
|
376
|
+
// We surface discoveredPrNumbers in the report so dry-run can show them.
|
|
377
|
+
return {
|
|
378
|
+
plan,
|
|
379
|
+
prevTag,
|
|
380
|
+
changelogTaskIds,
|
|
381
|
+
prTaskIds,
|
|
382
|
+
commitTaskIds: Array.from(allTokens),
|
|
383
|
+
discoveredPrNumbers,
|
|
384
|
+
changelogSectionFound: changelogResult.found,
|
|
385
|
+
};
|
|
386
|
+
}
|
|
49
387
|
const log = getLogger('release:reconcile-v2');
|
|
50
388
|
const execFileAsync = promisify(execFile);
|
|
51
389
|
/** Default subprocess timeout for git/gh calls (60s per task rules). */
|
|
@@ -676,10 +1014,65 @@ export async function releaseReconcileV2(version, opts = {}) {
|
|
|
676
1014
|
const startedAt = Date.now();
|
|
677
1015
|
const projectRoot = getProjectRoot(opts.projectRoot);
|
|
678
1016
|
// ── 1. Pre-conditions (R-080 .. R-083) ──
|
|
1017
|
+
//
|
|
1018
|
+
// T11977 / DHQ-080: tag-driven path (git tag + push → GHA publishes to npm,
|
|
1019
|
+
// no `cleo release plan` ever run) leaves no plan file. When the plan is
|
|
1020
|
+
// absent BUT the git tag exists, synthesise a minimal plan at reconcile time
|
|
1021
|
+
// from the git log + CHANGELOG.md + merged PRs. The synthesised plan carries
|
|
1022
|
+
// `meta.origin = 'tag-reconcile-synthesized'` so honest plans are
|
|
1023
|
+
// distinguishable. Evidence-staleness validation (R-313) is skipped for
|
|
1024
|
+
// synthesised plans (there is no evidence to validate).
|
|
1025
|
+
//
|
|
1026
|
+
// When `opts.dryRun = true`, return the derivation report WITHOUT touching
|
|
1027
|
+
// the DB or disk.
|
|
1028
|
+
/** Populated when synthesis fires on the tag-driven path. */
|
|
1029
|
+
let synthReport = null;
|
|
679
1030
|
const planRes = loadPlan(version, projectRoot);
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
1031
|
+
let plan;
|
|
1032
|
+
if (!planRes.success) {
|
|
1033
|
+
// Only attempt synthesis for E_PLAN_NOT_FOUND — other errors (e.g.
|
|
1034
|
+
// E_PLAN_INVALID) are returned immediately (the file exists but is corrupt).
|
|
1035
|
+
if (planRes.error.code !== 'E_PLAN_NOT_FOUND')
|
|
1036
|
+
return planRes;
|
|
1037
|
+
// Check whether the tag exists before committing to synthesis.
|
|
1038
|
+
const earlyTagRes = assertTagExists(version, projectRoot);
|
|
1039
|
+
if (!earlyTagRes.success) {
|
|
1040
|
+
// Neither a plan file nor a git tag — original E_PLAN_NOT_FOUND is
|
|
1041
|
+
// the most useful error to surface.
|
|
1042
|
+
return planRes;
|
|
1043
|
+
}
|
|
1044
|
+
log.info({ version }, 'E_PLAN_NOT_FOUND on tag-driven path — synthesising minimal plan (DHQ-080 · T11977)');
|
|
1045
|
+
synthReport = await synthesizePlanForReconcile(version, projectRoot);
|
|
1046
|
+
plan = synthReport.plan;
|
|
1047
|
+
// Dry-run: return derivation metadata without any DB writes.
|
|
1048
|
+
if (opts.dryRun) {
|
|
1049
|
+
const durationMs = Date.now() - startedAt;
|
|
1050
|
+
return engineSuccess({
|
|
1051
|
+
version,
|
|
1052
|
+
tag: version,
|
|
1053
|
+
tagSha: earlyTagRes.data,
|
|
1054
|
+
commitCount: 0,
|
|
1055
|
+
taskCount: plan.tasks.length,
|
|
1056
|
+
changeCount: 0,
|
|
1057
|
+
artifactCount: 0,
|
|
1058
|
+
brainLinkCount: 0,
|
|
1059
|
+
orphanCommits: [],
|
|
1060
|
+
dryRun: true,
|
|
1061
|
+
synthesized: {
|
|
1062
|
+
changelogTaskCount: synthReport.changelogTaskIds.length,
|
|
1063
|
+
commitTaskCount: synthReport.commitTaskIds.length,
|
|
1064
|
+
discoveredPrNumbers: synthReport.discoveredPrNumbers,
|
|
1065
|
+
prevTag: synthReport.prevTag,
|
|
1066
|
+
changelogSectionFound: synthReport.changelogSectionFound,
|
|
1067
|
+
},
|
|
1068
|
+
durationMs,
|
|
1069
|
+
txSize: 0,
|
|
1070
|
+
});
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
else {
|
|
1074
|
+
plan = planRes.data;
|
|
1075
|
+
}
|
|
683
1076
|
const tagRes = assertTagExists(version, projectRoot);
|
|
684
1077
|
if (!tagRes.success)
|
|
685
1078
|
return tagRes;
|
|
@@ -689,8 +1082,9 @@ export async function releaseReconcileV2(version, opts = {}) {
|
|
|
689
1082
|
return releaseRes;
|
|
690
1083
|
// T9528: backfill walks historical tags whose evidence atoms may reference
|
|
691
1084
|
// long-deleted files or rebased commits — skip the R-313 staleness gate.
|
|
1085
|
+
// T11977: synthesised plans have no evidence atoms to validate — also skip.
|
|
692
1086
|
// Production publish flows still pass through this check.
|
|
693
|
-
if (!opts.backfill) {
|
|
1087
|
+
if (!opts.backfill && synthReport === null) {
|
|
694
1088
|
const stalenessRes = revalidateEvidenceStaleness(plan, version, projectRoot);
|
|
695
1089
|
if (!stalenessRes.success)
|
|
696
1090
|
return stalenessRes;
|
|
@@ -1310,6 +1704,19 @@ export async function releaseReconcileV2(version, opts = {}) {
|
|
|
1310
1704
|
...(unknownTokens.size > 0 ? { unknownTokens: Array.from(unknownTokens) } : {}),
|
|
1311
1705
|
...(skippedTaskRefs.size > 0 ? { skippedTaskRefs: Array.from(skippedTaskRefs) } : {}),
|
|
1312
1706
|
...(reReconciled ? { reReconciled: true } : {}),
|
|
1707
|
+
// T11977 / DHQ-080: include synthesis metadata when the plan was derived at
|
|
1708
|
+
// reconcile time (tag-driven path with no prior `cleo release plan`).
|
|
1709
|
+
...(synthReport !== null
|
|
1710
|
+
? {
|
|
1711
|
+
synthesized: {
|
|
1712
|
+
changelogTaskCount: synthReport.changelogTaskIds.length,
|
|
1713
|
+
commitTaskCount: synthReport.commitTaskIds.length,
|
|
1714
|
+
discoveredPrNumbers: synthReport.discoveredPrNumbers,
|
|
1715
|
+
prevTag: synthReport.prevTag,
|
|
1716
|
+
changelogSectionFound: synthReport.changelogSectionFound,
|
|
1717
|
+
},
|
|
1718
|
+
}
|
|
1719
|
+
: {}),
|
|
1313
1720
|
durationMs,
|
|
1314
1721
|
txSize,
|
|
1315
1722
|
};
|