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 @@
- **fix(hire): the guidance told agents to omit the department id; the schema required it; the model obeyed the schema and guessed.** `org_hire`'s `departmentId` was a REQUIRED field whose own description opened "DEFAULT: the department YOU head". An agent read that, reasoned correctly that it should omit the field, met a schema that would not permit it, and improvised the most salient name in context — the company's. It obeyed the instrument over the claim, which is the right thing for it to do.
**The field is optional now and the default is real.** An omitted id resolves to the department the caller heads, or failing that the one they sit in — the resolver that already existed and is word-for-word what the description promised. The promise was always implementable; it simply was not implemented. The worked example shows the omitted form first, because an example that keeps passing the field re-teaches the habit the description is trying to correct.
**The company name is still refused on the override path**, and that refusal is now the only thing standing behind an explicitly-passed id, so it is pinned by a test that passes the field deliberately rather than omitting it. Accepting the name as an alias was rejected again and for a sharper reason than before: with a default in place the alias would exist only on the override path, where a mid-level head naming the company means their OWN team — so it would resolve to the impressive-sounding wrong target rather than the semantically right one.
**A sweep asked the general question of every parameter in the file** — does the schema permit what the prose promises? — across `DEFAULT`, `omit`, `omitted`, `optional` and `leave empty`. Exactly one leaf field disagreed, this one. One other hit reads "never omit this field" and is required, so prose and schema agree. A mechanical guard was therefore not added: an instrument for a class of one is furniture.

- **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 — **A parameter description is a promise about the schema, and the schema is what the model obeys.** A documented default that the schema does not implement does not produce a caller who ignores the prose — it produces one who believes the prose, discovers the field is required anyway, and IMPROVISES a value. That is what happened: "DEFAULT: the department YOU head" over a required field, and an agent that reasoned its way to omitting the field, could not, and filled it with the most salient name in context. The instrument is the schema; the description is a claim about it; they must agree, and where they disagree the claim is what gets believed and the schema is what gets obeyed. The general question worth asking of any tool surface — **does the schema permit what the prose promises?** — is answerable mechanically, which is why the sweep was the substance of this change rather than an appendix to it. Recorded with the sharper reason the company-name alias stays rejected: with a real default in place the alias would exist ONLY on the override path, where a mid-level head naming the company means their own team — so it would resolve to the impressive-sounding wrong target rather than the semantically right one, which is worse than the refusal it would replace.

- 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
70 changes: 51 additions & 19 deletions packages/piing/extensions/organization-intercom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5884,15 +5884,23 @@ const ADD_DEPARTMENT_PARAMETERS = Type.Object({
}, { additionalProperties: false });

const HIRE_PARAMETERS = Type.Object({
departmentId: Type.String({
// OPTIONAL, because the description promises a default and a required field
// cannot deliver one. It was `Type.String` — required — under a description
// opening "DEFAULT: the department YOU head". An agent read the prose,
// reasoned that it should omit the field, met a schema that would not let it,
// and improvised the most salient name in context: the company. It obeyed
// the instrument over the claim, which is the correct thing for it to do.
departmentId: Type.Optional(Type.String({
description:
"Where this person lands. DEFAULT: the department YOU head — a hire joins the team that "
+ "asked for it. Name a different one only when the operator named it. This call never "
+ "creates a department, and a job title never asks for one: \"hire a Chief of Staff\" is a "
+ "hire into your own department, not a new unit. Create a department only when the "
+ "operator asked for a department in those words. "
+ "The company name or slug is NEVER a department id — the root department's id is in org_roster.",
}),
"Where this person lands. OMIT IT to hire into the department you head — that is the "
+ "DEFAULT and it is what you want almost always, because a hire joins the team that "
+ "asked for it. Pass one only to override that, and only when the operator named a "
+ "different department. This call never creates a department, and a job title never "
+ "asks for one: \"hire a Chief of Staff\" is a hire into your own department, not a new "
+ "unit. Create a department only when the operator asked for a department in those "
+ "words. If you do pass one, the company name or slug is NEVER a department id — the "
+ "root department's id is in org_roster.",
})),
/** One person, the original shape. */
person: Type.Optional(PERSON_SEED),
/** Several people in ONE call — see the batch note in `execute`. */
Expand Down Expand Up @@ -7439,7 +7447,7 @@ async function installSubtreeTools(
pi.registerTool({
name: "org_hire",
label: "Hire an organization person",
description: "Hire one durable worker into an EXISTING department — by DEFAULT the one you head — only after the roster shows no suitable existing person. Send person as real JSON, never a quoted string; use people: [ … ] for several at once. Example: {\"departmentId\":\"engineering\",\"person\":{\"name\":\"Rhea\",\"title\":\"Staff Engineer\",\"mandate\":\"Own the SQLite store.\"}}. name is one short first name; the job goes in title. A NEW DEPARTMENT IS THE OPERATOR'S DECISION AND NEVER YOURS TO INFER: if they asked for one in those words use org_add_department, which makes it and its head together; if they did not, this call is the whole answer. \"Chief of Staff\" and \"Head of Growth\" are TITLES, not requests for a unit. No field asks you to justify anything. Put technology requirements in mandate; a hire does not select skills, extensions, or packages. A new hire comes up on its own; you do not have to start them, and nobody is stopped at creation.",
description: "Hire one durable worker into an EXISTING department — by DEFAULT the one you head, so OMIT departmentId unless the operator named another — only after the roster shows no suitable existing person. Send person as real JSON, never a quoted string; use people: [ … ] for several at once. Example: {\"person\":{\"name\":\"Rhea\",\"title\":\"Staff Engineer\",\"mandate\":\"Own the SQLite store.\"}}; add departmentId only to override. name is one short first name; the job goes in title. A NEW DEPARTMENT IS THE OPERATOR'S DECISION AND NEVER YOURS TO INFER: if they asked for one in those words use org_add_department, which makes it and its head together; if they did not, this call is the whole answer. \"Chief of Staff\" and \"Head of Growth\" are TITLES, not requests for a unit. No field asks you to justify anything. Put technology requirements in mandate; a hire does not select skills, extensions, or packages. A new hire comes up on its own; you do not have to start them, and nobody is stopped at creation.",
parameters: HIRE_PARAMETERS,
prepareArguments: stringifiedArgumentRepair(context, "org_hire", HIRE_PARAMETERS) as never,
async execute(_toolCallId, params) {
Expand Down Expand Up @@ -7481,17 +7489,28 @@ async function installSubtreeTools(
// department 'belfort-brothers-capital'" for a department that simply
// did not exist, then followed its remediation sentence into a create
// the core refuses. Both halves are derived now, never static.
const hireDenial = departmentScopeDenial(gate.manifest, hiringManager, params.departmentId);
// THE DEFAULT THE DESCRIPTION PROMISES, resolved here rather than
// demanded of the caller: the department this person heads, or failing
// that the one they sit in. That is `authorityRootDepartmentId`, which
// already existed and is character-for-character what the prose says —
// the promise was always implementable, it simply was not implemented.
const departmentId = params.departmentId ?? authorityRootDepartmentId(gate.manifest, hiringManager);
if (departmentId === undefined) {
throw new CallerRefusal(
"Could not determine which department to hire into, and none was given. Pass departmentId naming one from org_roster.",
);
}
const hireDenial = departmentScopeDenial(gate.manifest, hiringManager, departmentId);
if (hireDenial === "unknown-department") {
throw new CallerRefusal(unknownDepartmentMessage(gate.manifest, hiringManager, params.departmentId, "hire into"));
throw new CallerRefusal(unknownDepartmentMessage(gate.manifest, hiringManager, departmentId, "hire into"));
}
if (hireDenial) {
// Name the ACCEPTED path, not just the refusal. Everyone now carries
// this tool, so the common refusal is a leaf hiring into the
// department it merely sits in — and the answer is to grow its own
// unit first, never to loosen the scope check.
throw new Error(
`'${hiringManager.id}' does not manage department '${params.departmentId}'. ${hiringPathAdvice(gate.manifest, hiringManager)}`,
`'${hiringManager.id}' does not manage department '${departmentId}'. ${hiringPathAdvice(gate.manifest, hiringManager)}`,
);
}
for (const seed of seeds) {
Expand All @@ -7501,35 +7520,35 @@ async function installSubtreeTools(
// on the operator's own defaults, like everybody else.
const request = hireRequest({
slug: gate.slug,
departmentId: params.departmentId,
departmentId,
hiringManagerPersonId: hiringManager.id,
person: seed as unknown as Record<string, unknown>,
});
const outcome = await staffingApply(gate, "/v1/org/person/hire", request as unknown as Record<string, unknown>, {
action: "hire", departmentId: params.departmentId, personId: request.personId || undefined, name: request.name,
action: "hire", departmentId, personId: request.personId || undefined, name: request.name,
});
// A refusal mid-batch reports WHO was already hired. Silently
// dropping that list is how an operator retries a batch and gets
// duplicates of the people who succeeded the first time.
if ("refused" in outcome) {
return routeRefusal("Hire", outcome, { departmentId: params.departmentId, hired });
return routeRefusal("Hire", outcome, { departmentId: departmentId, hired });
}
hired.push({ name: request.name });
}

if (hired.length === 1) {
const only = hired[0]!;
return toolResult(true, `Hired ${only.name} into '${params.departmentId}'. They come up on their own; they stop on their own once they settle after idling.`, {
return toolResult(true, `Hired ${only.name} into '${departmentId}'. They come up on their own; they stop on their own once they settle after idling.`, {
status: "applied",
departmentId: params.departmentId,
departmentId: departmentId,
name: only.name,
hired,
});
}
const roster = hired.map((entry) => entry.name).join(", ");
return toolResult(true, `Hired ${hired.length} people into '${params.departmentId}': ${roster}. They come up on their own; each stops on its own once it settles after idling.`, {
return toolResult(true, `Hired ${hired.length} people into '${departmentId}': ${roster}. They come up on their own; each stops on its own once it settles after idling.`, {
status: "applied",
departmentId: params.departmentId,
departmentId: departmentId,
hired,
});
} catch (error) {
Expand Down Expand Up @@ -8262,6 +8281,19 @@ export function messageWakeDispositionForTest(
* error a validation site throws, so the round trip is testable without
* driving a whole tool.
*/
/**
* The default `org_hire` resolves when `departmentId` is omitted — the one the
* parameter description promises. Exported so BOTH arms of it can be asserted
* without booting a company: the department a head heads, and the department a
* non-head merely sits in.
*/
export function hireDefaultDepartmentForTest(
manifest: IntercomOrganizationManifest,
person: PersonRecord,
): string | undefined {
return authorityRootDepartmentId(manifest, person);
}

export function refusalResultForTest(error: unknown): { details?: Record<string, unknown> } {
return refusalResult(error) as unknown as { details?: Record<string, unknown> };
}
Expand Down
Loading