diff --git a/scripts/pm/check-half-states.mjs b/scripts/pm/check-half-states.mjs index 90d391af06..610dfa259e 100644 --- a/scripts/pm/check-half-states.mjs +++ b/scripts/pm/check-half-states.mjs @@ -318,8 +318,8 @@ * own state model (maintainer ruling 2026-08-19/20, verbatim: 「同意 并 * 存」) has it COEXIST with the card's standing `pm:*` label rather than * replace it: the objecting seat applies it alongside its evidence - * comment, and the triage Routine — which re-judges every `pm:retriage` - * card each fire, high priority (SKILL.md) — is the only remover. That + * comment, and the triage Routine — which answers every `pm:retriage` + * card's question each fire (SKILL.md) — is the only remover. That * division of duties is H13's healing-loop shape again: a label nobody * re-checks for age is the next "state nobody is watching", and without * this item nothing here would say so. Aged past one cycle the row names @@ -329,11 +329,11 @@ * disputed grading is unidentifiable (异议对象不明) from the label set * alone. Age is read from `updated_at`, the same proxy H13 uses for the * same reason: this sweep makes no per-card timeline fetch for the - * label-APPLICATION event, and every triage re-judgement (grade kept or - * changed) bumps `updated_at`, so a stale reading means nothing touched - * the card since some triage pass — which is exactly the failure this - * item exists to name. Report-only and NOT loud, like H14–H16: the - * remedy is the triage Routine's next fire, never a label written here. + * label-APPLICATION event, and every triage answer (whatever it decides) + * bumps `updated_at`, so a stale reading means nothing touched the card + * since some triage pass — which is exactly the failure this item exists + * to name. Report-only and NOT loud, like H14–H16: the remedy is the + * triage Routine's next fire, never a label written here. * * ## H19 — the question that ENDS a block * @@ -3086,17 +3086,17 @@ function readTrackedFiles() { /** * H18 threshold — one triage-Routine cycle, the same reasoning as - * `DOMAIN_HALF_STATE_STALE_HOURS`: the Routine fires HOURLY and re-judges - * every `pm:retriage` card "each fire, high priority" (SKILL.md, 「`pm:retriage` - * 重判每 fire 高优先处理」), so a card still carrying the label after 2h has - * survived at least one re-judgement pass it should not have. Age is read - * from `updated_at` rather than a per-card timeline fetch for the - * label-APPLICATION event: this sweep makes that fetch for no item (H13's - * same proxy choice, for the same reason — see `h13DomainWithoutPmState`), - * and every triage write on the card (grade kept or changed) bumps - * `updated_at`, so a stale reading here means nothing touched the card since - * any triage pass at all, which is exactly the failure this item exists to - * name. + * `DOMAIN_HALF_STATE_STALE_HOURS`: the Routine fires HOURLY and ANSWERS the + * question on every `pm:retriage` card each fire (SKILL.md, 「`pm:retriage` 每 + * fire 先答异议评论所求,答后同笔摘标;须维护者答的进收件箱,标照摘」), so a card + * still carrying the label after 2h has survived at least one answer pass it + * should not have. Age is read from `updated_at` rather than a per-card + * timeline fetch for the label-APPLICATION event: this sweep makes that fetch + * for no item (H13's same proxy choice, for the same reason — see + * `h13DomainWithoutPmState`), and every triage write on the card (whatever + * the answer) bumps `updated_at`, so a stale reading here means nothing + * touched the card since any triage pass at all, which is exactly the failure + * this item exists to name. */ export const RETRIAGE_STALE_HOURS = 2; @@ -3127,10 +3127,11 @@ export function h18RetriageAged(issue, nowMs = Date.now()) { ? `alongside its standing ${coexisting.map((l) => `\`${l}\``).join(', ')}` : 'ALONE, with no coexisting standing `pm:*` label — the disputed grading is unidentifiable (异议对象不明)'; return ( - `\`pm:retriage\` carried ${carrying}, ${reading} — the objecting seat's grade is still undecided past ` + - `one triage cycle. The triage Routine re-judges every \`pm:retriage\` card each fire (SKILL.md); a card ` + - `still here past the threshold is a re-judgement pass that did not run, not inventory: resolve the ` + - `grade (keep or change) and drop the label in the same write, oldest first.` + `\`pm:retriage\` carried ${carrying}, ${reading} — the question the objecting seat put to triage is ` + + `still unanswered past one triage cycle. The triage Routine answers every \`pm:retriage\` card's ` + + `question each fire (SKILL.md); a card still here past the threshold is an answer pass that did not ` + + `run, not inventory: answer what the comment asks and drop the label in the same write (an answer that ` + + `needs the maintainer goes to the inbox with the label still removed), oldest first.` ); } @@ -4081,10 +4082,10 @@ export function h21NegatedClosingKeyword(pr) { * itself, whose label is what makes the seat list page a board. * A closed seat card keeps it as identity, not as state. * `pm:epic` a delegation marker on a parent, the same kind of identity. - * `pm:retriage` a request for re-judgement. Plausibly residue too, and it did - * not appear in the census — so it stays out until something - * measures it, rather than being widened in on a hunch. The - * set is one edit away when that measurement exists. + * `pm:retriage` a question put to the triage seat. Plausibly residue too, and + * it did not appear in the census — so it stays out until + * something measures it, rather than being widened in on a + * hunch. The set is one edit away when that measurement exists. */ export const PM_RESIDUE_LABELS = [ 'pm:dispatched', @@ -14307,6 +14308,16 @@ function selfTest() { t('H18: …and the finding names the threshold', h18row(domainCard(['pm:retriage', 'pm:queue'], hoursAgo(3)), NOW).includes(`${RETRIAGE_STALE_HOURS}h`), true); t('H18: …and names the coexisting standing label', h18row(domainCard(['pm:retriage', 'pm:queue'], hoursAgo(3)), NOW).includes('`pm:queue`'), true); t('H18: multiple coexisting labels are all named', h18row(domainCard(['pm:retriage', 'pm:blocked', 'pm:blocking'], hoursAgo(3)), NOW).includes('`pm:blocked`') && h18row(domainCard(['pm:retriage', 'pm:blocked', 'pm:blocking'], hoursAgo(3)), NOW).includes('`pm:blocking`'), true); + // The label is a QUESTION put to the triage seat, not a request to re-grade + // (SKILL.md 「`pm:retriage` 每 fire 先答异议评论所求,答后同笔摘标;须维护者答的进收件箱,标照摘」): + // the row says the question is unanswered, and the remedy is to answer what + // the objection comment asks and drop the label in the same write. + t('H18: the row reads the label as an unanswered question', h18row(domainCard(['pm:retriage', 'pm:queue'], hoursAgo(3)), NOW).includes('is still unanswered past one triage cycle'), true); + t('H18: …and the remedy is answer + drop the label in the same write', h18row(domainCard(['pm:retriage', 'pm:queue'], hoursAgo(3)), NOW).includes('answer what the comment asks and drop the label in the same write'), true); + t('H18: …and an answer that needs the maintainer still drops the label', h18row(domainCard(['pm:retriage', 'pm:queue'], hoursAgo(3)), NOW).includes('needs the maintainer goes to the inbox with the label still removed'), true); + // The retired re-grade vocabulary is gone from BOTH row shapes. + t('H18: no re-grade vocabulary in the coexisting row', /re-judg|grade \(keep or change\)/.test(h18row(domainCard(['pm:retriage', 'pm:queue'], hoursAgo(3)), NOW)), false); + t('H18: …nor in the ALONE row', /re-judg|grade \(keep or change\)/.test(h18row(domainCard(['pm:retriage'], hoursAgo(3)), NOW)), false); // Under-threshold: fresh objection is normal intake latency, not a finding. t('H18: retriage under the threshold -> clean', h18RetriageAged(domainCard(['pm:retriage', 'pm:queue'], hoursAgo(1)), NOW), null); t('H18: exactly at the threshold -> clean (strictly beyond fires)', h18RetriageAged(domainCard(['pm:retriage', 'pm:queue'], hoursAgo(RETRIAGE_STALE_HOURS)), NOW), null); diff --git a/scripts/pm/ensure-pm-labels.sh b/scripts/pm/ensure-pm-labels.sh index 0f0a19d9fb..1d035a969e 100644 --- a/scripts/pm/ensure-pm-labels.sh +++ b/scripts/pm/ensure-pm-labels.sh @@ -255,19 +255,24 @@ for R in objectstack-ai/objectstack objectstack-ai/objectui objectstack-ai/cloud # and list-page scans. A hand-set instance is mislabeling — the sweep # corrects it against the index. gh label create pm:blocking -R "$R" -c 8250df -d "Derived cache from the Blocked-by reverse index: open card with open dependents — never hand-set" 2>/dev/null || true - # pm:retriage COEXISTS with the standing pm:* label and ⛔ never replaces it + # pm:retriage is a QUESTION put to the triage seat (a re-grade, a lane for a + # cross-domain PR, a re-route, a split, a disposition fork a dev report left + # open); it COEXISTS with the standing pm:* label and ⛔ never replaces it # (maintainer ruling 2026-08-19/20, 「同意 并存」): the objecting seat sets it in - # the same write as its evidence comment, and the triage Routine is the only - # remover — hanging and removing belong to two different parties, which is - # what makes the state safe. Named consumers: the dispatchable-candidate - # query (a pm:queue card carrying it is skipped — objection undecided, no - # dispatch) and the triage round's high-priority re-judgement pass. It is in - # this five-repo loop because the state-model row requires the label to exist - # in all five; where a first application already auto-created the object, - # creation here is a no-op and `--reconcile` is what gives it this colour and - # description (measured 2026-08-20: grey with an empty description in - # objectstack, and absent entirely from objectui). - gh label create pm:retriage -R "$R" -c d4c5f9 -d "Awaiting triage re-judgement — coexists with the standing pm:* label; queued cards skip dispatch" 2>/dev/null || true + # the same write as its objection comment, which states what it asks, and the + # triage Routine is the only remover — hanging and removing belong to two + # different parties, which is what makes the state safe. Named consumers: the + # dispatchable-candidate query (a pm:queue card carrying it is skipped — the + # question is unanswered, no dispatch) and the triage round's answer pass, + # which answers what the objection comment asks each fire and drops the label + # in the same write (SKILL.md 「`pm:retriage` 每 fire 先答异议评论所求,答后同笔 + # 摘标;须维护者答的进收件箱,标照摘」). It is in this five-repo loop because the + # state-model row requires the label to exist in all five; where a first + # application already auto-created the object, creation here is a no-op and + # `--reconcile` is what gives it this colour and description (measured + # 2026-08-20: grey with an empty description in objectstack, and absent + # entirely from objectui). + gh label create pm:retriage -R "$R" -c d4c5f9 -d "Question for triage, answered each fire; coexists with the standing pm:* label; no dispatch" 2>/dev/null || true gh label create finding -R "$R" -c c2e0c6 -d "Recorded observation — held, not dispatchable until the findings triage round grades it" 2>/dev/null || true gh label create pm:epic -R "$R" -c 5319e7 -d "Reserved by a dedicated epic PM, parent or sub-issue; other PMs never take it; never with pm:queue" 2>/dev/null || true done