@pmelab/gtd 49.0.0 → 50.0.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.
Files changed (32) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +31 -26
  3. package/dist/chunks/{reserved-CYZ1bSBy.mjs → reserved-CdOzKFVX.mjs} +59 -2
  4. package/dist/chunks/{review-BewyJflS.mjs → review-BChXEl2Y.mjs} +84 -20
  5. package/dist/chunks/{steering-BXwjmocB.mjs → steering-DtUVUz9l.mjs} +44 -14
  6. package/dist/chunks/{tail-C1Oi2YEy.mjs → tail-DRM2eC1z.mjs} +1 -1
  7. package/dist/gtd.bundle.mjs +22 -5
  8. package/dist/steering.mjs +2 -2
  9. package/dist/testkit.mjs +1 -1
  10. package/dist/web/main.js +27 -3
  11. package/dist/workflow-kit.mjs +1 -1
  12. package/dist/workflows/cleanup.mjs +2 -2
  13. package/dist/workflows/feature.mjs +3 -3
  14. package/dist/workflows/fix.mjs +3 -3
  15. package/dist/workflows/review.mjs +3 -3
  16. package/dist/workflows/rollout.mjs +1 -1
  17. package/dist/workflows.mjs +181 -30
  18. package/package.json +1 -1
  19. package/src/steering/index.ts +1 -0
  20. package/src/steering/review.test.ts +60 -2
  21. package/src/steering/review.ts +66 -9
  22. package/src/workflows/feature/lens-review.feature +16 -4
  23. package/src/workflows/feature/review-answers.feature +8 -1
  24. package/src/workflows/feature/review-gate.feature +20 -3
  25. package/src/workflows/feature/reviewer-walkthrough.feature +14 -3
  26. package/src/workflows/feature/severity-budget.feature +111 -0
  27. package/src/workflows/feature/severity-sorted-review.feature +197 -0
  28. package/src/workflows/feature/slice-findings.feature +14 -2
  29. package/src/workflows/feature/standards.feature +4 -0
  30. package/src/workflows/shared/review.ts +124 -18
  31. package/src/workflows/shared/text.test.ts +90 -0
  32. package/src/workflows/shared/text.ts +77 -1
package/dist/steering.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import { A as formatFinding, C as openThreadFindings, D as getParseCount, E as parseThreadsWithFindings, O as headingSectionBodies, S as THREAD_REPLY_ACTION_TITLE, T as parseThreads, _ as reviewRisks, a as unansweredQuestions, b as isAnswered, c as stripCodeThreads, d as ANSWER_FIX, f as ANSWER_LEAD, g as reviewNotes, h as reviewDecisions, i as steeringFormatOrFreeForm, k as headingSections, l as freeFormFormat, m as reviewChunks, n as openThreads, o as viewOf, p as ANSWER_WONT_FIX, r as steeringFormatFor, s as parseCodeThreads, t as BUILT_IN_MODE_NAMES, u as applySteeringEdits, v as unansweredNotes, w as parseFootnotes, x as FOOTNOTE_ACTION_TITLE, y as FREE_TEXT_PLACEHOLDER } from "./chunks/steering-BXwjmocB.mjs";
2
- export { ANSWER_FIX, ANSWER_LEAD, ANSWER_WONT_FIX, BUILT_IN_MODE_NAMES, FOOTNOTE_ACTION_TITLE, FREE_TEXT_PLACEHOLDER, THREAD_REPLY_ACTION_TITLE, applySteeringEdits, formatFinding, freeFormFormat, getParseCount, headingSectionBodies, headingSections, isAnswered, openThreadFindings, openThreads, parseCodeThreads, parseFootnotes, parseThreads, parseThreadsWithFindings, reviewChunks, reviewDecisions, reviewNotes, reviewRisks, steeringFormatFor, steeringFormatOrFreeForm, stripCodeThreads, unansweredNotes, unansweredQuestions, viewOf };
1
+ import { A as headingSections, C as THREAD_REPLY_ACTION_TITLE, D as parseThreadsWithFindings, E as parseThreads, O as getParseCount, S as FOOTNOTE_ACTION_TITLE, T as parseFootnotes, _ as reviewRisks, a as unansweredQuestions, b as FREE_TEXT_PLACEHOLDER, c as stripCodeThreads, d as ANSWER_FIX, f as ANSWER_LEAD, g as reviewNotes, h as reviewDecisions, i as steeringFormatOrFreeForm, j as formatFinding, k as headingSectionBodies, l as freeFormFormat, m as reviewChunks, n as openThreads, o as viewOf, p as ANSWER_WONT_FIX, r as steeringFormatFor, s as parseCodeThreads, t as BUILT_IN_MODE_NAMES, u as applySteeringEdits, v as severityLine, w as openThreadFindings, x as isAnswered, y as unansweredNotes } from "./chunks/steering-DtUVUz9l.mjs";
2
+ export { ANSWER_FIX, ANSWER_LEAD, ANSWER_WONT_FIX, BUILT_IN_MODE_NAMES, FOOTNOTE_ACTION_TITLE, FREE_TEXT_PLACEHOLDER, THREAD_REPLY_ACTION_TITLE, applySteeringEdits, formatFinding, freeFormFormat, getParseCount, headingSectionBodies, headingSections, isAnswered, openThreadFindings, openThreads, parseCodeThreads, parseFootnotes, parseThreads, parseThreadsWithFindings, reviewChunks, reviewDecisions, reviewNotes, reviewRisks, severityLine, steeringFormatFor, steeringFormatOrFreeForm, stripCodeThreads, unansweredNotes, unansweredQuestions, viewOf };
package/dist/testkit.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import { answered, applyScript, checkScript, configProblems, configure, installContext, isFactoryWorkflow, moveScript, numeric, protocolMismatch, removeScript, restoreScript, revertScript, showScript, t as flows_exports, writeScript } from "./flows.mjs";
2
2
  import { A as beatKindOf, B as ACCESS_REFUSAL, C as renderLandPlain, D as renderBeatJson, E as beatDocument, O as statusOf, S as renderLandJson, T as renderBeatPlain, U as renderFormat, _ as selectPath, a as contextLimitFault, g as resolveSession, h as globMatches, j as demandOf, l as parseContextWindow, m as slicesDocument, o as contextShapeFault, p as renderSlicesJson, v as judgeJson, x as noopText, y as landFields } from "./chunks/wire-BJuZWR3C.mjs";
3
- import { A as formatFinding, E as parseThreadsWithFindings, O as headingSectionBodies, a as unansweredQuestions, k as headingSections, n as openThreads, r as steeringFormatFor, s as parseCodeThreads, t as BUILT_IN_MODE_NAMES } from "./chunks/steering-BXwjmocB.mjs";
3
+ import { A as headingSections, D as parseThreadsWithFindings, a as unansweredQuestions, j as formatFinding, k as headingSectionBodies, n as openThreads, r as steeringFormatFor, s as parseCodeThreads, t as BUILT_IN_MODE_NAMES } from "./chunks/steering-DtUVUz9l.mjs";
4
4
  import { $ as logWarning, $i as BaseProto, $n as map$5, $r as make$19, $t as provide$1, A as catchIf, Ai as add$1, An as scopeMake, Ar as flatMap$3, At as suspend$5, B as flatMap$4, Bi as getOrElse, Bn as get$3, Br as sync$1, Bt as unsafeMakeLatch, C as all$1, Ci as toArray, Cn as isInterrupted, Cr as exitFail, Ct as sandbox, D as async, Di as GenericTag, Dn as squash, Dr as exitZipRight, Dt as scopedWith$1, E as asVoid$1, Ei as unsafeHead, En as reduceWithContext, Er as exitVoid, Et as scoped$4, F as ensuring$2, Fi as fromIterable, Fn as join$3, Fr as map$3, Ft as tryMap, G as forkIn, Gi as match$4, Gn as TypeId$10, Gr as whenEffect, Gt as zipLeft, H as fnUntraced, Hi as isNone, Hn as modify$1, Hr as uninterruptible, Ht as when, I as exit, Ii as splitAt, In as sequential, Ir as onInterrupt, It as tryPromise, J as ignore, Ji as isLeft, Jn as all, Jr as DeferredTypeId$1, Jt as spaced$1, K as forkScoped, Ki as none, Kn as Class, Kr as withFiberRuntime, Kt as zipRight$3, L as fail$5, Li as filter$1, Ln as bind, Lr as scopeClose, Lt as try_, M as die$2, Mi as getOption, Mn as _await$2, Mr as interruptAsFiber, Mt as tap, N as dieMessage, Ni as omit, Nn as inheritAll$1, Nr as interruptFiber, Nt as tapErrorCause, O as catchAll$1, Oi as Reference, On as ensuring$3, Or as fiberId, Ot as serviceOption, P as either, Pi as empty$5, Pn as interruptAllAs$1, Pr as interruptible$1, Pt as timeout, Q as isEffect, Qi as pipeArguments, Qn as isFailure, Qr as ids, Qt as mergeAll$2, R as failCause$4, Ri as flatMap$5, Rn as bindTo, Rr as succeed$4, Rt as uninterruptible$1, S as addFinalizer$1, Si as takeRight, Sn as isDieType, Sr as exitAs, St as runtime, T as as, Ti as unsafeGet, Tn as map$6, Tr as exitSucceed, Tt as scopeWith, U as forEach$2, Ui as isSome, Un as set$1, Ur as uninterruptibleMask$1, Ut as withFiberRuntime$1, V as flatten$4, Vi as getOrUndefined, Vn as make$16, Vr as tap$1, Vt as unsafeMakeSemaphore$1, W as forkDaemon, Wi as map$8, Wn as update$1, Wr as void_$2, Wt as zip$1, X as interruptible, Xi as match$2, Xn as fail$4, Xr as combine, Xt as tagged, Y as interrupt$4, Yi as left, Yn as die, Yr as empty$6, Yt as TaggedError$1, Z as intoDeferred, Zi as right, Zn as failCause$3, Zr as combineAll, Zt as fromEffect$5, _ as decodeSync, _i as of, _n as fail$3, _r as deferredIsDone, _t as raceWith, a as Literal, aa as constFalse, ai as size$2, an as updateContext$1, ar as zipRight$2, at as matchCause, b as acquireRelease, bi as reverse, bn as interrupt$3, br as deferredUnsafeDone, bt as runPromise, c as String$, ca as constVoid, ci as append$1, cn as Scope, cr as asyncInterrupt, ct as negate, d as Union, da as pipe, di as empty$4, dn as close, dr as deferredAwait, dt as orElse, ea as NodeInspectSymbol, ei as none$1, en as provideMerge$1, er as mapBoth, et as makeSemaphore, f as between, fi as filter, fn as extend, fr as deferredComplete, ft as orElseSucceed, gi as map$7, gn as die$1, gr as deferredInterruptWith, gt as race, hi as isEmpty$2, hn as NoSuchElementException, hr as deferredInterrupt, ht as provideService, i as Defect, ia as hasProperty, ii as has, in as unsafeRunPromiseExit, ir as zip$2, it as match$5, j as catchTag, ji as get$5, jn as scopeTag, jr as interrupt$2, jt as sync$2, k as catchAllCause, ki as Tag, kn as forEachConcurrentDiscard, kr as fiberRefLocally, kt as succeed$6, l as Struct, la as dual, li as appendAll, ln as addFinalizer, lr as catchAll, lt as onError, m as optional$1, mi as isChunk, mn as make$18, mr as deferredFailCause, mt as provide$2, n as Boolean$, na as toJSON, ni as make$17, nn as succeed$3, nr as succeed$5, nt as mapBoth$1, o as Never, oa as constTrue, oi as decode, on as forkIn$1, or as as$1, ot as matchCauseEffect, p as decodeUnknown, pi as fromIterable$1, pn as fork, pr as deferredFail, pt as promise, q as gen, qi as some, qn as spanTag, qr as zipRight$1, qt as zipWith, r as Cause, ra as equals, ri as set$2, rn as unsafeFork, rr as void_$3, rt as mapError, s as Number$, sa as constUndefined, si as isFinite, sn as unsafeMakeSemaphore, sr as asVoid, st as matchEffect, t as Array$, ta as format, ti as get$4, tn as scoped$3, tr as match$3, tt as map$4, u as TaggedError, ua as identity, ui as drop, un as addFinalizerExit, ur as currentContext$1, ut as orDie, v as encodeSync, vi as prepend, vn as failureOrCause, vr as deferredMake, vt as repeat, w as annotateLogs, wi as unsafeFromArray, wn as isInterruptedOnly, wr as exitFailCause, wt as scope, x as acquireUseRelease$2, xi as take$2, xn as interruptors, xr as deferredUnsafeMake, xt as runPromiseExit$1, y as _void, yi as reduce, yn as flipCauseOption, yr as deferredSucceed, yt as retry, z as fiberIdWith, zi as fromNullable, zn as sleep, zr as suspend$4, zt as uninterruptibleMask } from "./chunks/Schema-C6fLlQzx.mjs";
5
5
  import { createRequire } from "node:module";
6
6
  import * as Crypto from "node:crypto";
package/dist/web/main.js CHANGED
@@ -31565,6 +31565,8 @@ const qaDescriptor = {
31565
31565
  unansweredQuestions,
31566
31566
  openThreads: openThreadsOf
31567
31567
  };
31568
+ /** A chunk's leading `_critical: security_` paragraph: the tier gtd ranked it with, and why. */
31569
+ const SEVERITY_LINE_RE = /^_((?:critical|should|could)(?::[^_\n]*)?)_$/;
31568
31570
  /**
31569
31571
  * The canonical sample. Every detail here is load-bearing for the formatter
31570
31572
  * round-trip: both footnote bodies exceed 80 characters so a reflow actually
@@ -31855,12 +31857,26 @@ const answerListOf = (content, body) => {
31855
31857
  })
31856
31858
  };
31857
31859
  };
31860
+ /** A leading severity line is the chunk's tier, never its prose. */
31861
+ const splitSeverity = (content, run) => {
31862
+ const first = run[0];
31863
+ const label = first?.type === "paragraph" ? SEVERITY_LINE_RE.exec(sourceText(content, first).trim())?.[1] : void 0;
31864
+ if (label === void 0) return { leadingRun: run };
31865
+ return {
31866
+ severity: {
31867
+ label,
31868
+ line: toLspPosition(first.position.start).line
31869
+ },
31870
+ leadingRun: run.slice(1)
31871
+ };
31872
+ };
31858
31873
  /** Splits one chunk's body nodes into its file pointers (hunk pointers are task items, collected recursively at ANY nesting depth via `taskItems` — a nested hunk is the same kind of hunk as a top-level one) and description prose. The description is the chunk's own leading run and never a node that contains a hunk pointer: it stops at the first node whose `taskItems` include a real pointer (at any depth — a blockquote wrapping a pointer list stops it just as a top-level list would). Every node kind in what's left before that point contributes — a `footnoteDefinition` is the one exception, excluded by `blockNodesOfRun` itself (it's a note, never document content). */
31859
31874
  const parseChunkBody = (content, lines, title, body) => {
31860
31875
  const answer = answerListOf(content, body);
31861
31876
  const isAnswer = (n) => answer !== void 0 && (n === answer.list || n === body[answer.lead]);
31862
31877
  const firstPointerIndex = body.findIndex((n) => !isAnswer(n) && taskItems(n).some((item) => hasPointerToken(content, item)));
31863
- const leadingRun = (firstPointerIndex === -1 ? body : body.slice(0, firstPointerIndex)).filter((n) => !isAnswer(n));
31878
+ const run = (firstPointerIndex === -1 ? body : body.slice(0, firstPointerIndex)).filter((n) => !isAnswer(n));
31879
+ const { severity, leadingRun } = splitSeverity(content, run);
31864
31880
  const descriptionNodes = blockNodesOfRun(content, leadingRun);
31865
31881
  const descriptionInline = collapseImages(blockRunInline(content, leadingRun));
31866
31882
  const description = leadingRun.filter((n) => n.type !== "footnoteDefinition").map((n) => sourceText(content, n)).join(" ").trim();
@@ -31879,6 +31895,7 @@ const parseChunkBody = (content, lines, title, body) => {
31879
31895
  files,
31880
31896
  errors,
31881
31897
  ...answer === void 0 ? {} : { answers: answer.options },
31898
+ ...severity === void 0 ? {} : { severity },
31882
31899
  boxed: body.some((n) => !isAnswer(n) && taskItems(n).length > 0)
31883
31900
  };
31884
31901
  };
@@ -31915,7 +31932,7 @@ const parseChangesets = (tree, content, lines) => {
31915
31932
  const errors = [];
31916
31933
  for (const { heading, headingLine, body } of splitChunks(tree)) {
31917
31934
  const title = headingText$1(content, heading);
31918
- const { description, descriptionNodes, descriptionInline, files, answers, boxed, errors: bodyErrors } = parseChunkBody(content, lines, title, body);
31935
+ const { description, descriptionNodes, descriptionInline, files, answers, severity, boxed, errors: bodyErrors } = parseChunkBody(content, lines, title, body);
31919
31936
  errors.push(...bodyErrors);
31920
31937
  if (files.length === 0 && boxed) errors.push({
31921
31938
  message: `Chunk "${title}" has no file pointers`,
@@ -31929,7 +31946,8 @@ const parseChangesets = (tree, content, lines) => {
31929
31946
  descriptionInline,
31930
31947
  files,
31931
31948
  headingLine,
31932
- ...answers === void 0 ? {} : { answers }
31949
+ ...answers === void 0 ? {} : { answers },
31950
+ ...severity === void 0 ? {} : { severity }
31933
31951
  });
31934
31952
  }
31935
31953
  if (changesets.length === 0) errors.push({ message: "REVIEW.md has no '##' chunks" });
@@ -32230,6 +32248,7 @@ const reviewView = (content) => {
32230
32248
  detail: chunk.description,
32231
32249
  detailInline: chunk.descriptionInline,
32232
32250
  body: chunk.descriptionNodes,
32251
+ ...chunk.severity === void 0 ? {} : { status: chunk.severity.label },
32233
32252
  anchor: {
32234
32253
  kind: "chunk",
32235
32254
  index: chunkIndex
@@ -34779,6 +34798,11 @@ const ChunkRow = ({ chunk, chunkIndex, state }) => {
34779
34798
  disabled: hunks.length === 0,
34780
34799
  className: "flex-1 text-left",
34781
34800
  children: [
34801
+ chunk.status !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
34802
+ "data-testid": `chunk-severity-${chunkIndex}`,
34803
+ className: cn("text-small font-semibold", chunk.status.startsWith("critical") ? "text-warning" : "text-muted"),
34804
+ children: chunk.status
34805
+ }),
34782
34806
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
34783
34807
  className: "font-semibold",
34784
34808
  children: chunk.title
@@ -1,5 +1,5 @@
1
1
  import { changes, changesSince, hasThreadFor, head, mark, read, refuse, requireReplies, requireThreadsClosed, scope } from "./flows.mjs";
2
- import { T as parseThreads, _ as reviewRisks, c as stripCodeThreads, g as reviewNotes, h as reviewDecisions, v as unansweredNotes } from "./chunks/steering-BXwjmocB.mjs";
2
+ import { E as parseThreads, _ as reviewRisks, c as stripCodeThreads, g as reviewNotes, h as reviewDecisions, y as unansweredNotes } from "./chunks/steering-DtUVUz9l.mjs";
3
3
  //#region src/workflow-kit/health.ts
4
4
  /** Hand a red suite that repeated attempts could not fix to a person, until `limit` rounds are spent. */
5
5
  const escalation = async (count, steps) => {
@@ -1,6 +1,6 @@
1
1
  import { agent, changes, glob, read, refuse, scope, start, vars } from "../flows.mjs";
2
- import { Dt as envDefaults, Et as defaults, Ht as requireFlag, S as coder, Vt as registryEntry, i as skills, n as summary, o as flagShipped, r as access, t as steering, zt as flagReferences } from "../chunks/reserved-CYZ1bSBy.mjs";
3
- import { a as refusePendingHandOff, r as handOffTail } from "../chunks/tail-C1Oi2YEy.mjs";
2
+ import { At as defaults, Gt as registryEntry, Kt as requireFlag, S as coder, Ut as flagReferences, i as skills, jt as envDefaults, n as summary, o as flagShipped, r as access, t as steering } from "../chunks/reserved-CdOzKFVX.mjs";
3
+ import { a as refusePendingHandOff, r as handOffTail } from "../chunks/tail-DRM2eC1z.mjs";
4
4
  //#region src/workflows/cleanup/cleanup.ts
5
5
  const removingPrompt = (name, registry) => `Clean up the release flag "${name}", which is default-on: delete its entry in ${registry}, and remove every \`GTD_FLAG_${name.toUpperCase()}\` check from the shipped sources. Keep the flag-on path; delete code only the off path reached, including tests that pin the off path.`;
6
6
  const cleanupFlow = async () => {
@@ -1,8 +1,8 @@
1
1
  import { agent, answered, applyScript, changes, changesSince, check, codeChanges, env, hasThreadFor, head, human, judge, judgeDiffShape, kept, mark, matches, quote, read, refuse, removeScript, requireAnswers, requireProgress, requireReplies, requireThreadsClosed, revertScript, run, scope, slice, start, vars, wrote } from "../flows.mjs";
2
2
  import { lensEntriesOf } from "../workflow-kit.mjs";
3
- import { $ as naivePassPrompt, B as coverageQuestion, C as collecting, Dt as envDefaults, Et as defaults, F as builderSystem, G as designTriagePrompt, H as criterionLine, I as clearCriteriaMessage, L as clearCriteriaQuestion, M as agentWithSkills, N as buildReviewCollectingPrompt, O as planner, Ot as testFileGlobs, Q as naiveApplyFailure, S as coder, St as unwindFailure, T as fixNits, U as designGateAnswerMessage, V as coveringPrompt, W as designSystem, X as implementBuildingPrompt, Y as idleMessage, Z as implementFixPrompt, _ as REQUIREMENTS, _t as slicingPrompt, a as flagModeOn, at as refactorDroppedRefusal, b as answerReviewQuestions, bt as uncoveredMessage, ct as refactorRules, d as healthy, et as normaliseCriteriaScript, f as FEEDBACK, g as REFACTOR_NOTE, gt as seamSteerMessage, h as REFACTOR, ht as seamByHandMessage, i as skills, it as refactorBuildingPrompt, l as gate, mt as reviewerSystem, n as summary, ot as refactorFixPrompt, p as HANDOFF, r as access, s as shipped, st as refactorNotePrompt, t as steering, tt as oversizedMessage, vt as startGateBlockedMessage, xt as unwindFailedMessage, y as SLICES, z as coverageMessage } from "../chunks/reserved-CYZ1bSBy.mjs";
4
- import { a as refusePendingHandOff, c as joinReports, o as tail } from "../chunks/tail-C1Oi2YEy.mjs";
5
- import { i as reUnwind, n as humanReview, r as isCode$1, t as buildTail } from "../chunks/review-BewyJflS.mjs";
3
+ import { $ as naiveApplyFailure, At as defaults, B as coverageMessage, C as collecting, Ct as uncoveredMessage, Et as unwindFailure, F as builderSystem, G as designSystem, H as coveringPrompt, K as designTriagePrompt, L as clearCriteriaMessage, M as agentWithSkills, Mt as testFileGlobs, N as buildReviewCollectingPrompt, O as planner, Q as implementFixPrompt, R as clearCriteriaQuestion, S as coder, T as fixNits, Tt as unwindFailedMessage, U as criterionLine, V as coverageQuestion, W as designGateAnswerMessage, X as idleMessage, Z as implementBuildingPrompt, _ as REQUIREMENTS, _t as reviewerSystem, a as flagModeOn, b as answerReviewQuestions, bt as slicingPrompt, ct as refactorFixPrompt, d as healthy, et as naivePassPrompt, f as FEEDBACK, g as REFACTOR_NOTE, h as REFACTOR, i as skills, jt as envDefaults, l as gate, lt as refactorNotePrompt, n as summary, nt as oversizedMessage, ot as refactorBuildingPrompt, p as HANDOFF, r as access, s as shipped, st as refactorDroppedRefusal, t as steering, tt as normaliseCriteriaScript, ut as refactorRules, vt as seamByHandMessage, xt as startGateBlockedMessage, y as SLICES, yt as seamSteerMessage } from "../chunks/reserved-CdOzKFVX.mjs";
4
+ import { a as refusePendingHandOff, c as joinReports, o as tail } from "../chunks/tail-DRM2eC1z.mjs";
5
+ import { i as reUnwind, n as humanReview, r as isCode$1, t as buildTail } from "../chunks/review-BChXEl2Y.mjs";
6
6
  import { posix } from "node:path";
7
7
  //#region src/workflows/shared/fences.ts
8
8
  const markFenced = (text) => {
@@ -1,7 +1,7 @@
1
1
  import { read, refuse, removeScript, run, scope, start } from "../flows.mjs";
2
- import { _ as reviewRisks, g as reviewNotes } from "../chunks/steering-BXwjmocB.mjs";
3
- import { Dt as envDefaults, Et as defaults, K as finisherSystem, M as agentWithSkills, S as coder, c as baseline, i as skills, n as summary, q as fixReviewNotesPrompt, r as access, s as shipped, t as steering, v as REVIEW } from "../chunks/reserved-CYZ1bSBy.mjs";
4
- import { r as handOffTail, t as REVIEW_ORIG } from "../chunks/tail-C1Oi2YEy.mjs";
2
+ import { _ as reviewRisks, g as reviewNotes } from "../chunks/steering-DtUVUz9l.mjs";
3
+ import { At as defaults, J as fixReviewNotesPrompt, M as agentWithSkills, S as coder, c as baseline, i as skills, jt as envDefaults, n as summary, q as finisherSystem, r as access, s as shipped, t as steering, v as REVIEW } from "../chunks/reserved-CdOzKFVX.mjs";
4
+ import { r as handOffTail, t as REVIEW_ORIG } from "../chunks/tail-DRM2eC1z.mjs";
5
5
  //#region src/workflows/fix/fix.ts
6
6
  /**
7
7
  * Fix every note a human left in a handed-off `.gtd/REVIEW.md` and every
@@ -1,7 +1,7 @@
1
1
  import { read, refuse, removeScript, run, start } from "../flows.mjs";
2
- import { Dt as envDefaults, Et as defaults, K as finisherSystem, M as agentWithSkills, S as coder, _ as REQUIREMENTS, dt as reviewFeedbackFixPrompt, ft as reviewGateBlockedMessage, i as skills, l as gate, n as summary, r as access, s as shipped, t as steering } from "../chunks/reserved-CYZ1bSBy.mjs";
3
- import { a as refusePendingHandOff } from "../chunks/tail-C1Oi2YEy.mjs";
4
- import { i as reUnwind, t as buildTail } from "../chunks/review-BewyJflS.mjs";
2
+ import { At as defaults, M as agentWithSkills, S as coder, _ as REQUIREMENTS, i as skills, jt as envDefaults, l as gate, mt as reviewGateBlockedMessage, n as summary, pt as reviewFeedbackFixPrompt, q as finisherSystem, r as access, s as shipped, t as steering } from "../chunks/reserved-CdOzKFVX.mjs";
3
+ import { a as refusePendingHandOff } from "../chunks/tail-DRM2eC1z.mjs";
4
+ import { i as reUnwind, t as buildTail } from "../chunks/review-BChXEl2Y.mjs";
5
5
  //#region src/workflows/review/review.ts
6
6
  const fixFeedback = async () => {
7
7
  await agentWithSkills("feedback.fixing", reviewFeedbackFixPrompt(), {
@@ -1,5 +1,5 @@
1
1
  import { agent, changesSince, glob, head, mark, read, refuse, removeScript, run, scope, vars } from "../flows.mjs";
2
- import { Bt as flipProblems, Dt as envDefaults, Et as defaults, Ht as requireFlag, Rt as flagMentions, S as coder, f as FEEDBACK, i as skills, l as gate, n as summary, o as flagShipped, r as access, t as steering } from "../chunks/reserved-CYZ1bSBy.mjs";
2
+ import { At as defaults, Ht as flagMentions, Kt as requireFlag, S as coder, Wt as flipProblems, f as FEEDBACK, i as skills, jt as envDefaults, l as gate, n as summary, o as flagShipped, r as access, t as steering } from "../chunks/reserved-CdOzKFVX.mjs";
3
3
  //#region src/workflows/rollout/rollout.ts
4
4
  const flippingPrompt = (name, registry) => `Roll out the release flag "${name}": in ${registry}, set \`defaultOn: true\` on its entry. Change nothing else — no other entry, no other field, no other file, no comments.`;
5
5
  const suiteBlockedMessage = () => "The suite is red after the rollout. Fix it by hand — a rollout may only touch the flag's registry entry — then land.";
@@ -15648,6 +15648,9 @@ const qaDescriptor = {
15648
15648
  const ANSWER_LEAD = "Tick exactly one:";
15649
15649
  const ANSWER_FIX = "fix as suggested";
15650
15650
  const ANSWER_WONT_FIX = "won't fix";
15651
+ /** A chunk's leading `_critical: security_` paragraph: the tier gtd ranked it with, and why. */
15652
+ const SEVERITY_LINE_RE = /^_((?:critical|should|could)(?::[^_\n]*)?)_$/;
15653
+ const severityLine = (label) => `_${label}_`;
15651
15654
  /**
15652
15655
  * The canonical sample. Every detail here is load-bearing for the formatter
15653
15656
  * round-trip: both footnote bodies exceed 80 characters so a reflow actually
@@ -15938,12 +15941,26 @@ const answerListOf = (content, body) => {
15938
15941
  })
15939
15942
  };
15940
15943
  };
15944
+ /** A leading severity line is the chunk's tier, never its prose. */
15945
+ const splitSeverity = (content, run) => {
15946
+ const first = run[0];
15947
+ const label = first?.type === "paragraph" ? SEVERITY_LINE_RE.exec(sourceText(content, first).trim())?.[1] : void 0;
15948
+ if (label === void 0) return { leadingRun: run };
15949
+ return {
15950
+ severity: {
15951
+ label,
15952
+ line: toLspPosition(first.position.start).line
15953
+ },
15954
+ leadingRun: run.slice(1)
15955
+ };
15956
+ };
15941
15957
  /** Splits one chunk's body nodes into its file pointers (hunk pointers are task items, collected recursively at ANY nesting depth via `taskItems` — a nested hunk is the same kind of hunk as a top-level one) and description prose. The description is the chunk's own leading run and never a node that contains a hunk pointer: it stops at the first node whose `taskItems` include a real pointer (at any depth — a blockquote wrapping a pointer list stops it just as a top-level list would). Every node kind in what's left before that point contributes — a `footnoteDefinition` is the one exception, excluded by `blockNodesOfRun` itself (it's a note, never document content). */
15942
15958
  const parseChunkBody = (content, lines, title, body) => {
15943
15959
  const answer = answerListOf(content, body);
15944
15960
  const isAnswer = (n) => answer !== void 0 && (n === answer.list || n === body[answer.lead]);
15945
15961
  const firstPointerIndex = body.findIndex((n) => !isAnswer(n) && taskItems(n).some((item) => hasPointerToken(content, item)));
15946
- const leadingRun = (firstPointerIndex === -1 ? body : body.slice(0, firstPointerIndex)).filter((n) => !isAnswer(n));
15962
+ const run = (firstPointerIndex === -1 ? body : body.slice(0, firstPointerIndex)).filter((n) => !isAnswer(n));
15963
+ const { severity, leadingRun } = splitSeverity(content, run);
15947
15964
  const descriptionNodes = blockNodesOfRun(content, leadingRun);
15948
15965
  const descriptionInline = collapseImages(blockRunInline(content, leadingRun));
15949
15966
  const description = leadingRun.filter((n) => n.type !== "footnoteDefinition").map((n) => sourceText(content, n)).join(" ").trim();
@@ -15962,6 +15979,7 @@ const parseChunkBody = (content, lines, title, body) => {
15962
15979
  files,
15963
15980
  errors,
15964
15981
  ...answer === void 0 ? {} : { answers: answer.options },
15982
+ ...severity === void 0 ? {} : { severity },
15965
15983
  boxed: body.some((n) => !isAnswer(n) && taskItems(n).length > 0)
15966
15984
  };
15967
15985
  };
@@ -15998,7 +16016,7 @@ const parseChangesets = (tree, content, lines) => {
15998
16016
  const errors = [];
15999
16017
  for (const { heading, headingLine, body } of splitChunks(tree)) {
16000
16018
  const title = headingText$1(content, heading);
16001
- const { description, descriptionNodes, descriptionInline, files, answers, boxed, errors: bodyErrors } = parseChunkBody(content, lines, title, body);
16019
+ const { description, descriptionNodes, descriptionInline, files, answers, severity, boxed, errors: bodyErrors } = parseChunkBody(content, lines, title, body);
16002
16020
  errors.push(...bodyErrors);
16003
16021
  if (files.length === 0 && boxed) errors.push({
16004
16022
  message: `Chunk "${title}" has no file pointers`,
@@ -16012,7 +16030,8 @@ const parseChangesets = (tree, content, lines) => {
16012
16030
  descriptionInline,
16013
16031
  files,
16014
16032
  headingLine,
16015
- ...answers === void 0 ? {} : { answers }
16033
+ ...answers === void 0 ? {} : { answers },
16034
+ ...severity === void 0 ? {} : { severity }
16016
16035
  });
16017
16036
  }
16018
16037
  if (changesets.length === 0) errors.push({ message: "REVIEW.md has no '##' chunks" });
@@ -16313,6 +16332,7 @@ const reviewView = (content) => {
16313
16332
  detail: chunk.description,
16314
16333
  detailInline: chunk.descriptionInline,
16315
16334
  body: chunk.descriptionNodes,
16335
+ ...chunk.severity === void 0 ? {} : { status: chunk.severity.label },
16316
16336
  anchor: {
16317
16337
  kind: "chunk",
16318
16338
  index: chunkIndex
@@ -16460,15 +16480,19 @@ const reviewDescriptor = {
16460
16480
  openThreads: openThreadsOf
16461
16481
  };
16462
16482
  const pointerLabel = (f) => `${f.path}${f.line === void 0 ? "" : `#${f.line}${f.rangeEnd === void 0 ? "" : `-${f.rangeEnd}`}`}`;
16463
- const chunkNote = (chunk, prior) => chunk.description === (prior?.description ?? "") || prior === void 0 && chunk.answers !== void 0 ? [] : [{
16464
- line: chunk.headingLine,
16465
- note: {
16466
- kind: "chunk",
16467
- anchor: chunk.title,
16468
- before: prior?.description ?? "",
16469
- text: chunk.description
16470
- }
16471
- }];
16483
+ const chunkProse = (chunk, retiered) => [chunk?.description ?? "", retiered && chunk?.severity !== void 0 ? severityLine(chunk.severity.label) : ""].filter((p) => p !== "").join("\n\n");
16484
+ const chunkNote = (chunk, prior) => {
16485
+ const retiered = chunk.severity !== void 0 && chunk.severity.label !== prior?.severity?.label;
16486
+ return chunkProse(chunk, retiered) === chunkProse(prior, retiered) || prior === void 0 && chunk.answers !== void 0 ? [] : [{
16487
+ line: chunk.headingLine,
16488
+ note: {
16489
+ kind: "chunk",
16490
+ anchor: chunk.title,
16491
+ before: chunkProse(prior, retiered),
16492
+ text: chunkProse(chunk, retiered)
16493
+ }
16494
+ }];
16495
+ };
16472
16496
  const pointerNotes = (chunk, prior) => chunk.files.flatMap((file) => {
16473
16497
  const label = pointerLabel(file);
16474
16498
  const was = prior?.files.find((f) => pointerLabel(f) === label);
@@ -16582,7 +16606,13 @@ const reviewDecisions = (before, after) => {
16582
16606
  const reviewChunks = (content) => parseReviewDoc(content).changesets.map((c) => ({
16583
16607
  title: c.title,
16584
16608
  line: c.headingLine,
16585
- pointers: c.files.length
16609
+ pointers: c.files.length,
16610
+ ...c.severity === void 0 ? {} : { severityLine: c.severity.line },
16611
+ files: c.files.map((f) => ({
16612
+ path: f.path,
16613
+ ...f.line === void 0 ? {} : { line: f.line },
16614
+ ...f.rangeEnd === void 0 ? {} : { rangeEnd: f.rangeEnd }
16615
+ }))
16586
16616
  }));
16587
16617
  //#endregion
16588
16618
  //#region src/steering/CodeThreads.ts
@@ -17609,6 +17639,56 @@ const withWalkthrough = (review, report) => {
17609
17639
  const body = lines.join("\n").trimEnd();
17610
17640
  return summary.length === 0 ? `${body}\n` : `${body}\n\n${summary.join("\n\n")}\n`;
17611
17641
  };
17642
+ const chunkBlocks = (review) => {
17643
+ const lines = review.split("\n");
17644
+ const chunks = reviewChunks(review);
17645
+ return {
17646
+ head: lines.slice(0, chunks[0]?.line ?? lines.length),
17647
+ blocks: chunks.map((c, i) => ({
17648
+ title: c.title,
17649
+ lines: lines.slice(c.line, chunks[i + 1]?.line ?? lines.length)
17650
+ }))
17651
+ };
17652
+ };
17653
+ const isBlank = (line) => line !== void 0 && line.trim() === "";
17654
+ /** \`review\` with every chunk's severity line removed, so a re-review is ranked afresh. */
17655
+ const unranked = (review) => {
17656
+ const drop = new Set(reviewChunks(review).flatMap((c) => c.severityLine ?? []));
17657
+ if (drop.size === 0) return review;
17658
+ const lines = review.split("\n");
17659
+ return lines.filter((_, i) => !drop.has(i) && !(drop.has(i - 1) && isBlank(lines[i]))).join("\n");
17660
+ };
17661
+ const chunkTexts = (review) => new Map(chunkBlocks(review).blocks.map((b) => [b.title, b.lines.join("\n").trim()]));
17662
+ const trimBlankLines = (lines) => {
17663
+ let start = 0;
17664
+ let end = lines.length;
17665
+ while (start < end && isBlank(lines[start])) start++;
17666
+ while (end > start && isBlank(lines[end - 1])) end--;
17667
+ return lines.slice(start, end);
17668
+ };
17669
+ /**
17670
+ * \`review\` with the \`ranked\` chunks first, in their order, each led by its
17671
+ * severity line; every other chunk follows in place.
17672
+ */
17673
+ const rankedReview = (review, ranked) => {
17674
+ const { head, blocks } = chunkBlocks(review);
17675
+ const place = new Map(ranked.map((r, k) => [r.title, k]));
17676
+ const labels = new Map(ranked.map((r) => [r.title, r.label]));
17677
+ const order = (i) => place.get(blocks[i].title) ?? ranked.length;
17678
+ const chunk = (i) => {
17679
+ const { title, lines } = blocks[i];
17680
+ const label = labels.get(title);
17681
+ const heading = /^ {0,3}-+\s*$/.test(lines[1] ?? "") ? 2 : 1;
17682
+ const body = trimBlankLines(lines.slice(heading)).join("\n");
17683
+ return [
17684
+ lines.slice(0, heading).join("\n"),
17685
+ ...label === void 0 ? [] : [severityLine(label)],
17686
+ ...body === "" ? [] : [body]
17687
+ ].join("\n\n");
17688
+ };
17689
+ const sorted = blocks.map((_, i) => i).sort((x, y) => order(x) - order(y) || x - y);
17690
+ return `${[head.join("\n").trim(), ...sorted.map(chunk)].filter((p) => p !== "").join("\n\n")}\n`;
17691
+ };
17612
17692
  /**
17613
17693
  * \`.gtd/REVIEW.md\` as gtd writes it: one chunk per note, each ending in its
17614
17694
  * answer boxes, then the fixed and noise lists. Chunk titles are made unique —
@@ -18163,6 +18243,13 @@ const wrapItem = (item) => {
18163
18243
  } else line = line === "" ? word : `${line} ${word}`;
18164
18244
  return [...out, line];
18165
18245
  };
18246
+ const reviewRankingMessage = () => `Ranking each chunk of the review before the human reads it. Run
18247
+ \`gtd judge answer\` and pipe, per chunk \`C<n>\`: \`C<n>.security\`
18248
+ (yes/no), \`C<n>.reversibility\` (one-way/two-way),
18249
+ \`C<n>.blastRadius\` (local/module/contract) and \`C<n>.newConcept\`
18250
+ (yes/no) — or land without a verdict, which lists every chunk as
18251
+ critical.
18252
+ `;
18166
18253
  const buildReviewTriageMessage = () => `Judging each note the human added to \`.gtd/REVIEW.md\` (a line note, a
18167
18254
  footnote, or prose under a chunk) with one verdict: \`edit\` (a change
18168
18255
  request), \`question\`, \`nit\` (a small fix needing no re-plan) or
@@ -39052,7 +39139,8 @@ const severityOf = (note, judgment, minP) => {
39052
39139
  if (critical.length > 0) return {
39053
39140
  tier: "critical",
39054
39141
  reason: `critical: ${critical.join(", ")}`,
39055
- rank: rank()
39142
+ rank: rank(),
39143
+ ...a("security") === "yes" ? { security: true } : {}
39056
39144
  };
39057
39145
  const should = [...a("blastRadius") === "contract" ? ["contract"] : [], ...a("newConcept") === "yes" ? ["new concept"] : []];
39058
39146
  if (should.length > 0) return {
@@ -39066,29 +39154,30 @@ const severityOf = (note, judgment, minP) => {
39066
39154
  rank: 2
39067
39155
  };
39068
39156
  };
39069
- const severityQuestions = (note) => [
39157
+ /** `asks` names the change in full, `change` refers back to it. */
39158
+ const severityQuestions = (id, asks, change) => [
39070
39159
  {
39071
- id: `${note.id}.security`,
39160
+ id: `${id}.security`,
39072
39161
  primitive: "noul",
39073
- instructions: `Does the change the human asks for in note "${note.id}" (on "${note.anchor}") touch security — authentication, authorisation, secrets, untrusted input or data exposure?`,
39162
+ instructions: `Does ${asks} touch security — authentication, authorisation, secrets, untrusted input or data exposure?`,
39074
39163
  criteria: "yes: it changes or could weaken a security property. no: it does not."
39075
39164
  },
39076
39165
  {
39077
- id: `${note.id}.reversibility`,
39166
+ id: `${id}.reversibility`,
39078
39167
  primitive: "choice",
39079
- instructions: `Once the change in note "${note.id}" ships, can it be undone without breaking anyone who already depends on it?`,
39168
+ instructions: `Once ${change} ships, can it be undone without breaking anyone who already depends on it?`,
39080
39169
  criteria: "one-way: a published API, file format, persisted data, protocol or other contract that cannot be taken back. two-way: can be reverted freely."
39081
39170
  },
39082
39171
  {
39083
- id: `${note.id}.blastRadius`,
39172
+ id: `${id}.blastRadius`,
39084
39173
  primitive: "choice",
39085
- instructions: `How far does the change in note "${note.id}" reach?`,
39174
+ instructions: `How far does ${change} reach?`,
39086
39175
  criteria: "local: one function or file. module: a whole module or several files. contract: a public interface other code or users depend on."
39087
39176
  },
39088
39177
  {
39089
- id: `${note.id}.newConcept`,
39178
+ id: `${id}.newConcept`,
39090
39179
  primitive: "noul",
39091
- instructions: `Does the change in note "${note.id}" introduce a concept new to this codebase — a new abstraction, entity or term? Judge from the evidence, never a glossary; reusing or extending an existing concept is not new.`,
39180
+ instructions: `Does ${change} introduce a concept new to this codebase — a new abstraction, entity or term? Judge from the evidence, never a glossary; reusing or extending an existing concept is not new.`,
39092
39181
  criteria: "yes: a genuinely new concept. no: reuses or extends an existing one."
39093
39182
  }
39094
39183
  ];
@@ -39105,7 +39194,7 @@ const triage$1 = (severities) => async (notes) => {
39105
39194
  answers: {},
39106
39195
  truncated: []
39107
39196
  } : await judge("review.triage", {
39108
- questions: asked.flatMap((n) => [verdictQuestion(n), ...severityQuestions(n)]),
39197
+ questions: asked.flatMap((n) => [verdictQuestion(n), ...severityQuestions(n.id, `the change the human asks for in note "${n.id}" (on "${n.anchor}")`, `the change in note "${n.id}"`)]),
39109
39198
  evidence: Object.fromEntries(asked.map((n) => [n.id, `Anchor: ${n.anchor}\nReviewer's text: ${n.before}\nHuman's text: ${n.text}`])),
39110
39199
  message: buildReviewTriageMessage(),
39111
39200
  label: "Judging each note"
@@ -39121,6 +39210,67 @@ const triage$1 = (severities) => async (notes) => {
39121
39210
  return [n.id, "edit"];
39122
39211
  }));
39123
39212
  };
39213
+ /** Review order: security, one-way doors, the judge unsure, then should, then could. */
39214
+ const chunkKey = (s) => s.tier === "could" ? 4 : s.tier === "should" ? 3 : s.security === true ? 0 : s.rank === 0 ? 1 : 2;
39215
+ /** A chunk's text, then the code each of its pointers covers: a bare path the whole file. */
39216
+ const chunkEvidence = (text, files) => [text, ...files.map(({ path, line, rangeEnd }) => {
39217
+ const where = `${path}${line === void 0 ? "" : `#${line}${rangeEnd === void 0 ? "" : `-${rangeEnd}`}`}`;
39218
+ const lines = read(path.replace(/^\.\//, ""))?.split("\n");
39219
+ if (lines === void 0) return `${where}: no longer exists`;
39220
+ return `${where}:\n${(line === void 0 ? lines : lines.slice(line - 1, rangeEnd ?? line)).join("\n")}`;
39221
+ })].join("\n\n");
39222
+ const bytes = (text) => new TextEncoder().encode(text).length;
39223
+ /** Fills each batch while the budget's even share still holds its widest evidence, so nothing is cut that fits alone. */
39224
+ const batched = (items, budget) => {
39225
+ const batches = [];
39226
+ let batch = [];
39227
+ let widest = 0;
39228
+ for (const item of items) {
39229
+ const size = bytes(item.evidence);
39230
+ if (batch.length > 0 && Math.floor(budget / (batch.length + 1)) < Math.max(widest, size)) {
39231
+ batches.push(batch);
39232
+ batch = [];
39233
+ widest = 0;
39234
+ }
39235
+ batch.push(item);
39236
+ widest = Math.max(widest, size);
39237
+ }
39238
+ return batch.length === 0 ? batches : [...batches, batch];
39239
+ };
39240
+ /**
39241
+ * Rates every chunk the reviewer wrote — a walkthrough chunk or a carried
39242
+ * finding — and sorts \`full\` by it, each chunk led by its tier and reason.
39243
+ * gtd's summary chunks are not rated and stay last.
39244
+ */
39245
+ const ranked = async (review, full) => {
39246
+ const texts = chunkTexts(full);
39247
+ const chunks = reviewChunks(review).map((c, i) => ({
39248
+ id: `C${i + 1}`,
39249
+ title: c.title,
39250
+ evidence: chunkEvidence(texts.get(c.title) ?? "", c.files)
39251
+ }));
39252
+ if (chunks.length === 0) return full;
39253
+ const minP = numeric(vars.reviewSeverityMinP, Infinity);
39254
+ const batches = batched(chunks, numeric(vars.judgeBudgetBytes, 32768));
39255
+ const severities = /* @__PURE__ */ new Map();
39256
+ for (const [i, batch] of batches.entries()) {
39257
+ const judgment = await judge("review.ranking", {
39258
+ questions: batch.flatMap((c) => severityQuestions(c.id, `the change review chunk "${c.id}" ("${c.title}") walks through`, `the change in review chunk "${c.id}"`)),
39259
+ evidence: Object.fromEntries(batch.map((c) => [c.id, c.evidence])),
39260
+ message: reviewRankingMessage(),
39261
+ label: batches.length === 1 ? "Ranking the review" : `Ranking the review (${i + 1}/${batches.length})`
39262
+ });
39263
+ for (const c of batch) severities.set(c.id, severityOf(c, judgment, minP));
39264
+ }
39265
+ return rankedReview(full, chunks.map((c, i) => ({
39266
+ c,
39267
+ i,
39268
+ s: severities.get(c.id)
39269
+ })).sort((x, y) => chunkKey(x.s) - chunkKey(y.s) || x.i - y.i).map(({ c, s }) => ({
39270
+ title: c.title,
39271
+ label: s.reason
39272
+ })));
39273
+ };
39124
39274
  const isCode$1 = (path) => path !== ".gtd" && !path.startsWith(".gtd/");
39125
39275
  const composedOf = (base, report, nitFix, answered) => ({
39126
39276
  ...report,
@@ -39172,12 +39322,13 @@ const requirePointers = (review, notes, codeChanged) => {
39172
39322
  const walkthrough = async (base, report, lap, carry) => {
39173
39323
  const notes = titledNotes(report);
39174
39324
  await reviewing(base, carry, lap, walkthroughBlock(notes, report.nitFix));
39175
- const review = read(REVIEW);
39176
- if (review === void 0) return;
39177
- requirePointers(review, notes, changesSince(base).some((c) => isCode$1(c.path)));
39178
- const full = withWalkthrough(review, report);
39179
- if (full !== review) await run("review.carrying", writeScript(REVIEW, full), {
39180
- label: "Adding the quality lap's findings",
39325
+ const written = read(REVIEW);
39326
+ if (written === void 0) return;
39327
+ const review = unranked(written);
39328
+ requirePointers(written, notes, changesSince(base).some((c) => isCode$1(c.path)));
39329
+ const full = await ranked(review, withWalkthrough(review, report));
39330
+ if (full !== written) await run("review.carrying", writeScript(REVIEW, full), {
39331
+ label: "Ordering the review and adding the quality lap's findings",
39181
39332
  file: REVIEW,
39182
39333
  base
39183
39334
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pmelab/gtd",
3
- "version": "49.0.0",
3
+ "version": "50.0.0",
4
4
  "private": false,
5
5
  "description": "Git-aware CLI that emits the next prompt for an autonomous coding agent based on the current repository state",
6
6
  "bin": {
@@ -16,6 +16,7 @@ export {
16
16
  reviewDecisions,
17
17
  reviewNotes,
18
18
  reviewRisks,
19
+ severityLine,
19
20
  unansweredNotes,
20
21
  type ReviewChoice,
21
22
  type ReviewDecision,
@@ -1276,6 +1276,33 @@ describe("review.view", () => {
1276
1276
  expect(view.nodes[1]!.detail).toBe("")
1277
1277
  })
1278
1278
 
1279
+ it("shows a chunk's leading severity line as its status, not its prose", () => {
1280
+ const view = review.view(
1281
+ [
1282
+ "# Review: abc1234",
1283
+ "",
1284
+ "## Ranked",
1285
+ "",
1286
+ "_critical: security, one-way door_",
1287
+ "",
1288
+ "The new export.",
1289
+ "",
1290
+ "- [ ] ./a.ts#1-1",
1291
+ "",
1292
+ "## Unranked",
1293
+ "",
1294
+ "_emphasis, not a tier_",
1295
+ "",
1296
+ "- [ ] ./a.ts#2-2",
1297
+ "",
1298
+ ].join("\n"),
1299
+ )
1300
+ expect(view.nodes.map((c) => [c.status, c.detail])).toEqual([
1301
+ ["critical: security, one-way door", "The new export."],
1302
+ [undefined, "_emphasis, not a tier_"],
1303
+ ])
1304
+ })
1305
+
1279
1306
  it("is built from one parse of the document, not one per element", () => {
1280
1307
  const uniqueContent = [
1281
1308
  "# Review: def4567",
@@ -1706,6 +1733,29 @@ describe("reviewNotes", async () => {
1706
1733
  expect(reviewNotes(base, base)).toEqual([])
1707
1734
  })
1708
1735
 
1736
+ it("an edited tier line is a note", () => {
1737
+ const was = doc("_could_\n\nProse.")
1738
+ const after = doc("_critical: this leaks the token_\n\nProse.")
1739
+ expect(reviewNotes(was, after)).toMatchObject([
1740
+ {
1741
+ kind: "chunk",
1742
+ before: "Prose.\n\n_could_",
1743
+ text: "Prose.\n\n_critical: this leaks the token_",
1744
+ },
1745
+ ])
1746
+ })
1747
+
1748
+ it("prose added under a tier is the human's alone, and a leading ! still forces", () => {
1749
+ const was = doc("_could_\n\nProse.")
1750
+ expect(reviewNotes(was, doc("_could_\n\n! Not here.\n\nProse."))).toMatchObject([
1751
+ { kind: "chunk", text: "! Not here. Prose.", forced: true },
1752
+ ])
1753
+ })
1754
+
1755
+ it("a deleted tier line asks nothing", () => {
1756
+ expect(reviewNotes(doc("_could_\n\nProse."), base)).toEqual([])
1757
+ })
1758
+
1709
1759
  it("a pointer note added", () => {
1710
1760
  const after = doc("Prose.").replace("#1-3", "#1-3 rename this")
1711
1761
  expect(reviewNotes(base, after)).toEqual([
@@ -1863,8 +1913,16 @@ describe("reviewChunks", async () => {
1863
1913
  "- [ ] _your answer_",
1864
1914
  ].join("\n")
1865
1915
  expect(reviewChunks(content)).toEqual([
1866
- { title: "Walkthrough", line: 4, pointers: 2 },
1867
- { title: "Fixed automatically", line: 9, pointers: 0 },
1916
+ {
1917
+ title: "Walkthrough",
1918
+ line: 4,
1919
+ pointers: 2,
1920
+ files: [
1921
+ { path: "./src/a.ts", line: 1, rangeEnd: 3 },
1922
+ { path: "./src/b.ts", line: 4, rangeEnd: 6 },
1923
+ ],
1924
+ },
1925
+ { title: "Fixed automatically", line: 9, pointers: 0, files: [] },
1868
1926
  ])
1869
1927
  })
1870
1928
  })