@juzser/blacksmith 0.3.0 → 0.5.0
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/.claude/agents/{auditor.md → bs-auditor.md} +8 -5
- package/.claude/agents/{coder.md → bs-coder.md} +8 -7
- package/.claude/agents/{grader.md → bs-grader.md} +16 -9
- package/.claude/agents/{merger.md → bs-merger.md} +4 -4
- package/.claude/agents/{planner.md → bs-planner.md} +20 -6
- package/.claude/agents/{researcher.md → bs-researcher.md} +6 -5
- package/.claude/agents/{reviewer.md → bs-reviewer.md} +16 -5
- package/.claude/agents/{scribe.md → bs-scribe.md} +11 -2
- package/.claude/agents/{security-reviewer.md → bs-security-reviewer.md} +9 -6
- package/.claude/agents/{spec-reviewer.md → bs-spec-reviewer.md} +18 -10
- package/.claude/agents/{tester.md → bs-tester.md} +40 -12
- package/.claude/agents/bs-uiux.md +257 -0
- package/.claude/agents/{verifier.md → bs-verifier.md} +7 -4
- package/.claude/agents/{wave-runner.md → bs-wave-runner.md} +14 -5
- package/.claude/skills/bs/SKILL.md +99 -21
- package/.claude/skills/bs/audit.md +45 -31
- package/.claude/skills/bs/dispatch.md +133 -47
- package/.claude/skills/bs/lessons.md +28 -14
- package/.claude/skills/bs/mcp.md +4 -4
- package/.claude/skills/bs/new.md +3 -3
- package/.claude/skills/bs/plan.md +57 -16
- package/.claude/skills/bs/report.md +11 -11
- package/.claude/skills/bs/run.md +190 -52
- package/.claude/skills/bs/status.md +14 -7
- package/.claude/skills/bs/ui.md +21 -5
- package/.claude/skills/bs/waivers.md +78 -2
- package/.claude/skills/bs/wave.md +143 -50
- package/README.md +53 -32
- package/factory/orchestrator/dist/agentNames.js +43 -0
- package/factory/orchestrator/dist/agents-registry.js +11 -4
- package/factory/orchestrator/dist/agentsSync.js +70 -13
- package/factory/orchestrator/dist/artifacts.js +31 -0
- package/factory/orchestrator/dist/attribution.js +4 -0
- package/factory/orchestrator/dist/audit.js +245 -20
- package/factory/orchestrator/dist/autonomy.js +6 -2
- package/factory/orchestrator/dist/budgetAlarm.js +132 -37
- package/factory/orchestrator/dist/budgets.js +302 -84
- package/factory/orchestrator/dist/claims.js +9 -19
- package/factory/orchestrator/dist/cli.js +1087 -116
- package/factory/orchestrator/dist/cliName.js +27 -0
- package/factory/orchestrator/dist/crossFinding.js +20 -0
- package/factory/orchestrator/dist/crosscheck.js +136 -8
- package/factory/orchestrator/dist/daemon.js +352 -67
- package/factory/orchestrator/dist/db/projector.js +327 -54
- package/factory/orchestrator/dist/db/queries.js +1831 -94
- package/factory/orchestrator/dist/db/schema.js +57 -4
- package/factory/orchestrator/dist/declaredArtifactLine.js +18 -0
- package/factory/orchestrator/dist/delegation.js +86 -19
- package/factory/orchestrator/dist/diffstat.js +15 -8
- package/factory/orchestrator/dist/dispatchLint.js +108 -0
- package/factory/orchestrator/dist/effort.js +14 -2
- package/factory/orchestrator/dist/effortTiers.js +13 -0
- package/factory/orchestrator/dist/env.js +26 -0
- package/factory/orchestrator/dist/epic.js +494 -42
- package/factory/orchestrator/dist/epicBudget.js +93 -0
- package/factory/orchestrator/dist/epicWidth.js +22 -5
- package/factory/orchestrator/dist/errorIssues.js +182 -11
- package/factory/orchestrator/dist/escalation.js +2 -2
- package/factory/orchestrator/dist/events.js +164 -14
- package/factory/orchestrator/dist/feedback.js +169 -0
- package/factory/orchestrator/dist/findingContext.js +26 -0
- package/factory/orchestrator/dist/findings.js +108 -14
- package/factory/orchestrator/dist/gate.js +120 -10
- package/factory/orchestrator/dist/gh.js +19 -2
- package/factory/orchestrator/dist/git.js +20 -2
- package/factory/orchestrator/dist/goalCheck.js +2 -2
- package/factory/orchestrator/dist/harness.js +25 -17
- package/factory/orchestrator/dist/hookDecision.js +411 -39
- package/factory/orchestrator/dist/integration.js +20 -8
- package/factory/orchestrator/dist/issueReporter.js +81 -16
- package/factory/orchestrator/dist/judgePreflight.js +25 -4
- package/factory/orchestrator/dist/judgeRoles.js +47 -0
- package/factory/orchestrator/dist/judgeStopHook.js +239 -0
- package/factory/orchestrator/dist/judges.js +419 -21
- package/factory/orchestrator/dist/lessons.js +190 -11
- package/factory/orchestrator/dist/logCache.js +291 -0
- package/factory/orchestrator/dist/mcp.js +6 -6
- package/factory/orchestrator/dist/paths.js +20 -7
- package/factory/orchestrator/dist/plan.js +399 -33
- package/factory/orchestrator/dist/planQuorum.js +5 -2
- package/factory/orchestrator/dist/policy.js +1275 -30
- package/factory/orchestrator/dist/projectName.js +25 -0
- package/factory/orchestrator/dist/projects.js +12 -1
- package/factory/orchestrator/dist/promptCapture.js +306 -0
- package/factory/orchestrator/dist/promptHook.js +33 -0
- package/factory/orchestrator/dist/promptImport.js +281 -0
- package/factory/orchestrator/dist/prompts.js +13 -0
- package/factory/orchestrator/dist/providers/schema-validate.js +2 -2
- package/factory/orchestrator/dist/providers/types.js +1 -1
- package/factory/orchestrator/dist/queue.js +471 -30
- package/factory/orchestrator/dist/quorum.js +18 -5
- package/factory/orchestrator/dist/results.js +25 -7
- package/factory/orchestrator/dist/roadmap.js +10 -5
- package/factory/orchestrator/dist/run-cli.js +15 -10
- package/factory/orchestrator/dist/runner.js +2 -2
- package/factory/orchestrator/dist/scheduler.js +3 -3
- package/factory/orchestrator/dist/schemas.js +56 -8
- package/factory/orchestrator/dist/severity.js +8 -4
- package/factory/orchestrator/dist/spec.js +246 -12
- package/factory/orchestrator/dist/specChange.js +13 -9
- package/factory/orchestrator/dist/stack.js +6 -6
- package/factory/orchestrator/dist/storeRoot.js +90 -0
- package/factory/orchestrator/dist/symbols.js +261 -6
- package/factory/orchestrator/dist/taskEvents.js +192 -31
- package/factory/orchestrator/dist/taskStatus.js +14 -0
- package/factory/orchestrator/dist/taxonomy.js +11 -0
- package/factory/orchestrator/dist/testerAudit.js +31 -12
- package/factory/orchestrator/dist/testgate.js +20 -16
- package/factory/orchestrator/dist/uiuxGate.js +83 -0
- package/factory/orchestrator/dist/usage.js +113 -48
- package/factory/orchestrator/dist/waivers.js +99 -12
- package/factory/orchestrator/dist/waveBudget.js +2 -0
- package/factory/orchestrator/dist/waveConcurrency.js +143 -7
- package/factory/orchestrator/dist/waveSchedule.js +84 -0
- package/factory/orchestrator/dist/workroot.js +1 -1
- package/factory/orchestrator/dist/worktree.js +76 -21
- package/factory/orchestrator/drizzle/0013_operator_feedback.sql +19 -0
- package/factory/orchestrator/drizzle/0014_misty_madripoor.sql +2 -0
- package/factory/orchestrator/drizzle/0015_gifted_omega_flight.sql +1 -0
- package/factory/orchestrator/drizzle/0016_milestones_project_default.sql +30 -0
- package/factory/orchestrator/drizzle/0017_events_raw_cli_session_id.sql +2 -0
- package/factory/orchestrator/drizzle/meta/_journal.json +35 -0
- package/factory/policies/budgets.yml +123 -33
- package/factory/policies/crosscheck.yml +51 -13
- package/factory/policies/delegation.yml +3 -3
- package/factory/policies/effort.yml +18 -10
- package/factory/policies/guardrails.yml +8 -6
- package/factory/policies/harness.yml +5 -5
- package/factory/policies/lessons.md +53 -2
- package/factory/policies/scheduler.yml +4 -4
- package/factory/policies/sensitive-paths.yml +1 -1
- package/factory/policies/severity.yml +13 -1
- package/factory/policies/stack.yml +6 -6
- package/factory/policies/taxonomy.yml +9 -3
- package/factory/policies/worktree.yml +15 -9
- package/factory/specs/roadmap.md +27 -3
- package/factory/specs/schema/event.schema.json +8 -1
- package/factory/specs/schema/result.schema.json +21 -8
- package/factory/specs/schema/task-spec.schema.json +12 -0
- package/package.json +16 -12
- package/.claude/agents/uiux.md +0 -133
|
@@ -35,19 +35,21 @@
|
|
|
35
35
|
import { existsSync, mkdirSync } from 'node:fs';
|
|
36
36
|
import path from 'node:path';
|
|
37
37
|
import Database from 'better-sqlite3';
|
|
38
|
-
import { eq } from 'drizzle-orm';
|
|
38
|
+
import { and, eq, isNull } from 'drizzle-orm';
|
|
39
39
|
import { drizzle } from 'drizzle-orm/better-sqlite3';
|
|
40
40
|
import { migrate } from 'drizzle-orm/better-sqlite3/migrator';
|
|
41
41
|
import { detectStale as detectStaleAgents, foldAgents, } from '../agents-registry.js';
|
|
42
42
|
import { eventTaskId, listSessionIds, mergeSessionLogs, readEvents, } from '../events.js';
|
|
43
|
+
import { foldOperatorFeedback } from '../feedback.js';
|
|
43
44
|
import { findingScope, foldFindingsDetailed, missingProjectionFields, } from '../findings.js';
|
|
44
45
|
import { DB_MIGRATIONS_DIR, roadmapReadPath, STATE_DB_PATH, STATE_EVENTS_DIR } from '../paths.js';
|
|
45
46
|
import { isPlanRefTaskId, latestPlanVersion, loadPlan } from '../plan.js';
|
|
47
|
+
import { normalizeProjectName } from '../projectName.js';
|
|
46
48
|
import { loadRoadmap } from '../roadmap.js';
|
|
47
49
|
import { assertRuntimeSupported } from '../runtime.js';
|
|
48
50
|
import { bareTaskId, epicOfTaskId, isQualifiedTaskId, qualifyTaskId, taskIdsMatch, } from '../taskId.js';
|
|
49
51
|
import { TERMINAL_TASK_STATUSES } from '../taskStatus.js';
|
|
50
|
-
import { RESERVED_TASK_ID, taskBranchName } from '../worktree.js';
|
|
52
|
+
import { LEGACY_BRANCH_PREFIX, RESERVED_TASK_ID, taskBranchName } from '../worktree.js';
|
|
51
53
|
import * as schema from './schema.js';
|
|
52
54
|
/** Open (creating if needed) the projection DB and ensure its schema is current. */
|
|
53
55
|
export function openDb(dbPath = STATE_DB_PATH, opts = {}) {
|
|
@@ -79,6 +81,7 @@ const ALL_TABLES = [
|
|
|
79
81
|
schema.artifacts,
|
|
80
82
|
schema.milestones,
|
|
81
83
|
schema.issue_reports,
|
|
84
|
+
schema.operatorFeedback,
|
|
82
85
|
];
|
|
83
86
|
/** Delete every row in every projection table (full "drop" for rebuild()). */
|
|
84
87
|
function clearAll(handle) {
|
|
@@ -105,6 +108,9 @@ function clearSession(db, sessionId) {
|
|
|
105
108
|
// those tables whole (D-199, D-200, and the continuation case projectTasks()
|
|
106
109
|
// describes). Deleting by session_id here would delete a row this session
|
|
107
110
|
// raised and another session has since approved, closed or worked on.
|
|
111
|
+
// `operator_feedback` is absent for the same reason: a comment recorded here
|
|
112
|
+
// can be resolved from a later, continuing session, so
|
|
113
|
+
// projectOperatorFeedback() owns that table whole too.
|
|
108
114
|
}
|
|
109
115
|
/**
|
|
110
116
|
* Fully replace the milestones table from factory/specs/roadmap.md (not
|
|
@@ -154,6 +160,19 @@ function projectMilestones(handle, opts) {
|
|
|
154
160
|
}
|
|
155
161
|
});
|
|
156
162
|
}
|
|
163
|
+
/** Task 3 (dispatch reason fallback): writers put the reason under other
|
|
164
|
+
* keys than `reason` — the dashboard's "Recent dispatch decisions" showed
|
|
165
|
+
* "no reason given" on almost every row because of it. Same chain as the
|
|
166
|
+
* UI's own fallback (ui/src/lib/timelineDisplay.ts's dispatchReasonText):
|
|
167
|
+
* no shared lib between the two runtimes, so it's kept in step by hand. */
|
|
168
|
+
function dispatchReasonText(p) {
|
|
169
|
+
for (const key of ['reason', 'rationale', 'note', 'why']) {
|
|
170
|
+
const v = p[key];
|
|
171
|
+
if (typeof v === 'string' && v.trim() !== '')
|
|
172
|
+
return v.trim();
|
|
173
|
+
}
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
157
176
|
function describeShape(value) {
|
|
158
177
|
if (value === null)
|
|
159
178
|
return 'null';
|
|
@@ -161,6 +180,26 @@ function describeShape(value) {
|
|
|
161
180
|
return 'an array';
|
|
162
181
|
return typeof value === 'object' ? 'an object' : `a ${typeof value}`;
|
|
163
182
|
}
|
|
183
|
+
/**
|
|
184
|
+
* A hand-written `task-result-recorded` from before `validateResultArtifactsShape`
|
|
185
|
+
* (events.ts) started refusing non-array artifacts at append time keyed them
|
|
186
|
+
* by name instead of listing them, e.g. `{"claude_half":
|
|
187
|
+
* "scratchpad/r23/claude-half.json"}`. The event log is append-only history,
|
|
188
|
+
* so that line can never be rewritten into the array shape the guard now
|
|
189
|
+
* requires — read it as the legacy shape it plainly is, rather than holding
|
|
190
|
+
* it back forever behind a banner an operator can never clear.
|
|
191
|
+
*
|
|
192
|
+
* Only a non-empty plain object whose every value is a non-empty string
|
|
193
|
+
* counts: an empty object, one bad value, or any other non-array shape goes
|
|
194
|
+
* through the loud skip below (D-141 — a loud undercount beats a crash).
|
|
195
|
+
*/
|
|
196
|
+
function readLegacyArtifactsMap(value) {
|
|
197
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value))
|
|
198
|
+
return null;
|
|
199
|
+
const entries = Object.entries(value);
|
|
200
|
+
const allPaths = entries.length > 0 && entries.every(([, v]) => typeof v === 'string' && v.length > 0);
|
|
201
|
+
return allPaths ? entries : null;
|
|
202
|
+
}
|
|
164
203
|
/** Splits an `error.class` string into its group/class parts; tolerant of malformed values. */
|
|
165
204
|
function splitErrorClass(value) {
|
|
166
205
|
const [group, cls] = (value ?? '').split('.');
|
|
@@ -218,10 +257,16 @@ function readClaims(raw) {
|
|
|
218
257
|
return undefined;
|
|
219
258
|
return raw.every((c) => typeof c === 'string') ? raw : undefined;
|
|
220
259
|
}
|
|
260
|
+
// Fills in `branch` for a `task-added` written before taskEvents.ts's
|
|
261
|
+
// `addedPayload` carried that field at all — every such event predates the
|
|
262
|
+
// field, so it was written by a build that only ever knew `smith/`, never
|
|
263
|
+
// `bs/`. Defaulting to `BRANCH_PREFIX` here would assert a branch
|
|
264
|
+
// `worktree create` never cut for that task; `LEGACY_BRANCH_PREFIX` is the
|
|
265
|
+
// one prefix that build could have used.
|
|
221
266
|
function branchFor(epicId, taskId) {
|
|
222
267
|
if (!epicId)
|
|
223
268
|
return null;
|
|
224
|
-
return taskBranchName(epicId, taskId);
|
|
269
|
+
return taskBranchName(epicId, taskId, LEGACY_BRANCH_PREFIX);
|
|
225
270
|
}
|
|
226
271
|
/**
|
|
227
272
|
* Every (task id, epic) pair an event asserts: from the id itself when it is
|
|
@@ -396,23 +441,55 @@ function waveTaskIds(record) {
|
|
|
396
441
|
const taskId = eventTaskId(record);
|
|
397
442
|
return taskId ? [taskId] : [];
|
|
398
443
|
}
|
|
444
|
+
/**
|
|
445
|
+
* The tasks one error-logged event names: the envelope/payload `task_id`
|
|
446
|
+
* (D-245) when the producer set one, else `payload.task_ref` split on `,`
|
|
447
|
+
* and each part trimmed. A comma joins several real ids into one string
|
|
448
|
+
* when a producer logged a single check's failure against more than one
|
|
449
|
+
* task at once — events.ts's append-time guard now refuses to write that
|
|
450
|
+
* shape, but a log written before the guard existed still carries it, and
|
|
451
|
+
* foldTasks() replays every log ever written. Splitting here, not only in
|
|
452
|
+
* assertedTaskIds()'s pre-pass, is what keeps the two agreeing on what the
|
|
453
|
+
* event names (see that function's own comment) — a fix that touched one
|
|
454
|
+
* and not the other used to mint a card whose id was the literal
|
|
455
|
+
* "epic-1/task-1,epic-1/task-2" string.
|
|
456
|
+
*/
|
|
457
|
+
function errorTaskRefIds(record) {
|
|
458
|
+
const envelopeId = eventTaskId(record);
|
|
459
|
+
if (envelopeId)
|
|
460
|
+
return [envelopeId];
|
|
461
|
+
const ref = record.payload.task_ref;
|
|
462
|
+
if (!ref)
|
|
463
|
+
return [];
|
|
464
|
+
return ref
|
|
465
|
+
.split(',')
|
|
466
|
+
.map((id) => id.trim())
|
|
467
|
+
.filter(Boolean);
|
|
468
|
+
}
|
|
469
|
+
/**
|
|
470
|
+
* `record.project`, normalized through the legacy alias -- the one place an
|
|
471
|
+
* event's raw project value becomes the current one before it reaches a
|
|
472
|
+
* row, so an old event stamped `project: 'black-smith'` never lands in the
|
|
473
|
+
* projection under that name.
|
|
474
|
+
*/
|
|
475
|
+
function recordProject(record) {
|
|
476
|
+
return record.project === undefined ? undefined : normalizeProjectName(record.project);
|
|
477
|
+
}
|
|
399
478
|
/**
|
|
400
479
|
* The task ids one event asserts something about, spelled the way foldTasks()'s
|
|
401
480
|
* switch reads them: the envelope or payload `task_id` (D-245) for the
|
|
402
481
|
* single-task events, `payload.task_ids` for a wave's admission or merge, and
|
|
403
|
-
* `error-logged`'s own `payload.task_ref
|
|
404
|
-
*
|
|
405
|
-
* pre-pass never saw.
|
|
482
|
+
* `error-logged`'s own `payload.task_ref` (or its comma-joined list — see
|
|
483
|
+
* errorTaskRefIds()). Kept beside the switch on purpose — the two must agree
|
|
484
|
+
* on what names a task, or the walk touches an id the pre-pass never saw.
|
|
406
485
|
*/
|
|
407
486
|
function assertedTaskIds(record) {
|
|
408
487
|
switch (record.event_type) {
|
|
409
488
|
case 'wave-admitted':
|
|
410
489
|
case 'wave-merged':
|
|
411
490
|
return waveTaskIds(record);
|
|
412
|
-
case 'error-logged':
|
|
413
|
-
|
|
414
|
-
return id ? [id] : [];
|
|
415
|
-
}
|
|
491
|
+
case 'error-logged':
|
|
492
|
+
return errorTaskRefIds(record);
|
|
416
493
|
case 'task-added':
|
|
417
494
|
case 'gate-outcome':
|
|
418
495
|
case 'task-superseded': {
|
|
@@ -423,6 +500,27 @@ function assertedTaskIds(record) {
|
|
|
423
500
|
return [];
|
|
424
501
|
}
|
|
425
502
|
}
|
|
503
|
+
/**
|
|
504
|
+
* The agent role an event names, read the one place both spellings live.
|
|
505
|
+
* `dispatch_decision` and `judge-reported` carry it as `agent_role`
|
|
506
|
+
* (DispatchPayload; events.ts's PAYLOAD_TAG_MAP); `task-result-recorded`
|
|
507
|
+
* carries the same fact as `agent` (results.ts's `ResultEnvelope.agent` —
|
|
508
|
+
* the role the dispatcher sent the work to). One reader for both spellings,
|
|
509
|
+
* so the auditor-close/reopen checks below never grow a second string
|
|
510
|
+
* compare that could disagree with this one.
|
|
511
|
+
*/
|
|
512
|
+
function closingTurnRole(record) {
|
|
513
|
+
const p = record.payload;
|
|
514
|
+
if (record.event_type === 'task-result-recorded')
|
|
515
|
+
return p?.agent ?? null;
|
|
516
|
+
return p?.agent_role ?? null;
|
|
517
|
+
}
|
|
518
|
+
/** Whether an event names an auditor turn — the only role that closes or
|
|
519
|
+
* reopens a task row outside wave-merged/task-superseded/error-logged (an
|
|
520
|
+
* audit axis never joins a wave, so these are the only signals it gets). */
|
|
521
|
+
function isAuditorTurn(record) {
|
|
522
|
+
return closingTurnRole(record) === 'auditor';
|
|
523
|
+
}
|
|
426
524
|
export function foldTasks(events, opts = {}) {
|
|
427
525
|
const byId = new Map();
|
|
428
526
|
// Normalisation happens at this boundary, once, before any row exists —
|
|
@@ -461,12 +559,15 @@ export function foldTasks(events, opts = {}) {
|
|
|
461
559
|
taskStatus: 'todo',
|
|
462
560
|
planVersion: null,
|
|
463
561
|
objective: null,
|
|
562
|
+
title: null,
|
|
563
|
+
summary: null,
|
|
464
564
|
claims: null,
|
|
465
565
|
budgetTokens: null,
|
|
466
566
|
branch: null,
|
|
467
567
|
createdAt: ts,
|
|
468
568
|
updatedAt: ts,
|
|
469
569
|
project: null,
|
|
570
|
+
terminalAt: null,
|
|
470
571
|
};
|
|
471
572
|
// Not every id an event carries is a task, and a ref that is not a
|
|
472
573
|
// task must never surface as a kanban card. Three shapes are refused by
|
|
@@ -545,10 +646,12 @@ export function foldTasks(events, opts = {}) {
|
|
|
545
646
|
row.taskStatus = p.task_status ?? row.taskStatus;
|
|
546
647
|
row.planVersion = p.plan_version ?? row.planVersion;
|
|
547
648
|
row.objective = p.objective ?? row.objective;
|
|
649
|
+
row.title = p.title ?? row.title;
|
|
650
|
+
row.summary = p.summary ?? row.summary;
|
|
548
651
|
row.claims = readClaims(p.claims) ?? row.claims;
|
|
549
652
|
row.budgetTokens = p.budget_tokens ?? row.budgetTokens;
|
|
550
653
|
row.branch = p.branch ?? branchFor(row.epicId, row.taskId);
|
|
551
|
-
row.project = record
|
|
654
|
+
row.project = recordProject(record) ?? row.project;
|
|
552
655
|
break;
|
|
553
656
|
}
|
|
554
657
|
case 'wave-admitted': {
|
|
@@ -570,9 +673,44 @@ export function foldTasks(events, opts = {}) {
|
|
|
570
673
|
if (!eventTask)
|
|
571
674
|
break;
|
|
572
675
|
const row = touch(eventTask, record.ts, record.session_id);
|
|
573
|
-
|
|
676
|
+
const terminal = TERMINAL_TASK_STATUSES.has(row.taskStatus);
|
|
677
|
+
if (!terminal) {
|
|
574
678
|
row.taskStatus = 'in-progress';
|
|
575
|
-
|
|
679
|
+
}
|
|
680
|
+
else if (isAuditorTurn(record) && row.taskStatus === 'completed') {
|
|
681
|
+
// An audit axis never joins a wave, so a `completed` row only ever
|
|
682
|
+
// got there via the judge-reported/task-result-recorded close
|
|
683
|
+
// below — a later auditor dispatch for the same id is a re-run of
|
|
684
|
+
// that axis, not a stray dispatch against a shipped plan task
|
|
685
|
+
// (those always move through wave-merged/task-superseded/
|
|
686
|
+
// error-logged instead, never this role). terminalAt resets to
|
|
687
|
+
// null — schema.ts's comment on tasks.terminalAt ("null while the
|
|
688
|
+
// task is still open") describes this row again, exactly. Any
|
|
689
|
+
// other terminal status (escalated, failed, waived, superseded…)
|
|
690
|
+
// is held open for the operator and must not be reopened here.
|
|
691
|
+
row.taskStatus = 'in-progress';
|
|
692
|
+
row.terminalAt = null;
|
|
693
|
+
}
|
|
694
|
+
row.project = recordProject(record) ?? row.project;
|
|
695
|
+
break;
|
|
696
|
+
}
|
|
697
|
+
case 'judge-reported':
|
|
698
|
+
case 'task-result-recorded': {
|
|
699
|
+
if (!eventTask)
|
|
700
|
+
break;
|
|
701
|
+
if (!isAuditorTurn(record))
|
|
702
|
+
break;
|
|
703
|
+
if (record.event_type === 'task-result-recorded') {
|
|
704
|
+
const p = record.payload;
|
|
705
|
+
if (p.run_status !== 'done')
|
|
706
|
+
break;
|
|
707
|
+
}
|
|
708
|
+
const row = touch(eventTask, record.ts, record.session_id);
|
|
709
|
+
// First terminal transition wins — see schema.ts's terminalAt comment.
|
|
710
|
+
if (TERMINAL_TASK_STATUSES.has(row.taskStatus))
|
|
711
|
+
break;
|
|
712
|
+
row.terminalAt = record.ts;
|
|
713
|
+
row.taskStatus = 'completed';
|
|
576
714
|
break;
|
|
577
715
|
}
|
|
578
716
|
case 'gate-outcome': {
|
|
@@ -594,38 +732,51 @@ export function foldTasks(events, opts = {}) {
|
|
|
594
732
|
}
|
|
595
733
|
case 'wave-merged': {
|
|
596
734
|
for (const taskId of waveTaskIds(record)) {
|
|
597
|
-
touch(taskId, record.ts, record.session_id)
|
|
735
|
+
const row = touch(taskId, record.ts, record.session_id);
|
|
736
|
+
// First terminal transition wins — see schema.ts's terminalAt comment.
|
|
737
|
+
if (row.terminalAt === null)
|
|
738
|
+
row.terminalAt = record.ts;
|
|
739
|
+
row.taskStatus = 'completed';
|
|
598
740
|
}
|
|
599
741
|
break;
|
|
600
742
|
}
|
|
601
743
|
case 'task-superseded': {
|
|
602
744
|
if (!eventTask)
|
|
603
745
|
break;
|
|
604
|
-
touch(eventTask, record.ts, record.session_id)
|
|
746
|
+
const row = touch(eventTask, record.ts, record.session_id);
|
|
747
|
+
if (row.terminalAt === null)
|
|
748
|
+
row.terminalAt = record.ts;
|
|
749
|
+
row.taskStatus = 'superseded';
|
|
605
750
|
break;
|
|
606
751
|
}
|
|
607
752
|
case 'error-logged': {
|
|
608
753
|
const p = record.payload;
|
|
609
|
-
//
|
|
610
|
-
//
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
754
|
+
// errorTaskRefIds() is this event's own spelling (D-245's envelope
|
|
755
|
+
// reader, or a comma-joined `task_ref` split into its real ids) —
|
|
756
|
+
// shared with assertedTaskIds()'s pre-pass so the two agree.
|
|
757
|
+
const taskIds = errorTaskRefIds(record);
|
|
758
|
+
for (const taskId of taskIds) {
|
|
759
|
+
const row = touch(taskId, record.ts, record.session_id);
|
|
760
|
+
row.project = recordProject(record) ?? row.project;
|
|
761
|
+
if (TERMINAL_TASK_STATUSES.has(row.taskStatus))
|
|
762
|
+
continue;
|
|
763
|
+
// Severity decides whether the task moves; the error class decides
|
|
764
|
+
// where. taxonomy.yml: S3 is "real but waivable; batched to operator
|
|
765
|
+
// at epic end", S4 is "logged, never asked" — a budget note or a
|
|
766
|
+
// tool hiccup at that level is on the record but the task carries on,
|
|
767
|
+
// and showing it as blocked is what the board did for 43 of the 55
|
|
768
|
+
// errors logged so far. A record with no severity is treated as
|
|
769
|
+
// major: the write path requires the field, so its absence means a
|
|
770
|
+
// log this reader does not own.
|
|
771
|
+
if (NOTE_ONLY_SEVERITIES.has(p.severity ?? ''))
|
|
772
|
+
continue;
|
|
773
|
+
const nextStatus = p.error?.startsWith('coordination.') ? 'escalated' : 'blocked';
|
|
774
|
+
// Only `escalated` is terminal (TERMINAL_TASK_STATUSES); `blocked` is
|
|
775
|
+
// not, so it never sets terminalAt.
|
|
776
|
+
if (nextStatus === 'escalated' && row.terminalAt === null)
|
|
777
|
+
row.terminalAt = record.ts;
|
|
778
|
+
row.taskStatus = nextStatus;
|
|
779
|
+
}
|
|
629
780
|
break;
|
|
630
781
|
}
|
|
631
782
|
default:
|
|
@@ -755,7 +906,7 @@ export function foldEpics(events) {
|
|
|
755
906
|
blockers: Array.isArray(p.blockers) ? p.blockers : [],
|
|
756
907
|
closedAt: record.ts,
|
|
757
908
|
eventId: event_id,
|
|
758
|
-
project: record
|
|
909
|
+
project: recordProject(record) ?? null,
|
|
759
910
|
});
|
|
760
911
|
}
|
|
761
912
|
return [...byId.values()];
|
|
@@ -784,7 +935,7 @@ export function foldEpics(events) {
|
|
|
784
935
|
* deliberate that an absent project stays absent on the wire -- but it IS a
|
|
785
936
|
* gap in these tables, because db/queries.ts's projectOf() resolves a NULL
|
|
786
937
|
* column to the DEFAULT project. Left alone, every gate outcome and every
|
|
787
|
-
* failure of a demo-rpg run reads back as
|
|
938
|
+
* failure of a demo-rpg run reads back as blacksmith's.
|
|
788
939
|
*
|
|
789
940
|
* A child row's project is its task's, which is exactly the rule the Scope
|
|
790
941
|
* docblock in db/queries.ts already states (D-170). Derived from the same
|
|
@@ -821,7 +972,7 @@ function projectResolver(taskRows) {
|
|
|
821
972
|
* rows fold to project NULL, projectResolver() hands the NULL down to every
|
|
822
973
|
* dispatch and error under them, and db/queries.ts's projectOf() resolves the
|
|
823
974
|
* lot to the DEFAULT project: the whole demo-rpg epic filed under
|
|
824
|
-
*
|
|
975
|
+
* blacksmith, its own board empty. That history cannot be repaired from the
|
|
825
976
|
* log, because the log never held the answer. The plan file did, all along.
|
|
826
977
|
*
|
|
827
978
|
* Same source and same precedence as the write side -- what the envelope
|
|
@@ -848,8 +999,12 @@ function planProjectResolver(specsDir) {
|
|
|
848
999
|
let project = null;
|
|
849
1000
|
try {
|
|
850
1001
|
const version = latestPlanVersion(epicId, { specsDir });
|
|
851
|
-
if (version !== null)
|
|
852
|
-
|
|
1002
|
+
if (version !== null) {
|
|
1003
|
+
// The plan file is immutable and may still literally say the legacy
|
|
1004
|
+
// name; normalize it the same way an event's project value is.
|
|
1005
|
+
const planProject = loadPlan(epicId, version, { specsDir }).project;
|
|
1006
|
+
project = planProject === undefined ? null : normalizeProjectName(planProject);
|
|
1007
|
+
}
|
|
853
1008
|
}
|
|
854
1009
|
catch (err) {
|
|
855
1010
|
console.error(`db/projector.ts planProjectResolver(): plan file for "${epicId}" ` +
|
|
@@ -869,11 +1024,50 @@ function planProjectResolver(specsDir) {
|
|
|
869
1024
|
* projectTasks() writes the tasks table from another over every session's
|
|
870
1025
|
* log, and projectFindings() folds the same events again for
|
|
871
1026
|
* findings.project. Split the backfill across only one of them and a single
|
|
872
|
-
* task reads as demo-rpg's on the board and
|
|
1027
|
+
* task reads as demo-rpg's on the board and blacksmith's on the errors page.
|
|
873
1028
|
*/
|
|
874
1029
|
function foldTasksWithPlanProject(events, opts) {
|
|
875
1030
|
const projectFromPlan = planProjectResolver(opts.specsDir);
|
|
876
|
-
|
|
1031
|
+
const folded = foldTasks([...events], { specsDir: opts.specsDir });
|
|
1032
|
+
// The log before the plan file: a task added with no project -- a follow-up
|
|
1033
|
+
// minted inside a wave, whose runner stamps nothing -- belongs where the
|
|
1034
|
+
// rest of its epic's tasks were stamped. The plan file is only read when
|
|
1035
|
+
// the log has nothing to say, and it may not be the project's own (ui serve
|
|
1036
|
+
// without --specs-dir reads the factory's specs).
|
|
1037
|
+
const fromSiblings = unanimousEpicProjects(folded);
|
|
1038
|
+
return folded.map((t) => t.project === null
|
|
1039
|
+
? {
|
|
1040
|
+
...t,
|
|
1041
|
+
project: (t.epicId === null ? undefined : fromSiblings.get(t.epicId)) ??
|
|
1042
|
+
projectFromPlan(t.epicId),
|
|
1043
|
+
}
|
|
1044
|
+
: t);
|
|
1045
|
+
}
|
|
1046
|
+
/**
|
|
1047
|
+
* The one project each epic's tasks agree on, keyed by epic id.
|
|
1048
|
+
*
|
|
1049
|
+
* An epic belongs to one project, so a task of it that was never stamped
|
|
1050
|
+
* belongs to the project its siblings were. An epic whose tasks name two
|
|
1051
|
+
* projects breaks that assumption, and the map leaves it out rather than
|
|
1052
|
+
* pick a winner by log order: an unanswered epic stays NULL, which is
|
|
1053
|
+
* visible; a guessed one is filed under the wrong project silently.
|
|
1054
|
+
*/
|
|
1055
|
+
export function unanimousEpicProjects(rows) {
|
|
1056
|
+
const seen = new Map();
|
|
1057
|
+
for (const { epicId, project } of rows) {
|
|
1058
|
+
if (!epicId || project === null)
|
|
1059
|
+
continue;
|
|
1060
|
+
const prior = seen.get(epicId);
|
|
1061
|
+
if (prior === undefined)
|
|
1062
|
+
seen.set(epicId, project);
|
|
1063
|
+
else if (prior !== project)
|
|
1064
|
+
seen.set(epicId, null);
|
|
1065
|
+
}
|
|
1066
|
+
const agreed = new Map();
|
|
1067
|
+
for (const [epicId, project] of seen)
|
|
1068
|
+
if (project !== null)
|
|
1069
|
+
agreed.set(epicId, project);
|
|
1070
|
+
return agreed;
|
|
877
1071
|
}
|
|
878
1072
|
export function projectSession(handle, sessionId, events, opts = {}) {
|
|
879
1073
|
const skippedArtifacts = [];
|
|
@@ -889,7 +1083,7 @@ export function projectSession(handle, sessionId, events, opts = {}) {
|
|
|
889
1083
|
// plan-file backfill lands here so they agree with the global one.
|
|
890
1084
|
const taskRows = foldTasksWithPlanProject(events, opts);
|
|
891
1085
|
const projectForTask = projectResolver(taskRows);
|
|
892
|
-
const projectForEpic =
|
|
1086
|
+
const projectForEpic = unanimousEpicProjects(taskRows);
|
|
893
1087
|
/**
|
|
894
1088
|
* The project a row inherits from whatever its ref names, task or epic.
|
|
895
1089
|
*
|
|
@@ -899,7 +1093,7 @@ export function projectSession(handle, sessionId, events, opts = {}) {
|
|
|
899
1093
|
* them (D-250), so the task leg alone can never answer. Resolving through
|
|
900
1094
|
* it alone left those rows NULL, and queries.ts's projectOf() reads a NULL
|
|
901
1095
|
* back as the DEFAULT project, not as "unscoped": every one of them was
|
|
902
|
-
* filed under
|
|
1096
|
+
* filed under blacksmith, including the one stop-the-line error in the
|
|
903
1097
|
* shipped logs, which demo-rpg had raised (D-252).
|
|
904
1098
|
*
|
|
905
1099
|
* epicOfTaskId() answers for a qualified ref; `?? ref` covers the bare
|
|
@@ -939,8 +1133,11 @@ export function projectSession(handle, sessionId, events, opts = {}) {
|
|
|
939
1133
|
planVersion: record.plan_version,
|
|
940
1134
|
causalParent: record.causal_parent,
|
|
941
1135
|
payload: JSON.stringify(record.payload),
|
|
942
|
-
project: record
|
|
1136
|
+
project: recordProject(record) ?? projectForRef(eventTask),
|
|
943
1137
|
actor: record.actor,
|
|
1138
|
+
// Copied, never derived: absent on a log written outside a CLI
|
|
1139
|
+
// session or before the stamp existed, and NULL here for both.
|
|
1140
|
+
cliSessionId: record.cli_session_id ?? null,
|
|
944
1141
|
})
|
|
945
1142
|
.run();
|
|
946
1143
|
if (record.event_type === 'user_prompt') {
|
|
@@ -973,10 +1170,10 @@ export function projectSession(handle, sessionId, events, opts = {}) {
|
|
|
973
1170
|
provider: p.provider,
|
|
974
1171
|
modelTier: p.model_tier,
|
|
975
1172
|
specRef: p.spec_ref ?? null,
|
|
976
|
-
reason: p
|
|
1173
|
+
reason: dispatchReasonText(p),
|
|
977
1174
|
parentPromptId: p.parent_prompt_id ?? null,
|
|
978
1175
|
causalParent: record.causal_parent,
|
|
979
|
-
project: record
|
|
1176
|
+
project: recordProject(record) ?? projectForRef(eventTask),
|
|
980
1177
|
})
|
|
981
1178
|
.run();
|
|
982
1179
|
continue;
|
|
@@ -1016,7 +1213,7 @@ export function projectSession(handle, sessionId, events, opts = {}) {
|
|
|
1016
1213
|
errorClass: cls,
|
|
1017
1214
|
severity: p.severity,
|
|
1018
1215
|
detail: p.detail ?? null,
|
|
1019
|
-
project: record
|
|
1216
|
+
project: recordProject(record) ?? projectForRef(p.task_ref ?? eventTask),
|
|
1020
1217
|
})
|
|
1021
1218
|
.run();
|
|
1022
1219
|
continue;
|
|
@@ -1040,7 +1237,7 @@ export function projectSession(handle, sessionId, events, opts = {}) {
|
|
|
1040
1237
|
reason: p.reason ?? null,
|
|
1041
1238
|
repoSlug: p.repo_slug ?? null,
|
|
1042
1239
|
source: p.source ?? '',
|
|
1043
|
-
project: record
|
|
1240
|
+
project: recordProject(record) ?? projectForRef(p.task_ref ?? eventTask),
|
|
1044
1241
|
})
|
|
1045
1242
|
.run();
|
|
1046
1243
|
continue;
|
|
@@ -1058,6 +1255,28 @@ export function projectSession(handle, sessionId, events, opts = {}) {
|
|
|
1058
1255
|
// nothing. Hold back the one list that cannot be read, name it, and
|
|
1059
1256
|
// keep going (D-141: a loud undercount beats a crash).
|
|
1060
1257
|
if (p.artifacts !== undefined && !Array.isArray(p.artifacts)) {
|
|
1258
|
+
const legacyMap = readLegacyArtifactsMap(p.artifacts);
|
|
1259
|
+
if (legacyMap) {
|
|
1260
|
+
legacyMap.forEach(([description, artifactPath], index) => {
|
|
1261
|
+
txDb
|
|
1262
|
+
.insert(schema.artifacts)
|
|
1263
|
+
.values({
|
|
1264
|
+
id: `${event_id}#${index}`,
|
|
1265
|
+
sessionId: record.session_id,
|
|
1266
|
+
taskId,
|
|
1267
|
+
eventId: event_id,
|
|
1268
|
+
ts: record.ts,
|
|
1269
|
+
// Never 'screenshot': this is history from before artifacts
|
|
1270
|
+
// carried a type at all. An image path can still reach the
|
|
1271
|
+
// gallery by its extension, which is what it is.
|
|
1272
|
+
type: 'file',
|
|
1273
|
+
path: artifactPath,
|
|
1274
|
+
description,
|
|
1275
|
+
})
|
|
1276
|
+
.run();
|
|
1277
|
+
});
|
|
1278
|
+
continue;
|
|
1279
|
+
}
|
|
1061
1280
|
skippedArtifacts.push({
|
|
1062
1281
|
event_id,
|
|
1063
1282
|
session_id: record.session_id,
|
|
@@ -1180,7 +1399,8 @@ export function projectSession(handle, sessionId, events, opts = {}) {
|
|
|
1180
1399
|
export function projectTasks(handle, events, opts = {}) {
|
|
1181
1400
|
handle.db.transaction((txDb) => {
|
|
1182
1401
|
txDb.delete(schema.tasks).run();
|
|
1183
|
-
|
|
1402
|
+
const rows = foldTasksWithPlanProject(events, opts);
|
|
1403
|
+
for (const task of rows) {
|
|
1184
1404
|
txDb
|
|
1185
1405
|
.insert(schema.tasks)
|
|
1186
1406
|
.values({
|
|
@@ -1192,15 +1412,30 @@ export function projectTasks(handle, events, opts = {}) {
|
|
|
1192
1412
|
taskStatus: task.taskStatus,
|
|
1193
1413
|
planVersion: task.planVersion,
|
|
1194
1414
|
objective: task.objective,
|
|
1415
|
+
title: task.title,
|
|
1416
|
+
summary: task.summary,
|
|
1195
1417
|
claims: task.claims ? JSON.stringify(task.claims) : null,
|
|
1196
1418
|
budgetTokens: task.budgetTokens,
|
|
1197
1419
|
branch: task.branch,
|
|
1198
1420
|
createdAt: task.createdAt,
|
|
1199
1421
|
updatedAt: task.updatedAt,
|
|
1200
1422
|
project: task.project,
|
|
1423
|
+
terminalAt: task.terminalAt,
|
|
1201
1424
|
})
|
|
1202
1425
|
.run();
|
|
1203
1426
|
}
|
|
1427
|
+
// The epics row is written per session, and a session that closes an
|
|
1428
|
+
// epic it never planned -- a continuation -- folds none of its tasks, so
|
|
1429
|
+
// the row's own fallback (projectForEpic in projectSession) has nothing
|
|
1430
|
+
// to read and stays NULL. Every session's tasks are in hand here: fill
|
|
1431
|
+
// what is still unanswered, and never overwrite a stamped project.
|
|
1432
|
+
for (const [epicId, project] of unanimousEpicProjects(rows)) {
|
|
1433
|
+
txDb
|
|
1434
|
+
.update(schema.epics)
|
|
1435
|
+
.set({ project })
|
|
1436
|
+
.where(and(eq(schema.epics.epicId, epicId), isNull(schema.epics.project)))
|
|
1437
|
+
.run();
|
|
1438
|
+
}
|
|
1204
1439
|
});
|
|
1205
1440
|
}
|
|
1206
1441
|
/**
|
|
@@ -1265,6 +1500,41 @@ function projectLessons(handle, events) {
|
|
|
1265
1500
|
}
|
|
1266
1501
|
});
|
|
1267
1502
|
}
|
|
1503
|
+
/**
|
|
1504
|
+
* Fully replace the operator_feedback table from EVERY session's log at
|
|
1505
|
+
* once, in one causal order (mergeSessionLogs) — the same shape as
|
|
1506
|
+
* projectLessons() above, and for the same reason: a comment recorded in one
|
|
1507
|
+
* session's log is routinely resolved (bounced, or turned into a follow-up
|
|
1508
|
+
* task) from a later, continuing session, so the table's primary key is the
|
|
1509
|
+
* feedback id, not (session, feedback) — one global key, one global fold.
|
|
1510
|
+
* feedback.ts's foldOperatorFeedback() already resolves a `resolved` event
|
|
1511
|
+
* against the `recorded` row it belongs to; this only re-inserts the result.
|
|
1512
|
+
*/
|
|
1513
|
+
function projectOperatorFeedback(handle, events) {
|
|
1514
|
+
handle.db.transaction((txDb) => {
|
|
1515
|
+
txDb.delete(schema.operatorFeedback).run();
|
|
1516
|
+
for (const row of foldOperatorFeedback(events)) {
|
|
1517
|
+
txDb
|
|
1518
|
+
.insert(schema.operatorFeedback)
|
|
1519
|
+
.values({
|
|
1520
|
+
id: row.feedbackId,
|
|
1521
|
+
taskId: row.taskId,
|
|
1522
|
+
sessionId: row.sessionId,
|
|
1523
|
+
body: row.body,
|
|
1524
|
+
kind: row.kind,
|
|
1525
|
+
source: row.source,
|
|
1526
|
+
externalId: row.externalId,
|
|
1527
|
+
author: row.author,
|
|
1528
|
+
recordedAt: row.recordedAt,
|
|
1529
|
+
recordedEventId: row.recordedEventId,
|
|
1530
|
+
resolvedAt: row.resolvedAt,
|
|
1531
|
+
resolution: row.resolution,
|
|
1532
|
+
followUpTaskId: row.followUpTaskId,
|
|
1533
|
+
})
|
|
1534
|
+
.run();
|
|
1535
|
+
}
|
|
1536
|
+
});
|
|
1537
|
+
}
|
|
1268
1538
|
function findingTimestamps(events) {
|
|
1269
1539
|
const stamps = new Map();
|
|
1270
1540
|
for (const { event_id, record } of events) {
|
|
@@ -1349,7 +1619,7 @@ function projectFindings(handle, events, opts) {
|
|
|
1349
1619
|
// against `<epic>/integration`, a pseudo-task that never gets a task row:
|
|
1350
1620
|
// no task lookup can ever answer for it, its epic always can, and the row
|
|
1351
1621
|
// already stores that epic. On the shipped logs this is 37 of 56 findings.
|
|
1352
|
-
const projectForEpic =
|
|
1622
|
+
const projectForEpic = unanimousEpicProjects(taskRows);
|
|
1353
1623
|
const projectFromPlan = planProjectResolver(opts.specsDir);
|
|
1354
1624
|
const rows = [];
|
|
1355
1625
|
for (const finding of findings) {
|
|
@@ -1387,7 +1657,7 @@ function projectFindings(handle, events, opts) {
|
|
|
1387
1657
|
taskId: finding.task_id,
|
|
1388
1658
|
epicId: findingEpicId,
|
|
1389
1659
|
project: projectForTask(finding.task_id) ??
|
|
1390
|
-
projectForEpic.get(findingEpicId) ??
|
|
1660
|
+
(findingEpicId === null ? undefined : projectForEpic.get(findingEpicId)) ??
|
|
1391
1661
|
projectFromPlan(findingEpicId),
|
|
1392
1662
|
fingerprint: finding.fingerprint,
|
|
1393
1663
|
findingCategory: finding.finding_category,
|
|
@@ -1472,6 +1742,7 @@ export async function rebuild(dbPath = STATE_DB_PATH, sessions = 'all', opts = {
|
|
|
1472
1742
|
projectTasks(handle, merged, opts);
|
|
1473
1743
|
const skippedFindings = projectFindings(handle, merged, opts);
|
|
1474
1744
|
projectLessons(handle, merged);
|
|
1745
|
+
projectOperatorFeedback(handle, merged);
|
|
1475
1746
|
projectMilestones(handle, opts);
|
|
1476
1747
|
return {
|
|
1477
1748
|
sessionsProcessed: sessionIds.length,
|
|
@@ -1490,10 +1761,11 @@ export async function rebuild(dbPath = STATE_DB_PATH, sessions = 'all', opts = {
|
|
|
1490
1761
|
* replaces only its rows, leaving every other session's projection intact.
|
|
1491
1762
|
* Safe to call repeatedly while a session is still running (tailing).
|
|
1492
1763
|
*
|
|
1493
|
-
* The
|
|
1764
|
+
* The five tables that are not session-scoped are rewritten whole on every
|
|
1494
1765
|
* call: `milestones` from roadmap.md, and — from every session's log, which is
|
|
1495
1766
|
* why this reads more than the one session it re-folds — `tasks`
|
|
1496
|
-
* (projectTasks()), `lessons` (D-199)
|
|
1767
|
+
* (projectTasks()), `lessons` (D-199), `findings` (D-200) and
|
|
1768
|
+
* `operator_feedback` (projectOperatorFeedback(), same shape).
|
|
1497
1769
|
*/
|
|
1498
1770
|
export async function apply(dbPath = STATE_DB_PATH, sessionId, opts = {}) {
|
|
1499
1771
|
const handle = openDb(dbPath, opts);
|
|
@@ -1516,6 +1788,7 @@ export async function apply(dbPath = STATE_DB_PATH, sessionId, opts = {}) {
|
|
|
1516
1788
|
projectTasks(handle, merged, opts);
|
|
1517
1789
|
const skippedFindings = projectFindings(handle, merged, opts);
|
|
1518
1790
|
projectLessons(handle, merged);
|
|
1791
|
+
projectOperatorFeedback(handle, merged);
|
|
1519
1792
|
projectMilestones(handle, opts);
|
|
1520
1793
|
return {
|
|
1521
1794
|
sessionsProcessed: 1,
|