Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
- **feat(cards): a refusal says "refused"; a crash still says "failed".** The word is a claim about whose fault the failure was, and the two invite opposite recoveries — "refused" invites a corrected call, "failed" invites a retry. So `Hiring teammate refused` when the caller named a department that does not exist, and `Hiring teammate failed` when something actually broke. This is only implementable because the classification exists to follow; the verb reads it rather than matching on message text, which this file bans.
**The split follows the classification, so a refusal added next month gets the right word without anyone remembering to add it.** One predicate, in one place, because three renderers build a failure title and a rule copied three times is three rules waiting to disagree — two of them were bespoke titles that would otherwise have kept saying "failed" for a refusal.
**The `(system fault)` tag reads the same marker.** It measured only the ABSENCE of a status, so the verb moved to the fault marker and the tag stayed on the old instrument — a partial batch wrapping a real crash said "failed" correctly and then dropped the crash marker, leaving somebody debugging it to read a list of people already hired and reasonably conclude they had passed bad input. One classification, two surfaces, and a test that fails if either moves without the other.
**One case turned out not to be what the classification said it was.** A partial hire carries a status so the card can name the people already hired, and a retry does not double-hire them — but the error it wraps may be a genuine crash. A status carried for CONTEXT is not a claim about fault, so that path now asks the wrapped error's own type, and a producer in the same position marks itself rather than being enumerated in the renderer. Without it, a real crash mid-batch would have been labelled a refusal and invited a correction to a call that was never wrong — the earlier defect pointed the other way, and worse for it.

- **fix(install): the installer no longer prints a curl error on the first line a stranger runs.** A clean install printed `curl: (23) Failure writing output to destination` immediately under "Resolving the latest chief release…", and then completed successfully. The install was never broken; the message was, and it appeared on the very first command anyone runs against this project — where a reader has no reason to read it as anything but a failure.
**The cause was a pipeline, not the request.** The release lookup piped curl into `grep -m1`, which exits on its first match; if curl is still writing when it does, curl's write fails and it says so on stderr. The tag had already been captured, which is why everything downstream worked. Whether it appeared at all depended on whether the response outran the pipe buffer, which is why it was intermittent rather than constant — and why it is the kind of thing that reaches a stranger before it reaches the people who built it.
**The installer also puts chief on your PATH itself**, instead of printing a line to copy. It writes to `~/.bashrc` and `~/.zshrc` when they exist, creates the one matching your shell when neither does, and **names every file it touched** — a script that edits somebody's dotfiles and does not say which is asking to be distrusted. Running it again changes nothing: a profile that already exports that directory is recognised and left alone, matched on the PATH rather than on an exact line, so a hand-edited variant counts as done.
Expand Down
2 changes: 2 additions & 0 deletions DECISIONS.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
- 2026-08-28 — **The word a failure card uses is a claim about whose fault it was, so it follows the classification and never a list.** "Refused" invites a corrected call and "failed" invites a retry; using either for the other tells a reader to take the wrong action, and calling a crash a refusal is the worse direction because it sends somebody to fix a call that was right. The rule lives in ONE predicate because three renderers build failure titles, and a rule copied per renderer is a rule that will diverge at the first bespoke card. Recorded with the discovery that made it non-trivial: **a status carried for CONTEXT is not a classification.** The partial-hire card carries one so it can name the people already hired — that is data a retry needs, not an assertion about fault, and the error it wraps may be either kind. Where the two diverge, the error's own type decides and the producer marks itself; the renderer never grows a list of exceptions, because a list is where the next case is missed.

- 2026-08-28 — **Read your own pushed bytes: a claim that reads as verified is not verified, and the checks do not cover claims.** Three defects in one day were of this shape and none was catchable by any test in the repository: a comment naming THREE adapters when there were eight (a wrong number); a comment citing a guard called `CatchPathsFunnelThroughRefusalResult` that nothing answers to (a wrong referent); and a report that a group of conversions was complete on the strength of three sampled greps (a wrong scope). Each read as though somebody had checked, each was green, and each was found by looking at the pushed artifact rather than by running anything. The rule that follows is cheap and mechanical: after pushing, read what you pushed — the bytes at the SHA, not the diff you intended — and read the prose in it as sceptically as the code, because a comment is the one part of a change that no gate can falsify. Its companion, learned the same day: when a claim is disputed, settle it with the bytes at a named SHA rather than with a commit id, since a commit id proves when you pushed and only the bytes prove what is there.

- 2026-08-28 — **A refusal must not lie about whose fault it is, because the label chooses the recovery.** A system fault invites the same call again; a caller error invites a corrected one. So a failure a tool DECIDED must be distinguishable from an exception it SUFFERED, all the way to the surface — and where the distinction is carried by the presence of a field, every adapter that flattens an error has to preserve it or the classification is lost in transit. It was: validation refusals were thrown as plain errors and seven catch adapters dropped them into status-less results, so a whole class of deliberate refusals rendered as crashes. The marker travels ON the error rather than being re-derived, because re-deriving it means matching message text, which is a second parser by another name. Two corollaries recorded with it. The plain error keeps its meaning — an invariant no input should reach IS a system fault and retrying it is right — and the fix is pinned from both sides, since a test asserting only "this is not a system fault" can be satisfied by labelling everything a refusal, which deletes the distinction instead of repairing it. And on the same incident: an ambiguous name is PREVENTED at the parameter that accepts it, never accepted as an alias — an alias binds everywhere the parameter appears, so a convenience at hire becomes a destructive action at remove, which is the wrong-target-from-name-confusion class arriving by a friendlier road.
Expand Down
81 changes: 76 additions & 5 deletions packages/piing/extensions/organization-intercom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6111,6 +6111,24 @@ function organizationToolSuccessPlainText(presentation: ToolSuccessPresentation)
* quirk: the specific `recipient_lookup`/`message_text_required` statuses only
* produce their own label when `waiting` is also true) — `undefined` means this
* is a hard failure, not a retryable/waiting/busy one. */
/**
* Whether a failure card should say "refused" rather than "failed".
*
* ONE rule, in one place, because there are three renderers that build a
* failure title and a rule copied three times is three rules waiting to
* disagree. A classified failure is one the tool DECIDED and can explain, so
* "refused" — a word that invites a corrected call. Anything else stays
* "failed", which invites a retry.
*
* `fault: true` is what a producer sets when it carries a status for CONTEXT
* rather than as a classification: a partial batch naming what already landed,
* where the wrapped error may be a genuine crash. A marker, not a list, so the
* next producer in that position is covered without this predicate changing.
*/
function isCallerRefusalCard(detail: Record<string, any> | undefined): boolean {
return typeof detail?.status === "string" && detail.fault !== true;
}

function organizationToolRetryPresentation(detail: Record<string, any>): { state: CardState; title: string } | undefined {
const waiting = detail.retryable === true || detail.status === "awaiting_handoff" || detail.status === "awaiting_handoffs";
if (!waiting) return undefined;
Expand Down Expand Up @@ -6183,15 +6201,38 @@ function defaultOrganizationToolRenderResult(organization: string, name: string,
// the site describes by token NAME, so renderCard colors them and no color
// is hand-rolled here (AC1). #333: `opId` is the id the structured failure
// record was logged under, so a cryptic card is one grep from full context.
// THE VERB FOLLOWS THE CLASSIFICATION, not a list of card kinds.
//
// A classified failure is one the tool DECIDED and can explain, so it is
// "refused" — a word that invites a corrected call. An unclassified one is
// a caught exception, so it stays "failed", which invites a retry. Getting
// this backwards in either direction is the defect: calling a crash
// "refused" tells a reader to fix a call that was never wrong.
//
// `fault: true` is the one thing a producer sets when it carries a status
// for CONTEXT rather than as a classification — a partial batch naming
// what already landed, where the wrapped error may be a real crash. It is
// a marker rather than a list, so a future producer in the same position
// is covered without this line changing.
const refused = !retry && isCallerRefusalCard(detail);
const titleTags: CardTag[] = [];
if (unclassified) titleTags.push({ text: "(system fault)", token: "dim" });
// THE TAG READS THE SAME MARKER AS THE VERB. `unclassified` alone measured
// only the ABSENCE of a status, so a result carrying one for context while
// wrapping a real crash lost the tag — the verb had moved to the fault
// marker and the tag had stayed on the old instrument. One classification,
// two surfaces, and they must not disagree: a reader debugging a mid-batch
// crash would otherwise see "failed" with no crash marker beside a list of
// people already hired, and reasonably conclude they had passed bad input.
if (unclassified || detail.fault === true) titleTags.push({ text: "(system fault)", token: "dim" });
if (typeof detail.opId === "string") titleTags.push({ text: `(ref ${detail.opId})`, token: "dim" });
if (summary.text) titleTags.push({ text: `· ${summary.text}${summary.truncated ? "…" : ""}`, token: "dim" });
if (!expanded && summary.truncated) titleTags.push({ text: CARD_EXPAND_HINT_TEXT, token: "dim", sep: " " });
return renderDefaultOrganizationToolCard(theme, {
kind: "tool-failure",
icon: retry ? retry.state : "failure",
title: retry ? retry.title : `${organizationToolDomainIcon(name).title} failed`,
title: retry
? retry.title
: `${organizationToolDomainIcon(name).title} ${refused ? "refused" : "failed"}`,
target: retry ? undefined : (target || undefined),
mentions,
titleTags,
Expand Down Expand Up @@ -7541,7 +7582,17 @@ async function installSubtreeTools(
const landed = hired.length
? ` Already hired, do NOT re-send: ${hired.map((entry) => entry.name).join(", ")}. Retry only the rest.`
: "";
if (landed) return toolResult(false, `${safeExceptionMessage(error)}${landed}`, { status: "hire_partial", hired });
// The status here carries the already-hired list; it is NOT a claim
// about whose fault the failure was. The wrapped error can be either
// kind — a mid-batch caller refusal (an unknown department on person
// four) or a genuine crash — so the error's own type decides, exactly
// as it does everywhere else. Without this the card would call a
// crash "refused" and invite a correction to a call that was right.
if (landed) return toolResult(false, `${safeExceptionMessage(error)}${landed}`, {
status: "hire_partial",
hired,
...(error instanceof CallerRefusal ? {} : { fault: true }),
});
return lifecycleFailure(error);
}
},
Expand Down Expand Up @@ -7769,7 +7820,9 @@ async function installSubtreeTools(
// #360: this used to interpolate the raw internal verb into the
// title ("⚠️ bench failed", "⚠️ recall failed") instead of a proper
// sentence-case title.
const hardFailTitle = action === "bench" ? "Bench failed" : "Recall failed";
// The same rule as the default card: a decided refusal is "refused".
const verb = isCallerRefusalCard(detail) ? "refused" : "failed";
const hardFailTitle = action === "bench" ? `Bench ${verb}` : `Recall ${verb}`;
return renderOrganizationCard(theme, {
kind: "tool-failure",
icon: handoff ? "handoff" : domainIcon(CARD_GLYPHS.failure, detail?.retryable ? "warning" : "error"),
Expand Down Expand Up @@ -7970,7 +8023,8 @@ async function installSubtreeTools(
const handoff = detail?.status === "awaiting_handoff" || detail?.status === "awaiting_handoffs";
// #360: this used to interpolate the raw internal verb into the
// title ("⚠️ start-person failed", "⚠️ stop-person failed").
const hardFailTitle = action === "start-person" ? "Start failed" : "Stop failed";
const verb = isCallerRefusalCard(detail) ? "refused" : "failed";
const hardFailTitle = action === "start-person" ? `Start ${verb}` : `Stop ${verb}`;
return renderOrganizationCard(theme, {
kind: "tool-failure",
icon: handoff ? "handoff" : domainIcon(CARD_GLYPHS.failure, detail?.retryable ? "warning" : "error"),
Expand Down Expand Up @@ -8262,6 +8316,23 @@ export function messageWakeDispositionForTest(
* error a validation site throws, so the round trip is testable without
* driving a whole tool.
*/
/**
* Whether the card carries the `(system fault)` tag.
*
* The SAME marker the verb reads, exposed separately so a test can prove the
* two surfaces cannot drift apart — which they had, the verb having moved to
* the fault marker while the tag still measured only the absence of a status.
*/
export function showsSystemFaultTagForTest(detail: Record<string, unknown> | undefined): boolean {
const hasStatus = typeof detail?.status === "string";
return !hasStatus || detail?.fault === true;
}

/** The verb rule, for the discriminating pair. */
export function isCallerRefusalCardForTest(detail: Record<string, unknown> | undefined): boolean {
return isCallerRefusalCard(detail);
}

export function refusalResultForTest(error: unknown): { details?: Record<string, unknown> } {
return refusalResult(error) as unknown as { details?: Record<string, unknown> };
}
Expand Down
91 changes: 90 additions & 1 deletion packages/piing/test/CallerRefusalClassification.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,12 @@
import { readFileSync } from 'node:fs'
import { fileURLToPath } from 'node:url'

import { callerRefusalForTest, refusalResultForTest } from '@test-assets/organization-intercom'
import {
callerRefusalForTest,
isCallerRefusalCardForTest,
refusalResultForTest,
showsSystemFaultTagForTest
} from '@test-assets/organization-intercom'
import { describe, expect, test } from 'vitest'

describe('a decided refusal keeps its classification through the adapters', () => {
Expand Down Expand Up @@ -105,3 +110,87 @@ describe('every catch path funnels through refusalResult', () => {
expect(handFlattenedResults(offending)).toHaveLength(1)
})
})

/**
* THE VERB FOLLOWS THE CLASSIFICATION.
*
* "refused" invites a corrected call; "failed" invites a retry. Which word a
* card uses is therefore a claim about whose fault the failure was, and the
* two must not be interchangeable — a crash called "refused" tells a reader to
* fix a call that was never wrong, which is the #11 defect pointed the other
* way and worse for it.
*/
describe('a card says refused only when the tool decided it', () => {
test('a classified refusal is refused', () => {
expect(isCallerRefusalCardForTest({ status: 'refused' })).toBe(true)
expect(isCallerRefusalCardForTest({ status: 'incumbent_disposition_required' })).toBe(true)
})

/**
* THE DISCRIMINATING HALF. Without it the rule above passes by returning
* true for everything — which would relabel every crash a refusal and delete
* the distinction rather than using it.
*/
test('an unclassified failure is NOT refused', () => {
expect(isCallerRefusalCardForTest({})).toBe(false)
expect(isCallerRefusalCardForTest(undefined)).toBe(false)
})

/**
* A status carried for CONTEXT is not a classification. The partial-hire card
* names what already landed so a retry does not double-hire; the error it
* wraps may be a genuine crash, and only the error's own type knows.
*/
test('a status carried as context with fault:true is NOT refused', () => {
expect(isCallerRefusalCardForTest({ status: 'hire_partial', fault: true })).toBe(false)
expect(isCallerRefusalCardForTest({ status: 'hire_partial' })).toBe(true)
})
})

/**
* THE VERB AND THE TAG READ THE SAME MARKER.
*
* They diverged: the verb moved to the fault marker while the tag still
* measured only the absence of a status. A partial batch wrapping a real crash
* therefore said "failed" — correctly — with no crash marker beside the list of
* people already hired, which reads as bad input to anyone debugging it.
*
* One classification, two surfaces, and a test that fails if either moves
* without the other.
*/
describe('the system-fault tag reads the same marker as the verb', () => {
test('an unclassified failure carries the tag', () => {
expect(showsSystemFaultTagForTest({})).toBe(true)
expect(showsSystemFaultTagForTest(undefined)).toBe(true)
})

test('a decided refusal does NOT carry the tag', () => {
expect(showsSystemFaultTagForTest({ status: 'refused' })).toBe(false)
expect(showsSystemFaultTagForTest({ status: 'hire_partial' })).toBe(false)
})

test('a context-carried status wrapping a crash DOES carry the tag', () => {
expect(showsSystemFaultTagForTest({ status: 'hire_partial', fault: true })).toBe(true)
})

test('verb and tag never disagree about the same detail', () => {
// The property that matters is not either rule alone, it is that they are
// complementary: anything called "refused" must not be tagged a fault, and
// anything tagged a fault must not be called "refused".
const cases: Array<{ label: string; detail: Record<string, unknown> }> = [
{ label: 'no status', detail: {} },
{ label: 'a decided refusal', detail: { status: 'refused' } },
{ label: 'a context status', detail: { status: 'hire_partial' } },
{
label: 'a context status wrapping a crash',
detail: { status: 'hire_partial', fault: true }
}
]
for (const { label, detail } of cases) {
expect(
isCallerRefusalCardForTest(detail) && showsSystemFaultTagForTest(detail),
`${label} must not be both a refusal and a system fault`
).toBe(false)
}
})
})
2 changes: 2 additions & 0 deletions packages/piing/test/IntercomSeamClassification.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,8 @@ const CLASSIFICATION: Readonly<Record<string, Bucket>> = {
// the system's. Presentation, because the whole subject is what the card
// tells a reader about whose fault it is.
refusalResultForTest: 'B',
isCallerRefusalCardForTest: 'B',
showsSystemFaultTagForTest: 'B',
callerRefusalForTest: 'B',
messageWakeDispositionForTest: 'B',
primeManifestForTest: 'B',
Expand Down