@zq-silk/yui 0.6.14 → 0.6.16

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.
@@ -1137,7 +1137,7 @@ export const ROOT_COMMAND = buildNode({
1137
1137
  {
1138
1138
  id: "manage",
1139
1139
  title: "Commands",
1140
- entries: ["add", "update", "retire", "list", "show", "propose", "proposals", "accept", "reject"]
1140
+ entries: ["add", "retire", "list", "show", "propose", "proposals", "accept", "reject"]
1141
1141
  }
1142
1142
  ],
1143
1143
  children: [
@@ -1147,12 +1147,6 @@ export const ROOT_COMMAND = buildNode({
1147
1147
  usage: "yui project knowledge add <project> <title> --body <text>",
1148
1148
  options: ["--body"]
1149
1149
  },
1150
- {
1151
- name: "update",
1152
- summary: "Update active Project knowledge (Operator authority).",
1153
- usage: "yui project knowledge update <project> <knowledge> [--title <text>] [--body <text>]",
1154
- options: ["--title", "--body"]
1155
- },
1156
1150
  {
1157
1151
  name: "retire",
1158
1152
  summary: "Retire Project knowledge without deleting its record (Operator authority).",
package/dist/cli.js CHANGED
@@ -1699,13 +1699,16 @@ async function deltaRecheckPreflightForTaskCommand(args, store, actualTaskReview
1699
1699
  if (previous === undefined) {
1700
1700
  throw usageError("Delta-recheck requires a previous completed Task-final Review that accepted a head.");
1701
1701
  }
1702
- const projectId = actualTaskReviewCandidate.projects[0].projectId;
1703
- const project = store.getProject(projectId);
1704
- if (project === null) {
1705
- throw usageError(`Delta-recheck Project not found: ${projectId}.`);
1702
+ const repositoryPaths = {};
1703
+ for (const candidateProject of actualTaskReviewCandidate.projects) {
1704
+ const project = store.getProject(candidateProject.projectId);
1705
+ if (project === null) {
1706
+ throw usageError(`Delta-recheck Project not found: ${candidateProject.projectId}.`);
1707
+ }
1708
+ repositoryPaths[candidateProject.projectId] = project.path;
1706
1709
  }
1707
1710
  const assessment = await assessDeltaRecheck({
1708
- repositoryPath: project.path,
1711
+ repositoryPaths,
1709
1712
  previousRound: previous,
1710
1713
  candidate: actualTaskReviewCandidate,
1711
1714
  git: new NodeGitWorkspace(),
@@ -610,23 +610,10 @@ function projectKnowledge(args, store, options) {
610
610
  };
611
611
  }
612
612
  if (command === "update") {
613
- const usage = "Project knowledge update usage: yui project knowledge update <project> <knowledge> [--title <text>] [--body <text>].";
614
- const parsed = parseKnowledgeUpdateArguments(rest, usage);
615
- assertKnowledgeOperator(options, "update");
616
- const updated = store.transaction((tx) => {
617
- const project = requireProject(tx, parsed.project);
618
- const next = updateProjectKnowledge(project, parsed.id, {
619
- ...(parsed.title === undefined ? {} : { title: parsed.title }),
620
- ...(parsed.body === undefined ? {} : { body: parsed.body })
621
- }, (options.now ?? (() => new Date()))());
622
- tx.saveProject(next);
623
- return next;
624
- });
625
- return {
626
- output: `Updated project knowledge ${parsed.id} in ${updated.id}\n`,
627
- projectId: updated.id,
628
- knowledgeId: parsed.id
629
- };
613
+ throw usageError("Direct Project Knowledge update is not supported because it would erase version history. "
614
+ + "Propose the replacement with `yui project knowledge propose <project> ... --task <task>` "
615
+ + "and let the Operator apply it with `yui project knowledge accept <project> <proposal> "
616
+ + "--update <knowledge>`; use --supersedes when replacing Knowledge without proposal history.");
630
617
  }
631
618
  if (command === "retire") {
632
619
  if (rest.length !== 2) {
@@ -1024,6 +1011,17 @@ function acceptKnowledgeProposal(args, store, options) {
1024
1011
  if (target.status !== "active") {
1025
1012
  throw usageError(`Knowledge is not active: ${target.id}.`);
1026
1013
  }
1014
+ const previousProposal = target.provenance?.proposalId === undefined
1015
+ ? null
1016
+ : findKnowledgeProposal(next, target.provenance.proposalId);
1017
+ if (previousProposal === null
1018
+ || previousProposal.status !== "accepted"
1019
+ || previousProposal.knowledgeId !== target.id
1020
+ || previousProposal.title !== target.title
1021
+ || previousProposal.body !== target.body) {
1022
+ throw usageError(`Knowledge ${target.id} has no complete proposal-backed version history. `
1023
+ + "Create the replacement proposal with --supersedes instead of --update.");
1024
+ }
1027
1025
  knowledgeId = target.id;
1028
1026
  next = updateProjectKnowledge(next, knowledgeId, {
1029
1027
  title: proposal.title,
@@ -1255,43 +1253,6 @@ function parseProjectUpdateArguments(args, usage) {
1255
1253
  ...(development === undefined ? {} : { development })
1256
1254
  };
1257
1255
  }
1258
- function parseKnowledgeUpdateArguments(args, usage) {
1259
- const positionals = [];
1260
- let title;
1261
- let body;
1262
- for (let index = 0; index < args.length; index += 1) {
1263
- const value = args[index];
1264
- if (value === "--title" || value === "--body") {
1265
- const next = args[index + 1];
1266
- if (next === undefined || next.startsWith("--"))
1267
- throw usageError(`${value} is required. ${usage}`);
1268
- if (value === "--title") {
1269
- if (title !== undefined)
1270
- throw usageError(`--title may only be provided once. ${usage}`);
1271
- title = requireText(next, value);
1272
- }
1273
- else {
1274
- if (body !== undefined)
1275
- throw usageError(`--body may only be provided once. ${usage}`);
1276
- body = requireText(next, value);
1277
- }
1278
- index += 1;
1279
- continue;
1280
- }
1281
- if (value.startsWith("--"))
1282
- throw usageError(`Unknown option: ${value}. ${usage}`);
1283
- positionals.push(value);
1284
- }
1285
- if (positionals.length !== 2 || (title === undefined && body === undefined)) {
1286
- throw usageError(usage);
1287
- }
1288
- return {
1289
- project: positionals[0],
1290
- id: positionals[1],
1291
- ...(title === undefined ? {} : { title }),
1292
- ...(body === undefined ? {} : { body })
1293
- };
1294
- }
1295
1256
  function parseCloneArguments(args, usage) {
1296
1257
  const positionals = [];
1297
1258
  const aliases = [];
@@ -60,8 +60,11 @@ function usageTotal(usage) {
60
60
  const inputTokens = integer(record.inputTokens);
61
61
  if (inputTokens === null)
62
62
  return null;
63
- const cachedInputTokens = integer(record.cachedInputTokens) ?? 0;
64
- return inputTokens + cachedInputTokens;
63
+ // RuntimeUsageSnapshot.inputTokens is the normalized processed input total.
64
+ // cachedInputTokens is an informational breakdown of that total, matching
65
+ // runtimeProjection and the Driver documentation; adding it again would
66
+ // fabricate context pressure and trigger premature Session rollover.
67
+ return inputTokens;
65
68
  }
66
69
  function integer(value) {
67
70
  return Number.isSafeInteger(value) && value >= 0 ? value : null;
@@ -2068,6 +2068,14 @@ export class FileSchedulerStoreAdapter {
2068
2068
  if (input.roleName !== "leader") {
2069
2069
  enqueueWork(store, { kind: "role", taskId: input.taskId, roleName: "leader" }, "provider-policy-blocked", now, [{ type: "run", taskId: input.taskId, id: input.runId }]);
2070
2070
  }
2071
+ else {
2072
+ const message = `Leader Run ${input.runId} is blocked by Provider policy: ${summary}`;
2073
+ store.saveOperatorNotification(createLeaderRecoveryNotification(input.taskId, message, now, store.getOperatorNotification(input.taskId)));
2074
+ enqueueWork(store, { kind: "operator" }, "leader-provider-policy-blocked", now, [
2075
+ { type: "task", id: input.taskId },
2076
+ { type: "run", taskId: input.taskId, id: input.runId }
2077
+ ]);
2078
+ }
2071
2079
  return { disposition: "applied", runId: input.runId };
2072
2080
  }
2073
2081
  recordProviderRetryClassified(store, input, errorClass, extra, now) {
@@ -507,7 +507,11 @@ function matchesRecoverySessionFence(store, input) {
507
507
  return false;
508
508
  if (session.agentId !== input.agentId || session.adapterId !== input.adapterId)
509
509
  return false;
510
- if (session.status === "stopped" || session.status === "broken")
510
+ // A dead Session is precisely when replace-session/terminate recovery is
511
+ // needed. Preserve its exact identity as the CAS fence; only same-Session
512
+ // retry is invalid once the native process is stopped or broken.
513
+ if ((session.status === "stopped" || session.status === "broken")
514
+ && input.action === "retry")
511
515
  return false;
512
516
  const sessionNativeSessionId = session.nativeSessionId;
513
517
  if (sessionNativeSessionId === undefined) {
@@ -7,7 +7,7 @@ import { validateDeltaRecheckRecord } from "./reviewRound.js";
7
7
  * Reviewer's explicit disposition.
8
8
  */
9
9
  export async function assessDeltaRecheck(input) {
10
- const { repositoryPath, previousRound, candidate, git, config } = input;
10
+ const { repositoryPaths, previousRound, candidate, git, config } = input;
11
11
  if (previousRound.status !== "completed"
12
12
  || (previousRound.scope ?? "work-item") !== "task") {
13
13
  return {
@@ -28,6 +28,13 @@ export async function assessDeltaRecheck(input) {
28
28
  let deletedLines = 0;
29
29
  let anyChange = false;
30
30
  for (const project of candidate.projects) {
31
+ const repositoryPath = repositoryPaths[project.projectId];
32
+ if (repositoryPath === undefined) {
33
+ return {
34
+ kind: "ineligible",
35
+ reason: `Delta recheck repository is unavailable for Project ${project.projectId}.`
36
+ };
37
+ }
31
38
  const previousHead = previousByProject.get(project.projectId);
32
39
  if (previousHead === undefined) {
33
40
  return {
@@ -185,14 +192,19 @@ function digestDiff(diffByProject) {
185
192
  * the dispatch context; only path-like entries drive the deterministic gate.
186
193
  */
187
194
  function extractEvidencePaths(round) {
188
- return [...new Set(extractEvidenceFromReportJson(round.report ?? ""))]
195
+ return previousEvidenceReferences(round)
189
196
  .filter((entry) => isPathLike(entry));
190
197
  }
191
- /** All evidence references from the previous Round's JSON report. */
198
+ /** All path-like evidence references from Markdown or JSON Review reports. */
192
199
  function previousEvidenceReferences(round) {
193
- return extractEvidenceFromReportJson(round.report ?? "");
200
+ const report = round.report ?? "";
201
+ return [...new Set([
202
+ ...extractDeclaredEvidence(report),
203
+ ...extractEvidenceReferences(report)
204
+ ])];
194
205
  }
195
- function extractEvidenceFromReportJson(report) {
206
+ /** Preserve the free-form evidence array that older JSON reports exposed. */
207
+ function extractDeclaredEvidence(report) {
196
208
  let parsed;
197
209
  try {
198
210
  parsed = JSON.parse(report);
@@ -205,9 +217,18 @@ function extractEvidenceFromReportJson(report) {
205
217
  const evidence = parsed.evidence;
206
218
  if (!Array.isArray(evidence))
207
219
  return [];
208
- return evidence
209
- .filter((entry) => typeof entry === "string")
210
- .map((entry) => entry.trim());
220
+ return evidence.flatMap((entry) => (typeof entry === "string" && entry.trim().length > 0 ? [entry.trim()] : []));
221
+ }
222
+ /**
223
+ * Reviewer reports intentionally accept clear Markdown or JSON without a
224
+ * fixed schema. Extract conservative repo-relative path tokens from the full
225
+ * preserved report so evidence overlap cannot be bypassed by presentation
226
+ * format, nested JSON, Markdown links, backticks, or line-qualified paths.
227
+ */
228
+ function extractEvidenceReferences(report) {
229
+ const references = report.match(/(?:\.{0,2}\/)?[A-Za-z0-9_.-]+(?:\/[A-Za-z0-9_.-]+)+|[A-Za-z0-9_.-]+\.[A-Za-z0-9]{1,12}/gu) ?? [];
230
+ return [...new Set(references.map((entry) => entry.replace(/^\.\//u, "")))]
231
+ .filter((entry) => isPathLike(entry));
211
232
  }
212
233
  function isPathLike(value) {
213
234
  if (value.length === 0 || value.length > 512)
@@ -223,8 +244,8 @@ function isPathLike(value) {
223
244
  return value.includes("/") || /\.[A-Za-z0-9]{1,12}$/u.test(value);
224
245
  }
225
246
  function pathEvidenceMatches(evidencePath, changedFile) {
226
- const normalizedEvidence = evidencePath.replace(/^\.\//u, "");
227
- const normalizedFile = changedFile.replace(/^\.\//u, "");
247
+ const normalizedEvidence = evidencePath.replace(/^\.\//u, "").replace(/\/+$/u, "");
248
+ const normalizedFile = changedFile.replace(/^\.\//u, "").replace(/\/+$/u, "");
228
249
  if (normalizedEvidence === normalizedFile)
229
250
  return true;
230
251
  // A directory-level evidence reference covers every file under it.
@@ -57,8 +57,11 @@ export function projectRunRecovery(facts) {
57
57
  ? ["accepted", "ambiguous"]
58
58
  : ["rejected", "ambiguous"];
59
59
  const blocked = recoveryBlocker(facts, session, canonicalProgressAt);
60
+ const supportedActions = session?.status === "stopped" || session?.status === "broken"
61
+ ? RUN_RECOVERY_ACTIONS.filter((action) => action !== "retry")
62
+ : RUN_RECOVERY_ACTIONS;
60
63
  const actions = blocked === null
61
- ? RUN_RECOVERY_ACTIONS.map((action) => buildActionPlan(facts, action, session, canonicalProgressAt))
64
+ ? supportedActions.map((action) => buildActionPlan(facts, action, session, canonicalProgressAt))
62
65
  : [];
63
66
  const judgmentRequired = blocked === null && actions.some((plan) => plan.argv.includes(PROVIDER_ACCEPTANCE_PLACEHOLDER))
64
67
  ? "Provider acceptance is not durably determined for every action; pass --provider-acceptance explicitly."
@@ -121,10 +124,6 @@ function recoveryBlocker(facts, session, canonicalProgressAt) {
121
124
  return "progress-unavailable";
122
125
  if (session === null)
123
126
  return "session-missing";
124
- if (session.status === "stopped")
125
- return "session-stopped";
126
- if (session.status === "broken")
127
- return "session-broken";
128
127
  return null;
129
128
  }
130
129
  function buildActionPlan(facts, action, session, canonicalProgressAt) {
@@ -1,3 +1,4 @@
1
+ import { createHash } from "node:crypto";
1
2
  import { requireDriverId } from "./agentDriver.js";
2
3
  export const RUNTIME_OBSERVATION_TASK_EVENT = "runtime.observation";
3
4
  const KINDS = [
@@ -405,7 +406,11 @@ export function runtimeObservationSemanticKey(input) {
405
406
  ].join(":");
406
407
  }
407
408
  if (input.kind === "continuation.reported") {
408
- return ["continuation-report", ...continuationIdentity, input.payload?.reportId ?? "missing"]
409
+ const summary = input.payload?.summary?.trim();
410
+ const resultIdentity = summary === undefined || summary.length === 0
411
+ ? input.payload?.reportId ?? "missing"
412
+ : `sha256:${createHash("sha256").update(summary).digest("hex")}`;
413
+ return ["continuation-report", ...continuationIdentity, resultIdentity]
409
414
  .join(":");
410
415
  }
411
416
  if (input.kind === "continuation.started") {
@@ -984,6 +984,11 @@ const STORED_TASK_V16_FIELDS = [
984
984
  "integrationQueue",
985
985
  "durableJobs",
986
986
  "jobCallerKeyHashes",
987
+ // The SQLite reverse reader reconstructs every physical family map even
988
+ // when the older manifest has not introduced that family yet. A v16
989
+ // aggregate may therefore contain an empty wakes map; preserve that exact
990
+ // repair shape, but reject records because taskWake has no v16 version.
991
+ "wakes",
987
992
  // May already be present after the publicationReference introduction runs
988
993
  // first; the 16->17 normalizer preserves the empty introduced map.
989
994
  "publicationReferences",
@@ -1022,6 +1027,12 @@ function requireStoredTaskV16Shape(snapshot) {
1022
1027
  if (unknown !== undefined) {
1023
1028
  throw new Error(`Task aggregate ${taskId} has an unknown v16 field: ${unknown}.`);
1024
1029
  }
1030
+ if (task.wakes !== undefined) {
1031
+ const wakes = asObject(task.wakes, `Task aggregate ${taskId} wakes`);
1032
+ if (Object.keys(wakes).length > 0) {
1033
+ throw new Error(`Task aggregate ${taskId} already has Task wakes before v17.`);
1034
+ }
1035
+ }
1025
1036
  if (task.publicationReferences !== undefined) {
1026
1037
  const references = asObject(task.publicationReferences, `Task aggregate ${taskId} publicationReferences`);
1027
1038
  if (Object.keys(references).length > 0) {
@@ -1049,6 +1060,9 @@ function normalizeStoredTaskV16ToV17(snapshot) {
1049
1060
  const existingReferences = task.publicationReferences === undefined
1050
1061
  ? {}
1051
1062
  : asObject(task.publicationReferences, `Task aggregate ${taskId} publicationReferences`);
1063
+ const existingWakes = task.wakes === undefined
1064
+ ? {}
1065
+ : asObject(task.wakes, `Task aggregate ${taskId} wakes`);
1052
1066
  const marks = asObject(task.idHighWaterMarks, `Task id high-water marks ${taskId}`);
1053
1067
  const existingMark = marks.publicationReference;
1054
1068
  const existingWakeMark = marks.taskWake;
@@ -1061,7 +1075,7 @@ function normalizeStoredTaskV16ToV17(snapshot) {
1061
1075
  taskWake: typeof existingWakeMark === "number" ? existingWakeMark : TASK_WAKE_FROM_VERSION
1062
1076
  },
1063
1077
  publicationReferences: existingReferences,
1064
- wakes: {}
1078
+ wakes: existingWakes
1065
1079
  };
1066
1080
  }
1067
1081
  return {
@@ -33,12 +33,19 @@ export function projectCompletionReadiness(facts, options = {}) {
33
33
  : `wait for Reviewer Run on ${round.id} to finish`
34
34
  });
35
35
  }
36
- // Issue 07: a completed delta-recheck that did not accept the head keeps the
37
- // completion gate closed. `requires-full-review` escalates to a full Review;
38
- // `finding` stays a blocker for the Leader to repair.
39
- for (const round of facts.reviewRounds) {
40
- if (!deltaRecheckBlocksAcceptance(round))
41
- continue;
36
+ // Issue 07: only the latest completed Task-final Review can define whether
37
+ // the current review lineage is accepted. Historical delta findings and
38
+ // escalations remain audit evidence, but a later completed full Review (or
39
+ // accepted delta) supersedes them instead of blocking completion forever.
40
+ const latestCompletedTaskReview = facts.reviewRounds
41
+ .filter((round) => ((round.scope ?? "work-item") === "task" && round.status === "completed"))
42
+ .slice()
43
+ .sort((left, right) => (left.createdAt.localeCompare(right.createdAt)
44
+ || left.id.localeCompare(right.id, undefined, { numeric: true })))
45
+ .at(-1);
46
+ if (latestCompletedTaskReview !== undefined
47
+ && deltaRecheckBlocksAcceptance(latestCompletedTaskReview)) {
48
+ const round = latestCompletedTaskReview;
42
49
  const disposition = round.deltaRecheck.disposition;
43
50
  blockers.push({
44
51
  code: "delta-recheck-not-accepted",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zq-silk/yui",
3
- "version": "0.6.14",
3
+ "version": "0.6.16",
4
4
  "description": "Local control plane for long-running native agent CLI sessions backed by tmux.",
5
5
  "license": "MIT",
6
6
  "private": false,
@@ -142,6 +142,7 @@ yui project knowledge show <project> <knowledge-id>
142
142
  yui project knowledge proposals list <project>
143
143
  yui project knowledge proposals show <project> <proposal-id>
144
144
  yui project knowledge accept <project> <proposal-id>
145
+ yui project knowledge accept <project> <proposal-id> --update <knowledge-id>
145
146
  yui project knowledge reject <project> <proposal-id> --reason "<text>"
146
147
  yui task create "<title>" \
147
148
  --project <project-a> --project <project-b> \
@@ -155,8 +156,11 @@ Task/Decision/Milestone evidence) and the Operator reviews and accepts or
155
156
  rejects them. Acceptance writes the Knowledge entry with its provenance; a
156
157
  candidate that duplicates an existing entry is deduplicated, and one that
157
158
  conflicts with an existing title fails closed so the Operator must choose an
158
- explicit `--update`, supersede, or reject. Update current Knowledge and retire
159
- obsolete Knowledge without deleting its history. If discovery finds an
159
+ explicit proposal-backed `accept --update`, supersede, or reject. Direct
160
+ Knowledge mutation is not supported because it would erase version history;
161
+ `--update` is allowed only when the current version is already traceable to an
162
+ accepted proposal. Otherwise submit the replacement with `--supersedes` so the
163
+ old Knowledge entry remains retired and readable. If discovery finds an
160
164
  existing stable checkout, bind it with `project add`. If only a remote is
161
165
  known, explain the clone destination and impact, obtain confirmation, then run
162
166
  `project clone`; do not send the user mechanical clone steps.