@ozzylabs/feedradar 0.1.6 → 0.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/dist/agents/_boundary.d.ts +74 -1
- package/dist/agents/_boundary.d.ts.map +1 -1
- package/dist/agents/_boundary.js +152 -0
- package/dist/agents/_boundary.js.map +1 -1
- package/dist/claude-skills/dismiss/SKILL.md +18 -12
- package/dist/claude-skills/research/SKILL.md +21 -1
- package/dist/claude-skills/review/SKILL.md +23 -1
- package/dist/claude-skills/update/SKILL.md +24 -2
- package/dist/cli/_commit-path.d.ts +33 -0
- package/dist/cli/_commit-path.d.ts.map +1 -0
- package/dist/cli/_commit-path.js +43 -0
- package/dist/cli/_commit-path.js.map +1 -0
- package/dist/cli/dismiss.d.ts +38 -7
- package/dist/cli/dismiss.d.ts.map +1 -1
- package/dist/cli/dismiss.js +239 -54
- package/dist/cli/dismiss.js.map +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +7 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/items.d.ts +44 -0
- package/dist/cli/items.d.ts.map +1 -0
- package/dist/cli/items.js +288 -0
- package/dist/cli/items.js.map +1 -0
- package/dist/cli/research.d.ts +21 -0
- package/dist/cli/research.d.ts.map +1 -1
- package/dist/cli/research.js +360 -54
- package/dist/cli/research.js.map +1 -1
- package/dist/cli/review.d.ts +23 -0
- package/dist/cli/review.d.ts.map +1 -1
- package/dist/cli/review.js +462 -2
- package/dist/cli/review.js.map +1 -1
- package/dist/cli/source.d.ts.map +1 -1
- package/dist/cli/source.js +18 -0
- package/dist/cli/source.js.map +1 -1
- package/dist/cli/triage.d.ts +136 -0
- package/dist/cli/triage.d.ts.map +1 -0
- package/dist/cli/triage.js +1110 -0
- package/dist/cli/triage.js.map +1 -0
- package/dist/cli/undismiss.d.ts +30 -0
- package/dist/cli/undismiss.d.ts.map +1 -0
- package/dist/cli/undismiss.js +133 -0
- package/dist/cli/undismiss.js.map +1 -0
- package/dist/cli/update.d.ts.map +1 -1
- package/dist/cli/update.js +429 -141
- package/dist/cli/update.js.map +1 -1
- package/dist/cli/workflow/generate-combined-with-triage.d.ts +163 -0
- package/dist/cli/workflow/generate-combined-with-triage.d.ts.map +1 -0
- package/dist/cli/workflow/generate-combined-with-triage.js +582 -0
- package/dist/cli/workflow/generate-combined-with-triage.js.map +1 -0
- package/dist/cli/workflow.d.ts +6 -5
- package/dist/cli/workflow.d.ts.map +1 -1
- package/dist/cli/workflow.js +13 -8
- package/dist/cli/workflow.js.map +1 -1
- package/dist/core/feeds/json-api.d.ts +5 -2
- package/dist/core/feeds/json-api.d.ts.map +1 -1
- package/dist/core/feeds/json-api.js +99 -13
- package/dist/core/feeds/json-api.js.map +1 -1
- package/dist/core/feeds/types.d.ts +26 -0
- package/dist/core/feeds/types.d.ts.map +1 -1
- package/dist/core/recipes.d.ts.map +1 -1
- package/dist/core/recipes.js +6 -0
- package/dist/core/recipes.js.map +1 -1
- package/dist/core/transitions.d.ts +30 -0
- package/dist/core/transitions.d.ts.map +1 -0
- package/dist/core/transitions.js +103 -0
- package/dist/core/transitions.js.map +1 -0
- package/dist/core/triage/adapter.d.ts +80 -0
- package/dist/core/triage/adapter.d.ts.map +1 -0
- package/dist/core/triage/adapter.js +128 -0
- package/dist/core/triage/adapter.js.map +1 -0
- package/dist/core/triage/index.d.ts +105 -0
- package/dist/core/triage/index.d.ts.map +1 -0
- package/dist/core/triage/index.js +246 -0
- package/dist/core/triage/index.js.map +1 -0
- package/dist/core/triage/prompt.d.ts +30 -0
- package/dist/core/triage/prompt.d.ts.map +1 -0
- package/dist/core/triage/prompt.js +157 -0
- package/dist/core/triage/prompt.js.map +1 -0
- package/dist/core/triage/response.d.ts +114 -0
- package/dist/core/triage/response.d.ts.map +1 -0
- package/dist/core/triage/response.js +188 -0
- package/dist/core/triage/response.js.map +1 -0
- package/dist/gemini-commands/research.toml +1 -1
- package/dist/gemini-commands/review.toml +1 -1
- package/dist/gemini-commands/update.toml +1 -1
- package/dist/recipes/aws-whats-new.yaml +36 -1
- package/dist/recipes/dev-to.yaml +24 -0
- package/dist/schemas/item.d.ts +151 -5
- package/dist/schemas/item.d.ts.map +1 -1
- package/dist/schemas/item.js +164 -4
- package/dist/schemas/item.js.map +1 -1
- package/dist/schemas/recipe.d.ts +11 -1
- package/dist/schemas/recipe.d.ts.map +1 -1
- package/dist/schemas/recipe.js +10 -1
- package/dist/schemas/recipe.js.map +1 -1
- package/dist/schemas/source.d.ts +65 -4
- package/dist/schemas/source.d.ts.map +1 -1
- package/dist/schemas/source.js +65 -3
- package/dist/schemas/source.js.map +1 -1
- package/dist/skills/research/SKILL.md +57 -1
- package/dist/skills/review/SKILL.md +65 -1
- package/dist/skills/update/SKILL.md +54 -1
- package/dist/templates/agents/AGENTS.md +30 -0
- package/dist/templates/workflows/combined-with-triage.template.yaml.tmpl +132 -0
- package/package.json +1 -1
package/dist/cli/update.js
CHANGED
|
@@ -2,11 +2,13 @@ import { access, readFile, writeFile } from "node:fs/promises";
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import matter from "gray-matter";
|
|
4
4
|
import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
|
|
5
|
+
import { renderUpdatePayloadBlock } from "../agents/_boundary.js";
|
|
5
6
|
import { getAgentAdapter } from "../agents/index.js";
|
|
6
7
|
import { getDefaultAgent, loadRadarConfig, RadarConfigError } from "../core/config.js";
|
|
7
8
|
import { loadItems } from "../core/items.js";
|
|
8
9
|
import { loadTemplate } from "../core/templates.js";
|
|
9
10
|
import { AgentIdSchema, ResearchFrontmatterSchema } from "../schemas/index.js";
|
|
11
|
+
import { resolveCommitPathInside } from "./_commit-path.js";
|
|
10
12
|
import { buildAgentProgressCallback, buildReporter, ProgressFlagError, parseProgressFlags, pollOutputFileSize, } from "./_progress.js";
|
|
11
13
|
/**
|
|
12
14
|
* gray-matter defaults to js-yaml which auto-converts ISO 8601 strings to
|
|
@@ -39,6 +41,14 @@ function parseArgs(args) {
|
|
|
39
41
|
out.template = args[++i];
|
|
40
42
|
continue;
|
|
41
43
|
}
|
|
44
|
+
if (a === "--emit-payload") {
|
|
45
|
+
out.emitPayload = true;
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (a === "--commit") {
|
|
49
|
+
out.commit = args[++i];
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
42
52
|
if (a?.startsWith("--")) {
|
|
43
53
|
throw new Error(`unknown option: ${a}`);
|
|
44
54
|
}
|
|
@@ -51,7 +61,10 @@ function parseArgs(args) {
|
|
|
51
61
|
return out;
|
|
52
62
|
}
|
|
53
63
|
function printHelp(log) {
|
|
54
|
-
log("Usage:
|
|
64
|
+
log("Usage:");
|
|
65
|
+
log(" radar update <research-id> [--agent <agent-id>] [--template <template-id>]");
|
|
66
|
+
log(" radar update <research-id> --emit-payload [--template <id>]");
|
|
67
|
+
log(" radar update --commit <path>");
|
|
55
68
|
log("");
|
|
56
69
|
log("Arguments:");
|
|
57
70
|
log(" <research-id> Research id (basename of research/<id>.md without .md)");
|
|
@@ -59,6 +72,15 @@ function printHelp(log) {
|
|
|
59
72
|
log("Options:");
|
|
60
73
|
log(" --agent <agent-id> claude-code | codex-cli | gemini-cli | copilot (default: claude-code)");
|
|
61
74
|
log(" --template <id> Template id under templates/ (default: default)");
|
|
75
|
+
log(" --emit-payload Host-agent mode (ADR-0019): print the update payload to");
|
|
76
|
+
log(" stdout and DO NOT spawn an agent. The interactive host");
|
|
77
|
+
log(" session runs the SKILL procedure itself, then finalizes");
|
|
78
|
+
log(" with `radar update --commit <path>`. Interactive/opt-in");
|
|
79
|
+
log(" only — CI/headless must use the default spawn path.");
|
|
80
|
+
log(" --commit <path> Host-agent mode (ADR-0019): validate an externally-written");
|
|
81
|
+
log(" v+1 report (under <cwd>/research/) against ResearchFrontmatter-");
|
|
82
|
+
log(" Schema, assert the v+1 invariants against the `supersedes`");
|
|
83
|
+
log(" predecessor, and leave items.yaml untouched (ADR-0008).");
|
|
62
84
|
log(" --verbose Stream the agent CLI's stdout/stderr in addition to phase markers.");
|
|
63
85
|
log(" --quiet Suppress phase markers and spinner; print only the completion line.");
|
|
64
86
|
log(" Equivalent to setting RADAR_NO_PROGRESS=1 (ADR-0015 D2).");
|
|
@@ -132,117 +154,67 @@ async function findItemsForResearch(cwd, itemIds) {
|
|
|
132
154
|
return out;
|
|
133
155
|
}
|
|
134
156
|
/**
|
|
135
|
-
*
|
|
157
|
+
* Resolve the effective agent honoring the priority chain
|
|
158
|
+
* (`explicit --agent > defaultResearchAgent > hard-coded claude-code`).
|
|
136
159
|
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
* `research/<new-id>.md` (idempotent re-runs are user-facing errors).
|
|
142
|
-
* 4. Locate the linked items (read-only; their `status` is preserved per
|
|
143
|
-
* ADR-0008 / skill-design.md §8.4).
|
|
144
|
-
* 5. Reject `update` when any linked item is in `detected` / `dismissed`
|
|
145
|
-
* (no v1 research exists, or the user opted out of researching it).
|
|
146
|
-
* 6. Load `templates/<template-id>.md` (empty body when default is absent).
|
|
147
|
-
* 7. Invoke the adapter; the agent writes the new file at `outputPath`.
|
|
148
|
-
* 8. Re-read the new file, validate frontmatter against
|
|
149
|
-
* `ResearchFrontmatterSchema`, and assert v+1 invariants:
|
|
150
|
-
* - `id` matches the computed new id
|
|
151
|
-
* - `itemIds` / `templateId` / `createdAt` preserved from v(N)
|
|
152
|
-
* - `supersedes` equals the v(N) id
|
|
153
|
-
* - `reviewedAt` / `reviewedBy` are `null` (review state does NOT carry
|
|
154
|
-
* across versions per ADR-0003 / skill-design.md §8.3)
|
|
155
|
-
* 9. **No items.yaml mutation** — `update` is intentionally inert wrt item
|
|
156
|
-
* status. The v1 review event (if any) remains the authoritative record.
|
|
157
|
-
*
|
|
158
|
-
* Unlike `review`, `update` does not snapshot/restore: writing a new file is
|
|
159
|
-
* additive, so a failed adapter just leaves a missing / partial new file that
|
|
160
|
-
* the user can re-run after fixing the underlying issue. The predecessor file
|
|
161
|
-
* is never opened for writing.
|
|
160
|
+
* `update` borrows the `research` default because it shares the research SKILL
|
|
161
|
+
* body (rewrite-and-supersede strategy; skill-design.md §8.2). Extracted from
|
|
162
|
+
* the spawn path so the host-agent emit path resolves the same agent without
|
|
163
|
+
* duplicating the priority chain.
|
|
162
164
|
*/
|
|
163
|
-
|
|
164
|
-
const cwd = options.cwd ?? process.cwd();
|
|
165
|
-
const log = options.io?.log ?? ((m) => console.log(m));
|
|
166
|
-
const warn = options.io?.warn ?? ((m) => console.warn(m));
|
|
167
|
-
const error = options.io?.error ?? ((m) => console.error(m));
|
|
168
|
-
// Two-stage argv parse (see `src/cli/research.ts` for the full rationale).
|
|
169
|
-
let progressState;
|
|
170
|
-
try {
|
|
171
|
-
progressState = parseProgressFlags(args);
|
|
172
|
-
}
|
|
173
|
-
catch (e) {
|
|
174
|
-
if (e instanceof ProgressFlagError) {
|
|
175
|
-
error(`update: ${e.message}`);
|
|
176
|
-
return 2;
|
|
177
|
-
}
|
|
178
|
-
throw e;
|
|
179
|
-
}
|
|
180
|
-
const progress = options.progress ?? buildReporter({ level: progressState.level });
|
|
181
|
-
let parsed;
|
|
182
|
-
try {
|
|
183
|
-
parsed = parseArgs(progressState.rest);
|
|
184
|
-
}
|
|
185
|
-
catch (e) {
|
|
186
|
-
error(`update: ${e instanceof Error ? e.message : String(e)}`);
|
|
187
|
-
return 2;
|
|
188
|
-
}
|
|
189
|
-
if (parsed.help) {
|
|
190
|
-
printHelp(log);
|
|
191
|
-
return 0;
|
|
192
|
-
}
|
|
193
|
-
if (!parsed.researchId) {
|
|
194
|
-
error("update: missing <research-id>");
|
|
195
|
-
printHelp(error);
|
|
196
|
-
return 2;
|
|
197
|
-
}
|
|
198
|
-
// Resolve the agent honoring the priority chain. We do not yet have a
|
|
199
|
-
// `defaultUpdateAgent` field in `radar.config.yaml` (out of scope for #41
|
|
200
|
-
// per the Issue body), so resolution is:
|
|
201
|
-
// explicit --agent > defaultResearchAgent > hard-coded claude-code
|
|
202
|
-
// We pick `research` as the borrowed default because `update` shares its
|
|
203
|
-
// SKILL body (rewrite-and-supersede strategy reuses the research procedure;
|
|
204
|
-
// skill-design.md §8.2). When `radar.config.yaml` grows a dedicated
|
|
205
|
-
// `defaultUpdateAgent`, this fallback chain becomes a thin pass-through.
|
|
165
|
+
async function resolveUpdateAgent(cwd, rawAgent, error) {
|
|
206
166
|
let explicitAgent;
|
|
207
|
-
if (
|
|
208
|
-
const agentResult = AgentIdSchema.safeParse(
|
|
167
|
+
if (rawAgent !== undefined) {
|
|
168
|
+
const agentResult = AgentIdSchema.safeParse(rawAgent);
|
|
209
169
|
if (!agentResult.success) {
|
|
210
|
-
error(`update: invalid --agent '${
|
|
211
|
-
return 2;
|
|
170
|
+
error(`update: invalid --agent '${rawAgent}' (expected: claude-code | codex-cli | gemini-cli | copilot)`);
|
|
171
|
+
return { exitCode: 2 };
|
|
212
172
|
}
|
|
213
173
|
explicitAgent = agentResult.data;
|
|
214
174
|
}
|
|
215
|
-
let agent;
|
|
216
175
|
try {
|
|
217
176
|
const config = await loadRadarConfig(cwd);
|
|
218
|
-
agent = await getDefaultAgent("research", {
|
|
177
|
+
const agent = await getDefaultAgent("research", {
|
|
219
178
|
explicit: explicitAgent,
|
|
220
179
|
configOverride: config,
|
|
221
180
|
});
|
|
181
|
+
return { agent };
|
|
222
182
|
}
|
|
223
183
|
catch (e) {
|
|
224
184
|
if (e instanceof RadarConfigError) {
|
|
225
185
|
error(`update: ${e.message}`);
|
|
226
|
-
return 2;
|
|
186
|
+
return { exitCode: 2 };
|
|
227
187
|
}
|
|
228
188
|
throw e;
|
|
229
189
|
}
|
|
230
|
-
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* PRE block (shared by the spawn path and `--emit-payload`): resolve the
|
|
193
|
+
* predecessor research, validate its frontmatter, compute the deterministic
|
|
194
|
+
* v+1 `outputPath`, guard against overwriting an existing v+1, and resolve the
|
|
195
|
+
* linked items (read-only — their status is invariant under update, ADR-0008).
|
|
196
|
+
*
|
|
197
|
+
* Extracted from `runUpdate` so the host-agent emit path (#254 / ADR-0019)
|
|
198
|
+
* derives the exact same `newId` / `outputPath` and reuses the same collision +
|
|
199
|
+
* status guards as the spawn path, without the model-call step.
|
|
200
|
+
*/
|
|
201
|
+
async function prepareUpdate(params) {
|
|
202
|
+
const { cwd, researchId, warn, error, progress } = params;
|
|
231
203
|
// Resolve predecessor research path.
|
|
232
204
|
let prevId;
|
|
233
205
|
let prevPath;
|
|
234
206
|
try {
|
|
235
|
-
const resolved = resolveResearchPath(cwd,
|
|
207
|
+
const resolved = resolveResearchPath(cwd, researchId);
|
|
236
208
|
prevId = resolved.id;
|
|
237
209
|
prevPath = resolved.path;
|
|
238
210
|
}
|
|
239
211
|
catch (e) {
|
|
240
212
|
error(`update: ${e instanceof Error ? e.message : String(e)}`);
|
|
241
|
-
return 2;
|
|
213
|
+
return { exitCode: 2 };
|
|
242
214
|
}
|
|
243
215
|
if (!(await pathExists(prevPath))) {
|
|
244
216
|
error(`update: research file not found: ${prevPath}`);
|
|
245
|
-
return 1;
|
|
217
|
+
return { exitCode: 1 };
|
|
246
218
|
}
|
|
247
219
|
// Parse predecessor frontmatter.
|
|
248
220
|
let prevBody;
|
|
@@ -251,7 +223,7 @@ export async function runUpdate(args, options = {}) {
|
|
|
251
223
|
}
|
|
252
224
|
catch (e) {
|
|
253
225
|
error(`update: failed to read research file: ${e instanceof Error ? e.message : String(e)}`);
|
|
254
|
-
return 1;
|
|
226
|
+
return { exitCode: 1 };
|
|
255
227
|
}
|
|
256
228
|
let prevFrontmatterRaw;
|
|
257
229
|
try {
|
|
@@ -259,7 +231,7 @@ export async function runUpdate(args, options = {}) {
|
|
|
259
231
|
}
|
|
260
232
|
catch (e) {
|
|
261
233
|
error(`update: failed to parse frontmatter: ${e instanceof Error ? e.message : String(e)}`);
|
|
262
|
-
return 1;
|
|
234
|
+
return { exitCode: 1 };
|
|
263
235
|
}
|
|
264
236
|
const prevResult = ResearchFrontmatterSchema.safeParse(prevFrontmatterRaw);
|
|
265
237
|
if (!prevResult.success) {
|
|
@@ -267,14 +239,14 @@ export async function runUpdate(args, options = {}) {
|
|
|
267
239
|
for (const issue of prevResult.error.issues) {
|
|
268
240
|
error(` - ${issue.path.join(".") || "<root>"}: ${issue.message}`);
|
|
269
241
|
}
|
|
270
|
-
return 1;
|
|
242
|
+
return { exitCode: 1 };
|
|
271
243
|
}
|
|
272
244
|
const prevFm = prevResult.data;
|
|
273
245
|
// Sanity check: the id in the frontmatter must match the filename id so
|
|
274
246
|
// `supersedes: <prev id>` we write below points at a real predecessor.
|
|
275
247
|
if (prevFm.id !== prevId) {
|
|
276
248
|
error(`update: predecessor frontmatter id '${prevFm.id}' does not match filename id '${prevId}'`);
|
|
277
|
-
return 1;
|
|
249
|
+
return { exitCode: 1 };
|
|
278
250
|
}
|
|
279
251
|
// Compute the new id: increment the version suffix on the predecessor id.
|
|
280
252
|
let base;
|
|
@@ -286,14 +258,14 @@ export async function runUpdate(args, options = {}) {
|
|
|
286
258
|
}
|
|
287
259
|
catch (e) {
|
|
288
260
|
error(`update: ${e instanceof Error ? e.message : String(e)}`);
|
|
289
|
-
return 1;
|
|
261
|
+
return { exitCode: 1 };
|
|
290
262
|
}
|
|
291
263
|
const newVersion = prevVersion + 1;
|
|
292
264
|
const newId = `${base}_v${newVersion}`;
|
|
293
265
|
const outputPath = join(cwd, "research", `${newId}.md`);
|
|
294
266
|
if (await pathExists(outputPath)) {
|
|
295
267
|
error(`update: ${outputPath} already exists. v${newVersion} was already generated — pick a different predecessor or remove the stale file.`);
|
|
296
|
-
return 1;
|
|
268
|
+
return { exitCode: 1 };
|
|
297
269
|
}
|
|
298
270
|
// Resolve linked items. They are needed by the adapter (passed through as
|
|
299
271
|
// context) but `update` does NOT mutate them — per ADR-0008 / ADR-0003 the
|
|
@@ -301,13 +273,13 @@ export async function runUpdate(args, options = {}) {
|
|
|
301
273
|
const linkedItems = await findItemsForResearch(cwd, prevFm.itemIds);
|
|
302
274
|
if (linkedItems.length === 0) {
|
|
303
275
|
error(`update: no items/<id>.yaml found for itemIds=[${prevFm.itemIds.join(", ")}] referenced by ${prevFm.id}`);
|
|
304
|
-
return 1;
|
|
276
|
+
return { exitCode: 1 };
|
|
305
277
|
}
|
|
306
278
|
if (linkedItems.length !== prevFm.itemIds.length) {
|
|
307
279
|
const found = new Set(linkedItems.map((i) => i.id));
|
|
308
280
|
const missing = prevFm.itemIds.filter((id) => !found.has(id));
|
|
309
281
|
error(`update: ${missing.length} linked item(s) not found: ${missing.join(", ")} (referenced by ${prevFm.id})`);
|
|
310
|
-
return 1;
|
|
282
|
+
return { exitCode: 1 };
|
|
311
283
|
}
|
|
312
284
|
// Per skill-design.md §8.4, `update` requires an existing research file —
|
|
313
285
|
// i.e. the item must have been at `researched` or `reviewed` at some point.
|
|
@@ -318,7 +290,7 @@ export async function runUpdate(args, options = {}) {
|
|
|
318
290
|
error(`update: linked items must be in status 'researched' or 'reviewed'. Offenders: ${invalidStatus
|
|
319
291
|
.map((i) => `${i.id}=${i.status}`)
|
|
320
292
|
.join(", ")}`);
|
|
321
|
-
return 1;
|
|
293
|
+
return { exitCode: 1 };
|
|
322
294
|
}
|
|
323
295
|
// Surface any prompt-injection pre-filter hits recorded by the watcher
|
|
324
296
|
// (ADR-0009 M1a / M5a — Adopt). Audit-only: `update` still generates a v+1
|
|
@@ -333,55 +305,28 @@ export async function runUpdate(args, options = {}) {
|
|
|
333
305
|
progress.phase(linkedItems.length === 1
|
|
334
306
|
? `Loaded item: ${linkedItems[0].id}`
|
|
335
307
|
: `Loaded ${linkedItems.length} items`, linkedItems.map((i) => i.id).join(", "));
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
// because the predecessor was never touched and the new file simply does
|
|
357
|
-
// not appear (or is partial, in which case the user removes it and retries).
|
|
358
|
-
const adapter = getAgentAdapter(agent);
|
|
359
|
-
try {
|
|
360
|
-
await adapter.update({
|
|
361
|
-
agent,
|
|
362
|
-
templateId,
|
|
363
|
-
templateBody: template.body,
|
|
364
|
-
prevResearch: {
|
|
365
|
-
frontmatter: prevFm,
|
|
366
|
-
body: prevBody,
|
|
367
|
-
},
|
|
368
|
-
items: linkedItems,
|
|
369
|
-
outputPath,
|
|
370
|
-
cwd,
|
|
371
|
-
onProgress: buildAgentProgressCallback(progress),
|
|
372
|
-
});
|
|
373
|
-
}
|
|
374
|
-
catch (e) {
|
|
375
|
-
polling.stop();
|
|
376
|
-
progress.fail("Agent failed", e instanceof Error ? e.message : String(e));
|
|
377
|
-
error(`update: adapter failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
378
|
-
return 1;
|
|
379
|
-
}
|
|
380
|
-
polling.stop();
|
|
381
|
-
progress.succeed("Agent completed (exit 0)", Date.now() - adapterStartedAt);
|
|
382
|
-
// Re-read and validate the produced file.
|
|
308
|
+
return { prevId, prevBody, prevFm, base, newVersion, newId, outputPath, linkedItems };
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* POST block (shared by the spawn path and `--commit`): re-read the written
|
|
312
|
+
* v+1 report, validate it against `ResearchFrontmatterSchema`, assert the v+1
|
|
313
|
+
* invariants against the predecessor frontmatter (auto-correcting drift), and
|
|
314
|
+
* deliberately leave items.yaml untouched (ADR-0008).
|
|
315
|
+
*
|
|
316
|
+
* This is the single source of truth for "finalize an update" so the spawn and
|
|
317
|
+
* host-agent paths (#254 / ADR-0019) cannot diverge on schema validation, the
|
|
318
|
+
* supersedes/createdAt/itemIds drift checks, or the items.yaml status
|
|
319
|
+
* invariance — the CLI keeps owning those regardless of who wrote the file.
|
|
320
|
+
*
|
|
321
|
+
* `linkedItems` is optional: the spawn path passes the items it already
|
|
322
|
+
* resolved (drives the status phase marker); `--commit` omits it because the
|
|
323
|
+
* commit path validates the on-disk file without re-loading items (status is
|
|
324
|
+
* never mutated either way).
|
|
325
|
+
*/
|
|
326
|
+
async function finalizeUpdate(params) {
|
|
327
|
+
const { outputPath, prevFm, newId, agent, linkedItems, log, warn, error, progress } = params;
|
|
383
328
|
if (!(await pathExists(outputPath))) {
|
|
384
|
-
error(`update:
|
|
329
|
+
error(`update: did not write ${outputPath} (agent / host ignored the output path?)`);
|
|
385
330
|
return 1;
|
|
386
331
|
}
|
|
387
332
|
let newBody;
|
|
@@ -409,7 +354,7 @@ export async function runUpdate(args, options = {}) {
|
|
|
409
354
|
return 1;
|
|
410
355
|
}
|
|
411
356
|
const newFm = newResult.data;
|
|
412
|
-
// v+1 invariants. We enforce these in the CLI so a misbehaving agent
|
|
357
|
+
// v+1 invariants. We enforce these in the CLI so a misbehaving agent / host
|
|
413
358
|
// cannot silently corrupt the versioning chain. Drift is collected before
|
|
414
359
|
// we attempt repair, then a single corrected file is written.
|
|
415
360
|
const drift = [];
|
|
@@ -455,12 +400,355 @@ export async function runUpdate(args, options = {}) {
|
|
|
455
400
|
progress.phase("Frontmatter validated");
|
|
456
401
|
// `update` deliberately preserves items.yaml status (ADR-0008). We still
|
|
457
402
|
// surface a status phase marker so the progress stream stays uniform with
|
|
458
|
-
// research / review — the value just records the no-op transition.
|
|
459
|
-
|
|
403
|
+
// research / review — the value just records the no-op transition. The
|
|
404
|
+
// commit path has no resolved linked items, so it skips the per-item marker.
|
|
405
|
+
if (linkedItems !== undefined && linkedItems.length > 0) {
|
|
406
|
+
progress.phase(`Status: ${linkedItems[0].status} → ${linkedItems[0].status}`, "items.yaml unchanged per ADR-0008");
|
|
407
|
+
}
|
|
460
408
|
log(`update: wrote ${outputPath}`);
|
|
461
409
|
log(`update: supersedes ${prevFm.id} (items.yaml status unchanged per ADR-0008)`);
|
|
462
410
|
return 0;
|
|
463
411
|
}
|
|
412
|
+
/**
|
|
413
|
+
* Host-agent emit path (#254 / ADR-0019): run the same PRE block as the spawn
|
|
414
|
+
* path (`prepareUpdate`) to derive `outputPath` + predecessor context, then
|
|
415
|
+
* print the agent-neutral payload to stdout instead of spawning. The host
|
|
416
|
+
* session reads the payload, executes the SKILL procedure itself, and finalizes
|
|
417
|
+
* via `radar update --commit`.
|
|
418
|
+
*/
|
|
419
|
+
async function runUpdateEmitPayload(params) {
|
|
420
|
+
const { cwd, researchId, agent, templateId, log, warn, error, progress } = params;
|
|
421
|
+
const prepared = await prepareUpdate({ cwd, researchId, warn, error, progress });
|
|
422
|
+
if ("exitCode" in prepared)
|
|
423
|
+
return prepared.exitCode;
|
|
424
|
+
const templatesDir = join(cwd, "templates");
|
|
425
|
+
let template;
|
|
426
|
+
try {
|
|
427
|
+
template = await loadTemplate(templateId, templatesDir);
|
|
428
|
+
}
|
|
429
|
+
catch (e) {
|
|
430
|
+
error(`update: ${e instanceof Error ? e.message : String(e)}`);
|
|
431
|
+
return 1;
|
|
432
|
+
}
|
|
433
|
+
progress.phase(`Loaded template: ${templateId}.md`);
|
|
434
|
+
log(renderUpdatePayloadBlock({
|
|
435
|
+
agent,
|
|
436
|
+
templateId,
|
|
437
|
+
templateBody: template.body,
|
|
438
|
+
prevResearch: { frontmatter: prepared.prevFm, body: prepared.prevBody },
|
|
439
|
+
items: prepared.linkedItems,
|
|
440
|
+
outputPath: prepared.outputPath,
|
|
441
|
+
}));
|
|
442
|
+
return 0;
|
|
443
|
+
}
|
|
444
|
+
/**
|
|
445
|
+
* Host-agent commit path (#254 / ADR-0019): finalize a v+1 report the host
|
|
446
|
+
* session wrote out-of-band. The report is self-describing via its `supersedes`
|
|
447
|
+
* frontmatter, which names the predecessor; we read `research/<supersedes>.md`
|
|
448
|
+
* to recover the v(N) frontmatter and run the same drift checks as the spawn
|
|
449
|
+
* path. `supersedes: null` is rejected — update always has a predecessor.
|
|
450
|
+
*
|
|
451
|
+
* Before finalize, the committed path is constrained to `<cwd>/research/` so a
|
|
452
|
+
* host misled by injected content into committing an arbitrary path (e.g.
|
|
453
|
+
* `../../etc/...`) is rejected at the CLI boundary (ADR-0009 M3b enforced in
|
|
454
|
+
* code, not just SKILL guidance).
|
|
455
|
+
*/
|
|
456
|
+
async function runUpdateCommit(params) {
|
|
457
|
+
const { cwd, commitPath, log, warn, error, progress } = params;
|
|
458
|
+
const guard = await resolveCommitPathInside(cwd, "research", commitPath);
|
|
459
|
+
if ("error" in guard) {
|
|
460
|
+
error(`update: ${guard.error}`);
|
|
461
|
+
return 2;
|
|
462
|
+
}
|
|
463
|
+
const outputPath = guard.resolved;
|
|
464
|
+
if (!(await pathExists(outputPath))) {
|
|
465
|
+
error(`update: report was not written to ${outputPath} (did not write the output path?)`);
|
|
466
|
+
return 1;
|
|
467
|
+
}
|
|
468
|
+
let newBody;
|
|
469
|
+
try {
|
|
470
|
+
newBody = await readFile(outputPath, "utf8");
|
|
471
|
+
}
|
|
472
|
+
catch (e) {
|
|
473
|
+
error(`update: failed to read generated report: ${e instanceof Error ? e.message : String(e)}`);
|
|
474
|
+
return 1;
|
|
475
|
+
}
|
|
476
|
+
let newFrontmatterRaw;
|
|
477
|
+
try {
|
|
478
|
+
newFrontmatterRaw = matter(newBody, matterOptions).data;
|
|
479
|
+
}
|
|
480
|
+
catch (e) {
|
|
481
|
+
error(`update: failed to parse new frontmatter: ${e instanceof Error ? e.message : String(e)}`);
|
|
482
|
+
return 1;
|
|
483
|
+
}
|
|
484
|
+
const newResult = ResearchFrontmatterSchema.safeParse(newFrontmatterRaw);
|
|
485
|
+
if (!newResult.success) {
|
|
486
|
+
error(`update: new frontmatter does not match ResearchFrontmatterSchema:`);
|
|
487
|
+
for (const issue of newResult.error.issues) {
|
|
488
|
+
error(` - ${issue.path.join(".") || "<root>"}: ${issue.message}`);
|
|
489
|
+
}
|
|
490
|
+
return 1;
|
|
491
|
+
}
|
|
492
|
+
const newFm = newResult.data;
|
|
493
|
+
// `supersedes` names the predecessor. Update always has one (it generates
|
|
494
|
+
// v+1 from v(N)); a null supersedes means the host wrote a v1, which is a
|
|
495
|
+
// `research` artifact, not an `update` artifact.
|
|
496
|
+
if (newFm.supersedes === null) {
|
|
497
|
+
error("update: --commit report has `supersedes: null`. update finalizes a v+1 (use `radar research --commit` for a v1).");
|
|
498
|
+
return 1;
|
|
499
|
+
}
|
|
500
|
+
// Recover the predecessor frontmatter from research/<supersedes>.md so the
|
|
501
|
+
// drift checks (createdAt / itemIds preservation, supersedes wiring) run
|
|
502
|
+
// against the real v(N), exactly as the spawn path does.
|
|
503
|
+
let prevPath;
|
|
504
|
+
try {
|
|
505
|
+
prevPath = resolveResearchPath(cwd, newFm.supersedes).path;
|
|
506
|
+
}
|
|
507
|
+
catch (e) {
|
|
508
|
+
error(`update: invalid supersedes id in committed report: ${e instanceof Error ? e.message : String(e)}`);
|
|
509
|
+
return 1;
|
|
510
|
+
}
|
|
511
|
+
if (!(await pathExists(prevPath))) {
|
|
512
|
+
error(`update: predecessor research/${newFm.supersedes}.md (named by supersedes) not found under ${cwd}`);
|
|
513
|
+
return 1;
|
|
514
|
+
}
|
|
515
|
+
let prevBody;
|
|
516
|
+
try {
|
|
517
|
+
prevBody = await readFile(prevPath, "utf8");
|
|
518
|
+
}
|
|
519
|
+
catch (e) {
|
|
520
|
+
error(`update: failed to read predecessor research file: ${e instanceof Error ? e.message : String(e)}`);
|
|
521
|
+
return 1;
|
|
522
|
+
}
|
|
523
|
+
const prevResult = ResearchFrontmatterSchema.safeParse(matter(prevBody, matterOptions).data);
|
|
524
|
+
if (!prevResult.success) {
|
|
525
|
+
error(`update: predecessor frontmatter does not match ResearchFrontmatterSchema:`);
|
|
526
|
+
for (const issue of prevResult.error.issues) {
|
|
527
|
+
error(` - ${issue.path.join(".") || "<root>"}: ${issue.message}`);
|
|
528
|
+
}
|
|
529
|
+
return 1;
|
|
530
|
+
}
|
|
531
|
+
const prevFm = prevResult.data;
|
|
532
|
+
if (prevFm.id !== newFm.supersedes) {
|
|
533
|
+
error(`update: predecessor frontmatter id '${prevFm.id}' does not match supersedes '${newFm.supersedes}'`);
|
|
534
|
+
return 1;
|
|
535
|
+
}
|
|
536
|
+
// The expected new id is the basename of the committed file. finalizeUpdate
|
|
537
|
+
// re-validates the frontmatter id against it (drift auto-correction).
|
|
538
|
+
const newId = outputPath.replace(/^.*\//, "").replace(/\.md$/, "");
|
|
539
|
+
// Version monotonicity: the committed filename must be exactly one version
|
|
540
|
+
// above the predecessor of the same base, mirroring the spawn path which
|
|
541
|
+
// derives `newId = <base>_v<prev+1>` deterministically. Without this a host
|
|
542
|
+
// — possibly misled by injected content — could commit `foo_v9.md`
|
|
543
|
+
// superseding `foo_v2.md` and skip versions, breaking the ADR-0003 lineage
|
|
544
|
+
// contract. parseResearchId throws on a malformed predecessor id; that is a
|
|
545
|
+
// corrupt workspace, surfaced as an error rather than a silent pass.
|
|
546
|
+
let prevParsed;
|
|
547
|
+
try {
|
|
548
|
+
prevParsed = parseResearchId(prevFm.id);
|
|
549
|
+
}
|
|
550
|
+
catch (e) {
|
|
551
|
+
error(`update: predecessor id '${prevFm.id}' is not a valid <base>_v<n> id: ${e instanceof Error ? e.message : String(e)}`);
|
|
552
|
+
return 1;
|
|
553
|
+
}
|
|
554
|
+
const expectedNewId = `${prevParsed.base}_v${prevParsed.version + 1}`;
|
|
555
|
+
if (newId !== expectedNewId) {
|
|
556
|
+
error(`update: committed report '${newId}' must be '${expectedNewId}' — exactly v${prevParsed.version + 1} of '${prevParsed.base}' (predecessor '${prevFm.id}'). update finalizes a single version increment.`);
|
|
557
|
+
return 1;
|
|
558
|
+
}
|
|
559
|
+
// The committed file's `agent` is authoritative for the commit path: there is
|
|
560
|
+
// no `--agent` flag in play, so we accept whatever valid agent the host
|
|
561
|
+
// stamped (still schema-validated by ResearchFrontmatterSchema above).
|
|
562
|
+
return finalizeUpdate({
|
|
563
|
+
outputPath,
|
|
564
|
+
prevFm,
|
|
565
|
+
newId,
|
|
566
|
+
agent: newFm.agent,
|
|
567
|
+
linkedItems: undefined,
|
|
568
|
+
log,
|
|
569
|
+
warn,
|
|
570
|
+
error,
|
|
571
|
+
progress,
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
/**
|
|
575
|
+
* Implementation of `radar update <research-id>`.
|
|
576
|
+
*
|
|
577
|
+
* High-level flow (Phase 5, [#41](https://github.com/ozzy-labs/feedradar/issues/41)):
|
|
578
|
+
* 1. Parse + validate args (agent defaults to `claude-code`, template to `default`).
|
|
579
|
+
* 2. Resolve `research/<research-id>.md` and parse its frontmatter.
|
|
580
|
+
* 3. Compute the new id `<base>_v<n+1>` and refuse to overwrite an existing
|
|
581
|
+
* `research/<new-id>.md` (idempotent re-runs are user-facing errors).
|
|
582
|
+
* 4. Locate the linked items (read-only; their `status` is preserved per
|
|
583
|
+
* ADR-0008 / skill-design.md §8.4).
|
|
584
|
+
* 5. Reject `update` when any linked item is in `detected` / `dismissed`
|
|
585
|
+
* (no v1 research exists, or the user opted out of researching it).
|
|
586
|
+
* 6. Load `templates/<template-id>.md` (empty body when default is absent).
|
|
587
|
+
* 7. Invoke the adapter; the agent writes the new file at `outputPath`.
|
|
588
|
+
* 8. Re-read the new file, validate frontmatter against
|
|
589
|
+
* `ResearchFrontmatterSchema`, and assert v+1 invariants:
|
|
590
|
+
* - `id` matches the computed new id
|
|
591
|
+
* - `itemIds` / `templateId` / `createdAt` preserved from v(N)
|
|
592
|
+
* - `supersedes` equals the v(N) id
|
|
593
|
+
* - `reviewedAt` / `reviewedBy` are `null` (review state does NOT carry
|
|
594
|
+
* across versions per ADR-0003 / skill-design.md §8.3)
|
|
595
|
+
* 9. **No items.yaml mutation** — `update` is intentionally inert wrt item
|
|
596
|
+
* status. The v1 review event (if any) remains the authoritative record.
|
|
597
|
+
*
|
|
598
|
+
* Unlike `review`, `update` does not snapshot/restore: writing a new file is
|
|
599
|
+
* additive, so a failed adapter just leaves a missing / partial new file that
|
|
600
|
+
* the user can re-run after fixing the underlying issue. The predecessor file
|
|
601
|
+
* is never opened for writing.
|
|
602
|
+
*/
|
|
603
|
+
export async function runUpdate(args, options = {}) {
|
|
604
|
+
const cwd = options.cwd ?? process.cwd();
|
|
605
|
+
const log = options.io?.log ?? ((m) => console.log(m));
|
|
606
|
+
const warn = options.io?.warn ?? ((m) => console.warn(m));
|
|
607
|
+
const error = options.io?.error ?? ((m) => console.error(m));
|
|
608
|
+
// Two-stage argv parse (see `src/cli/research.ts` for the full rationale).
|
|
609
|
+
let progressState;
|
|
610
|
+
try {
|
|
611
|
+
progressState = parseProgressFlags(args);
|
|
612
|
+
}
|
|
613
|
+
catch (e) {
|
|
614
|
+
if (e instanceof ProgressFlagError) {
|
|
615
|
+
error(`update: ${e.message}`);
|
|
616
|
+
return 2;
|
|
617
|
+
}
|
|
618
|
+
throw e;
|
|
619
|
+
}
|
|
620
|
+
const progress = options.progress ?? buildReporter({ level: progressState.level });
|
|
621
|
+
let parsed;
|
|
622
|
+
try {
|
|
623
|
+
parsed = parseArgs(progressState.rest);
|
|
624
|
+
}
|
|
625
|
+
catch (e) {
|
|
626
|
+
error(`update: ${e instanceof Error ? e.message : String(e)}`);
|
|
627
|
+
return 2;
|
|
628
|
+
}
|
|
629
|
+
if (parsed.help) {
|
|
630
|
+
printHelp(log);
|
|
631
|
+
return 0;
|
|
632
|
+
}
|
|
633
|
+
// Host-agent commit (#254 / ADR-0019). Independent of agent / template /
|
|
634
|
+
// predecessor resolution: the report is self-describing via its `supersedes`
|
|
635
|
+
// frontmatter. Handled before the other modes since it takes a path, not a
|
|
636
|
+
// <research-id>.
|
|
637
|
+
if (parsed.commit !== undefined) {
|
|
638
|
+
if (parsed.emitPayload) {
|
|
639
|
+
error("update: --commit is incompatible with --emit-payload");
|
|
640
|
+
return 2;
|
|
641
|
+
}
|
|
642
|
+
if (parsed.researchId !== undefined) {
|
|
643
|
+
error(`update: --commit takes a <path>, not a <research-id> (got '${parsed.researchId}')`);
|
|
644
|
+
return 2;
|
|
645
|
+
}
|
|
646
|
+
return runUpdateCommit({ cwd, commitPath: parsed.commit, log, warn, error, progress });
|
|
647
|
+
}
|
|
648
|
+
if (!parsed.researchId) {
|
|
649
|
+
error("update: missing <research-id>");
|
|
650
|
+
printHelp(error);
|
|
651
|
+
return 2;
|
|
652
|
+
}
|
|
653
|
+
// Resolve the agent honoring the priority chain. We do not yet have a
|
|
654
|
+
// `defaultUpdateAgent` field in `radar.config.yaml` (out of scope for #41
|
|
655
|
+
// per the Issue body), so resolution is:
|
|
656
|
+
// explicit --agent > defaultResearchAgent > hard-coded claude-code
|
|
657
|
+
// We pick `research` as the borrowed default because `update` shares its
|
|
658
|
+
// SKILL body (rewrite-and-supersede strategy reuses the research procedure;
|
|
659
|
+
// skill-design.md §8.2). When `radar.config.yaml` grows a dedicated
|
|
660
|
+
// `defaultUpdateAgent`, this fallback chain becomes a thin pass-through.
|
|
661
|
+
const agentResult = await resolveUpdateAgent(cwd, parsed.agent, error);
|
|
662
|
+
if ("exitCode" in agentResult)
|
|
663
|
+
return agentResult.exitCode;
|
|
664
|
+
const agent = agentResult.agent;
|
|
665
|
+
const templateId = parsed.template ?? "default";
|
|
666
|
+
// Host-agent emit (#254 / ADR-0019): same predecessor / item resolution as
|
|
667
|
+
// the spawn path (`prepareUpdate`), but print the payload instead of
|
|
668
|
+
// spawning the adapter.
|
|
669
|
+
if (parsed.emitPayload) {
|
|
670
|
+
return runUpdateEmitPayload({
|
|
671
|
+
cwd,
|
|
672
|
+
researchId: parsed.researchId,
|
|
673
|
+
agent,
|
|
674
|
+
templateId,
|
|
675
|
+
log,
|
|
676
|
+
warn,
|
|
677
|
+
error,
|
|
678
|
+
progress,
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
// PRE block (shared with --emit-payload): resolve predecessor, compute the
|
|
682
|
+
// v+1 outputPath, collision-check, and resolve linked items.
|
|
683
|
+
const prepared = await prepareUpdate({
|
|
684
|
+
cwd,
|
|
685
|
+
researchId: parsed.researchId,
|
|
686
|
+
warn,
|
|
687
|
+
error,
|
|
688
|
+
progress,
|
|
689
|
+
});
|
|
690
|
+
if ("exitCode" in prepared)
|
|
691
|
+
return prepared.exitCode;
|
|
692
|
+
const { prevFm, prevBody, base, newVersion, newId, outputPath, linkedItems } = prepared;
|
|
693
|
+
// Load template.
|
|
694
|
+
const templatesDir = join(cwd, "templates");
|
|
695
|
+
let template;
|
|
696
|
+
try {
|
|
697
|
+
template = await loadTemplate(templateId, templatesDir);
|
|
698
|
+
}
|
|
699
|
+
catch (e) {
|
|
700
|
+
error(`update: ${e instanceof Error ? e.message : String(e)}`);
|
|
701
|
+
return 1;
|
|
702
|
+
}
|
|
703
|
+
progress.phase(`Loaded template: ${templateId}.md`);
|
|
704
|
+
log(`update: invoking ${agent} adapter for research '${prevFm.id}' -> ${base}_v${newVersion}.md`);
|
|
705
|
+
// Phase marker + spinner for the agent run. See `research.ts` for the
|
|
706
|
+
// shared pattern.
|
|
707
|
+
progress.phase(`Spawning ${agent}`, `cwd: ${cwd}`);
|
|
708
|
+
progress.start("Agent running");
|
|
709
|
+
const adapterStartedAt = Date.now();
|
|
710
|
+
const polling = pollOutputFileSize({ path: outputPath, reporter: progress });
|
|
711
|
+
// Invoke adapter. We do not snapshot the predecessor file: the adapter
|
|
712
|
+
// writes a new file at outputPath; if it fails, no rollback is necessary
|
|
713
|
+
// because the predecessor was never touched and the new file simply does
|
|
714
|
+
// not appear (or is partial, in which case the user removes it and retries).
|
|
715
|
+
const adapter = getAgentAdapter(agent);
|
|
716
|
+
try {
|
|
717
|
+
await adapter.update({
|
|
718
|
+
agent,
|
|
719
|
+
templateId,
|
|
720
|
+
templateBody: template.body,
|
|
721
|
+
prevResearch: {
|
|
722
|
+
frontmatter: prevFm,
|
|
723
|
+
body: prevBody,
|
|
724
|
+
},
|
|
725
|
+
items: linkedItems,
|
|
726
|
+
outputPath,
|
|
727
|
+
cwd,
|
|
728
|
+
onProgress: buildAgentProgressCallback(progress),
|
|
729
|
+
});
|
|
730
|
+
}
|
|
731
|
+
catch (e) {
|
|
732
|
+
polling.stop();
|
|
733
|
+
progress.fail("Agent failed", e instanceof Error ? e.message : String(e));
|
|
734
|
+
error(`update: adapter failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
735
|
+
return 1;
|
|
736
|
+
}
|
|
737
|
+
polling.stop();
|
|
738
|
+
progress.succeed("Agent completed (exit 0)", Date.now() - adapterStartedAt);
|
|
739
|
+
// POST block (shared with --commit): re-read, validate, drift-correct, log.
|
|
740
|
+
return finalizeUpdate({
|
|
741
|
+
outputPath,
|
|
742
|
+
prevFm,
|
|
743
|
+
newId,
|
|
744
|
+
agent,
|
|
745
|
+
linkedItems,
|
|
746
|
+
log,
|
|
747
|
+
warn,
|
|
748
|
+
error,
|
|
749
|
+
progress,
|
|
750
|
+
});
|
|
751
|
+
}
|
|
464
752
|
export const updateCommand = {
|
|
465
753
|
name: "update",
|
|
466
754
|
summary: "Refresh existing research reports against the latest items",
|