Skip to content

Commit bce074a

Browse files
committed
pm(signature): issue-create and the dispatch door refuse unsigned bodies; pins on every first-line reader; one skill line
issue-create refuses an unsigned card body beside the stamp-token rule; fleet-write/dispatch refuses an unsigned comment_edit / issue_patch body in a seat's actions file (the roster's generated body exempt by its imported marker). record-recognisers pins every named first-line reader with and without a leading signature; label-write pins its two readers. seat-post-protocol.md states the signature in one line, paid by the standing-liveness line whose content lives in dispatch-runbook.md. Claude-Session: https://claude.ai/code/session_01QSeKYmhU5KEj1nukYhp1Tg Co-authored-by: Claude <noreply@anthropic.com>
1 parent b691306 commit bce074a

8 files changed

Lines changed: 277 additions & 21 deletions

File tree

‎.claude/skills/pm-dispatch/references/seat-post-protocol.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,9 @@
2525

2626
- 活性惰性半边在接管冲突时评估一次:Routine 座位查调度器 `last_fired` 与 `next_run`。
2727
- 会话座位查最近一条产出评论的时间戳;回收 = 改正文 + 审计评论。
28-
- 常设半边是总监席摘要的座位贴活性巡查,同一判据,细则见 `dispatch-runbook.md`。
2928
- 可跑读法:本车道 `pm:dispatched` 卡按 `updated_at` 降序一搜,或读最近一条认领评论。
3029
- 写侧刷新三时刻:派发、收班、暂停或交接前;时间戳恒取自 `scripts/pm/post-stamped.mjs`。
30+
- 帖与卡正文首行恒为署名 `LANE#N · ACCOUNT · SESSION`;缺即写工具拒发(`post-stamped.mjs` 头注)。
3131
- 刷新前回执敲门;`--body` 拦截:评论晚于末次戳 ⇒ `--ack-through=ID`;空座欠账无卡号 ⇒ 拒写。
3232
- 耐久状态只住正文或以 App 写入;正文守中继上限 60,000 字节,超大贴由半状态巡查浮出。
3333
- 协议升级迁移状态、或巡检发现正文不符时,当场改正文 + 审计评论。

‎scripts/pm/check-half-states.mjs‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1358,7 +1358,8 @@ export function signatureRefusal(body) {
13581358
const lines = String(body ?? '').split(/\r?\n/);
13591359
const at = firstNonBlankIndex(lines);
13601360
const line = at < 0 ? '(the body is empty)' : lines[at].trim();
1361-
const near = at >= 0 && /·/u.test(line) && /\bsession_/u.test(line);
1361+
// A line naming a seat number AND a session id is a signature typed wrong — named as such, so the fix is the spelling.
1362+
const near = at >= 0 && /#[1-9]/u.test(line) && /\bsession_/u.test(line);
13621363
return {
13631364
line: line.length > 160 ? `${line.slice(0, 160)}…` : line,
13641365
why: near

‎scripts/pm/fleet-write/dispatch.mjs‎

Lines changed: 101 additions & 8 deletions
Large diffs are not rendered by default.

‎scripts/pm/issue-create.mjs‎

Lines changed: 55 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,13 @@
3434
* - a repo not shaped `owner/name` (the default is the board the sweep
3535
* tooling resolves, so a card never lands on a repo nobody named);
3636
* - a body carrying `{{NOW}}` / `{{WAS:…}}` — those are `post-stamped.mjs`'s
37-
* contract for comments and body EDITS; a new card is dated by the platform.
37+
* contract for comments and body EDITS; a new card is dated by the platform;
38+
* - a body whose first non-empty line is not the seat's signature,
39+
* `LANE#N · ACCOUNT · SESSION` (#27113) — read by `signatureRefusal`
40+
* (`check-half-states.mjs`, whose banner carries the shape and the ruling),
41+
* listed beside the token refusal when both apply. Every card this door
42+
* creates is seat-authored: the workflow-generated cards are written by
43+
* their workflows, never through here.
3844
*
3945
* ## The transport — `OS_FLEET_TRANSPORT` direct | dispatch | auto
4046
*
@@ -87,7 +93,7 @@ import process from 'node:process';
8793
import { fileURLToPath } from 'node:url';
8894

8995
import { isEntrypoint } from '../invoked-as.mjs';
90-
import { EXIT_PREREQUISITE_NOT_MET, PROXY_FLAG, proxyRearmPlan, resolveSweepRepo } from './check-half-states.mjs';
96+
import { EXIT_PREREQUISITE_NOT_MET, PROXY_FLAG, SEAT_SIGNATURE_SHAPE, proxyRearmPlan, resolveSweepRepo, signatureRefusal } from './check-half-states.mjs';
9197
import { EXIT_UNCONFIRMED, exitForResult, matchRunAnnotations, packRequest, parseRelayAnnotation, relayAnnotationMessage, resolveRoute, sendFleetWrite, unconfirmedText } from './fleet-write/dispatch.mjs';
9298
import { refusalText as relayRefusalText } from './fleet-write/validate.mjs';
9399
import { classifyHttp } from './label-write.mjs';
@@ -200,7 +206,12 @@ export function planFrom(opts, { read = (p) => readFileSync(p, 'utf8'), env = pr
200206
if (body !== null) {
201207
body = String(body).replace(/\r\n/g, '\n').trim();
202208
if (!body) errors.push('the body is empty — a card names its evidence');
203-
else if (STAMP_TOKEN.test(body)) errors.push('the body carries a {{NOW}} / {{WAS:…}} token — those are post-stamped.mjs\'s contract for comments; a new card is dated by the platform');
209+
else {
210+
if (STAMP_TOKEN.test(body)) errors.push('the body carries a {{NOW}} / {{WAS:…}} token — those are post-stamped.mjs\'s contract for comments; a new card is dated by the platform');
211+
// The signature (#27113), judged beside the token rule so a body breaking both hears both.
212+
const unsigned = signatureRefusal(body);
213+
if (unsigned !== null) errors.push(`the body carries no signature line — ${unsigned.why}; its first line reads \`${unsigned.line}\`. Write ${SEAT_SIGNATURE_SHAPE} as the FIRST line; nothing inserts it for you`);
214+
}
204215
}
205216

206217
const labels = dedupe(opts.labels);
@@ -421,13 +432,14 @@ export async function relayMissProbe({ route, send, pace }) {
421432
const SELF_TEST_BATTERIES = Object.freeze({
422433
'the arguments: files not argv for prose, exactly one title source, a repo shape': 10,
423434
'the payload: title trimmed to one line, labels and assignees de-duplicated, nothing invented': 5,
435+
"the signature: a card body opens with the seat's signature line, or the plan is refused before any request": 8,
424436
'the transport: a fake platform, and what each answer does to the exit code': 9,
425437
'dry-run: no request leaves, and the plan is printed': 3,
426438
'the wiring: both halves around the one POST, on the write verb only': 4,
427439
'the relay transport: ONE dispatch carrying the create, the card found by title since the dispatch, and an accepted dispatch with no run UNCONFIRMED under auto too — never the direct POST': 9,
428440
"the relay annotation: the number the relay run's annotation carries is read first — the card read back at it with NO list; absent, the title finds it as before; the read-back still judges the annotated card": 5,
429441
});
430-
const SELF_TEST_BATTERY_FLOOR = 7;
442+
const SELF_TEST_BATTERY_FLOOR = 8;
431443
const UNATTRIBUTED_BATTERY = '(unattributed)';
432444

433445
const batteryCases = new Map();
@@ -450,7 +462,18 @@ export async function selfTest() {
450462
const { tmpdir } = await import('node:os');
451463
const { join } = await import('node:path');
452464

453-
const files = { 'title.txt': ' A card with `backticks` and $HOME in it \n', 'body.md': 'Evidence: a repro.\n\n- step one\n', 'empty.md': ' \n', 'stamped.md': 'Reading time {{NOW}}\n' };
465+
// Every body a seat hands this door opens with the seat's signature (#27113), so the fixtures that must plan do too.
466+
const SIG = 'domain:skills#2 · os-justin · session_01ABCDEFGHJKMNPQRSTVWXYZ';
467+
const files = {
468+
'title.txt': ' A card with `backticks` and $HOME in it \n',
469+
'body.md': `${SIG}\nEvidence: a repro.\n\n- step one\n`,
470+
'empty.md': ' \n',
471+
'stamped.md': `${SIG}\nReading time {{NOW}}\n`,
472+
'unsigned.md': 'Evidence: a repro.\n\n- step one\n',
473+
'unsigned-stamped.md': 'Reading time {{NOW}}\n',
474+
'near-miss.md': 'domain:skills#2 - os-justin - session_01ABCDEFGHJKMNPQRSTVWXYZ\nEvidence.\n',
475+
'second-line.md': 'Evidence: a repro.\n\ndomain:skills#2 · os-justin · session_01ABCDEFGHJKMNPQRSTVWXYZ\n',
476+
};
454477
const read = (p) => {
455478
if (!(p in files)) throw new Error('ENOENT');
456479
return files[p];
@@ -486,6 +509,32 @@ export async function selfTest() {
486509
t('a body carrying a stamp token is refused — that is post-stamped\'s contract', planFrom(parseArgs(['--title', 'x', '--body-file', 'stamped.md']), { read, env }).errors.some((e) => e.includes('{{NOW}}')));
487510
}
488511

512+
// ── the signature ─────────────────────────────────────────────────────────
513+
battery('the signature: a card body opens with the seat\'s signature line, or the plan is refused before any request');
514+
{
515+
const plan = (file) => planFrom(parseArgs(['--title', 'x', '--body-file', file]), { read, env });
516+
const unsigned = plan('unsigned.md');
517+
t('an unsigned body is refused — the plan is not ok', unsigned.ok, false);
518+
t('…and the refusal names the shape to write and the line it read', unsigned.errors.some((e) => e.includes('no signature line') && e.includes('LANE#N · ACCOUNT · SESSION') && e.includes('`Evidence: a repro.`')));
519+
t('a signed body plans, and the signature is SENT as written — nothing stripped, nothing inserted', [plan('body.md').ok, plan('body.md').payload.body.split('\n')[0]], [true, SIG]);
520+
t('a near miss (hyphens for the ` · ` separator) is refused, and named as one', plan('near-miss.md').errors.some((e) => e.includes('reads like a signature and is not one')));
521+
t('⛔ a signature on a LATER line is not a signature: the first non-empty line decides', plan('second-line.md').ok, false);
522+
const both = plan('unsigned-stamped.md').errors;
523+
t('a body breaking BOTH rules hears both, in one refusal', [both.some((e) => e.includes('{{NOW}}')), both.some((e) => e.includes('no signature line'))], [true, true]);
524+
t('an empty body is told it is empty, once — not also unsigned', plan('empty.md').errors.filter((e) => e.includes('body is empty') || e.includes('signature')).length, 1);
525+
const dir2 = mkdtempSync(join(tmpdir(), 'issue-create-sig-'));
526+
try {
527+
writeFileSync(join(dir2, 'u.md'), 'A body.\n', 'utf8');
528+
const cli = spawnSync(process.execPath, [SELF_PATH, '--dry-run', '--repo', 'o/r', '--title', 'Dry', '--body-file', join(dir2, 'u.md')], {
529+
encoding: 'utf8',
530+
env: { ...process.env, OS_PM_WRITE_PACE_FILE: join(dir2, 'pace.jsonl'), GITHUB_TOKEN: 'ghs_FixtureTokenNotRealAtAll0000000000000', GH_TOKEN: '', HTTPS_PROXY: '', https_proxy: '' },
531+
});
532+
t('⛔ the CLI refuses an unsigned body on a DRY RUN too — usage, nothing printed as sent, nothing paced', [cli.status, cli.stdout.includes('NOTHING was sent'), existsSync(join(dir2, 'pace.jsonl'))], [EXIT_USAGE, false, false], cli.stderr.slice(-300));
533+
} finally {
534+
rmSync(dir2, { recursive: true, force: true });
535+
}
536+
}
537+
489538
const dir = mkdtempSync(join(tmpdir(), 'issue-create-'));
490539
try {
491540
// One pace file PER CASE: a stop marker one answer writes must not refuse
@@ -556,7 +605,7 @@ export async function selfTest() {
556605
// ── dry-run ─────────────────────────────────────────────────────────────
557606
battery('dry-run: no request leaves, and the plan is printed');
558607
{
559-
writeFileSync(join(dir, 'b.md'), 'A body.\n', 'utf8');
608+
writeFileSync(join(dir, 'b.md'), `${SIG}\nA body.\n`, 'utf8');
560609
const spawned = spawnSync(process.execPath, [SELF_PATH, '--dry-run', '--repo', 'o/r', '--title', 'Dry', '--body-file', join(dir, 'b.md')], {
561610
encoding: 'utf8',
562611
env: { ...process.env, OS_PM_WRITE_PACE_FILE: join(dir, 'dry-pace.jsonl'), GITHUB_TOKEN: TOKEN_FIXTURE, GH_TOKEN: '', HTTPS_PROXY: '', https_proxy: '' },

‎scripts/pm/label-write.mjs‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1563,7 +1563,7 @@ const SELF_TEST_BATTERIES = Object.freeze({
15631563
'the remaining exits: refusal, mismatch, the ONE-OF block and the dry run': 5,
15641564
'the CLI: what a typo must never be allowed to mean': 8,
15651565
'the relay transport: ONE dispatch carrying the whole stroke, the same read-back, and an accepted dispatch with no run UNCONFIRMED under auto too — never a direct write': 14,
1566-
'the release: a silent claim reclaimed in ONE relay stroke — Release: comment → pm:queue on → pm:dispatched off → unassign — on the knock\'s SERVER-clock age, RELAY-ONLY, never a direct write': 30,
1566+
'the release: a silent claim reclaimed in ONE relay stroke — Release: comment → pm:queue on → pm:dispatched off → unassign — on the knock\'s SERVER-clock age, RELAY-ONLY, never a direct write': 32,
15671567
});
15681568
const SELF_TEST_BATTERY_FLOOR = 11;
15691569
const UNATTRIBUTED_BATTERY = '(unattributed)';
@@ -1819,6 +1819,10 @@ export async function selfTest() {
18191819
{ op: 'unassign', issue: 7, assignees: ['os-tesla'] },
18201820
]);
18211821
t('the release text is judged by the owning `Release:` reader on its FIRST line, and may not pre-write the resume line', [releaseTextRefusal(RELEASE_TEXT), typeof releaseTextRefusal('Released: x'), typeof releaseTextRefusal('note\nRelease: x'), typeof releaseTextRefusal('Release: x\nResume-from: a@b'), typeof releaseTextRefusal('')], [null, 'string', 'string', 'string', 'string']);
1822+
// One leading signature line (#27113) stands ABOVE the first-line key, never in its place.
1823+
const SIG = 'domain:skills#2 · os-justin · session_01ABCDEFGHJKMNPQRSTVWXYZ';
1824+
t('a signed knock reads by the key on the line after its signature; a key on a later line, or under a near-miss signature, still does not', [isRecheckComment(`${SIG}\nOwned-24h re-check: x`), isRecheckComment(`${SIG}\n\n## Owned-24h re-check: x`), isRecheckComment(`${SIG}\nTriage\nOwned-24h re-check: x`), isRecheckComment('domain:skills#2 - os-justin - session_01ABCDEFGHJKMNPQRSTVWXYZ\nOwned-24h re-check: x')], [true, true, false, false]);
1825+
t('…and a signed release text is judged by its `Release:` line: accepted signed as unsigned, refused when no `Release:` line follows the signature', [releaseTextRefusal(`${SIG}\n${RELEASE_TEXT}`) === releaseTextRefusal(RELEASE_TEXT), typeof releaseTextRefusal(`${SIG}\nnote\nRelease: x`)], [true, 'string']);
18221826

18231827
// The fixture card the protocol names, younger than the clock.
18241828
const young = await driveRelease(RELEASE_ARGV, silentCard(5));

‎scripts/pm/post-stamped.mjs‎

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -932,7 +932,9 @@
932932

933933
import process from 'node:process';
934934
import { spawnSync } from 'node:child_process';
935-
import { readFileSync } from 'node:fs';
935+
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
936+
import { tmpdir } from 'node:os';
937+
import { join } from 'node:path';
936938
import { fileURLToPath } from 'node:url';
937939
import { isEntrypoint } from '../invoked-as.mjs';
938940
import {
@@ -4067,8 +4069,9 @@ const SELF_TEST_BATTERIES = Object.freeze({
40674069
'the decision facets: a decision analysis carries its marker, and a delta, a record or a quotation is not one': 22,
40684070
'the relay transport: the same act as one op, the comment found on the board, the exit register kept apart': 13,
40694071
"the size route: over the relay's body cap under auto THIS write goes direct with one line naming bytes, cap and identity; at or under it the relay; explicit dispatch refuses naming the bytes; nothing else re-routes": 13,
4072+
'the signature: every body opens with LANE#N · ACCOUNT · SESSION or is refused, beside the stamp contract; one leading signature changes no reading': 11,
40704073
});
4071-
const SELF_TEST_BATTERY_FLOOR = 17;
4074+
const SELF_TEST_BATTERY_FLOOR = 18;
40724075
const UNATTRIBUTED_BATTERY = '(unattributed)';
40734076

40744077
let selfTestReachedVerdict = false;
@@ -5521,6 +5524,40 @@ export function selfTest() {
55215524
t('⛔ the proxy re-exec at the entrypoint is UNCONDITIONAL — a dry run resolves the route, which is a read, so it re-execs too', entrySource.includes('rearmThroughProxy(process.argv.slice(2))') && /--dry-run'\)\s*\?\s*null\s*:\s*rearmThroughProxy/.test(entrySource) === false);
55225525
}
55235526

5527+
// ── the signature (#27113) ─────────────────────────────────────────────────
5528+
battery('the signature: every body opens with LANE#N · ACCOUNT · SESSION or is refused, beside the stamp contract; one leading signature changes no reading');
5529+
{
5530+
const SIG = 'domain:skills#2 · os-justin · session_01QSeKYmhU5KEj1nukYhp1Tg';
5531+
t('a signed body has no refusal row; an unsigned one has one naming the line it read', signatureRefusal(`${SIG}\nbody`) === null && signatureRefusal('Hello\nbody')?.line === 'Hello');
5532+
t('the refusal text prints the shape to write and says nothing was written', signatureRefusalText(signatureRefusal('Hello'), { tool: 'post-stamped' }).includes('LANE#N · ACCOUNT · SESSION') && signatureRefusalText(signatureRefusal('Hello'), { tool: 'post-stamped' }).includes('Nothing was written'));
5533+
t('⛔ the positional stamp rule still reads the ACT\'s line: a bare stamp on the claim line under a signature is refused', kinds(`${SIG}\nClaim: seat 2026-09-10T06:37Z`, NOW_MS).join() === 'positional');
5534+
t('…and `{{NOW}}` there is accepted, the signature sent as written above it', renderBody(`${SIG}\nClaim: seat {{NOW}}`, NOW_MS).ok && renderBody(`${SIG}\nClaim: seat {{NOW}}`, NOW_MS).body.split('\n')[0] === SIG);
5535+
const PLAIN = ['Decision record: who lands the gate', '', '- **A**: the seat lands it', '- **B**: the maintainer lands it', '', 'This seat recommends **A**.'].join('\n');
5536+
t('a decision analysis framed on its plain OPENING line is refused unsigned and signed alike — the frame is the line after the signature', decisionFacetRefusals(PLAIN).length === 1 && decisionFacetRefusals(`${SIG}\n${PLAIN}`).length === 1);
5537+
const dir = mkdtempSync(join(tmpdir(), 'post-stamped-sig-'));
5538+
try {
5539+
const run = (args, body) => {
5540+
writeFileSync(join(dir, 'b.md'), body, 'utf8');
5541+
return spawnSync(process.execPath, [SELF_PATH, ...args, `--file=${join(dir, 'b.md')}`], { encoding: 'utf8', env: { ...process.env, HTTPS_PROXY: '', https_proxy: '', GITHUB_TOKEN: '', GH_TOKEN: '' } });
5542+
};
5543+
const comment = run(['--dry-run', '--comment=1', '--repo=o/r'], 'Hello {{NOW}}\n');
5544+
t('⭐ the CLI refuses an unsigned COMMENT on a dry run: exit 2, the signature refusal on stderr, nothing on stdout', comment.status === EXIT_REFUSED && comment.stderr.includes('opens with no signature line') && comment.stdout === '');
5545+
const body = run(['--dry-run', '--body=1', '--repo=o/r'], '**This post is the single authoritative registration.**\n');
5546+
t('⭐ …and an unsigned card BODY too — both modes, the seat post above all', body.status === EXIT_REFUSED && body.stderr.includes('opens with no signature line'));
5547+
const both = run(['--dry-run', '--comment=1', '--repo=o/r'], 'Claim: seat 2026-09-10T06:37Z\n');
5548+
t('a body breaking the stamp contract AND unsigned hears BOTH, under one exit', both.status === EXIT_REFUSED && both.stderr.includes('stamp-contract problem') && both.stderr.includes('opens with no signature line'));
5549+
const empty = run(['--dry-run', '--comment=1', '--repo=o/r'], ' \n');
5550+
t('an empty body is told it is empty, once — not also unsigned', empty.status === EXIT_REFUSED && empty.stderr.includes('the body is empty') && !empty.stderr.includes('signature'));
5551+
} finally {
5552+
rmSync(dir, { recursive: true, force: true });
5553+
}
5554+
// The slice the size-route battery takes: main, up to the self-test's banner.
5555+
const ownText = readFileSync(SELF_PATH, 'utf8');
5556+
const mainSource = ownText.slice(ownText.indexOf('async function main(argv)'), ownText.indexOf('// --self-test — offline'));
5557+
t('structural: the signature is judged on BOTH modes, before the keyed lines and before the route is resolved', /const unsigned = rendered\.kind === 'empty' \? null : signatureRefusal\(input\);/u.test(mainSource) && mainSource.indexOf('signatureRefusal(input)') < mainSource.indexOf('claimKeyedLineRefusals(') && mainSource.indexOf('signatureRefusal(input)') < mainSource.indexOf('await resolveRoute('));
5558+
t('the usage says it, with the shape', USAGE.includes('signature') && USAGE.includes(SEAT_SIGNATURE_SHAPE));
5559+
}
5560+
55245561
// The floor, evaluated last: a battery that stops running names itself here.
55255562
const failed = cases.filter((c) => !c.ok);
55265563
for (const c of cases) console.log(` ${c.ok ? '✓' : '✗'} ${c.name}`);

0 commit comments

Comments
 (0)