* 공정 O4 · 겨냥 퍼널 단계 봉인→귀환 — 감사 잔여분 전량 + 그것을 볼 수 있는 게이트 (1.15.3 / 2.21.4)
1.15.2 감사는 목록을 남겼다. 이 커밋이 그 목록을 닫는다. 아래 하나하나는
같은 방식으로만 신뢰한다: 수리를 되돌리고, 게이트가 빨간불이 되는지 확인하고,
되돌린다 (`npm run verify`가 한 번에 12개를 그렇게 검증한다).
우리에게 도착한 사용자의 작업이 버려지고 있던 자리
- 픽커 6개 중 5개가 "창이 안 열렸다"를 "사용자가 거절했다"로 보고했다.
1.15.2는 봉인 확인창만 고쳤다. 정산·연기·전제확인·미결질문은 그대로
cancel/호스트실패를 거절로 접어버리고 "기록하지 않았습니다"라고 답한
뒤 사용자가 방금 쓴 글을 버렸다. 여섯 곳 모두 두 사실을 구분하고,
사용자의 재료를 돌려주며 한 문장으로 끝내는 길을 준다.
- Accept 뒤에 오는 거절이 그 말을 돌려준다. 400자를 넘겨 다시 쓰면
모델에게 도착하는 건 "너무 깁니다"뿐이었고, 그래서 모델은 사용자에게
문단을 다시 쓰라고 했다. data.user_input이 그 글을 싣는다. (data는
로케일 치환을 타지 않는 유일한 채널이라 retry_hint도 같이 싣는다.)
- 밖에서 뜬 물음이 더는 허공이 아니다. 툴 호출 밖에서 뜨니 답을 넣어도
화면이 그대로였다 — 성공과 실패가 똑같이 침묵이었다. 결과가 다음 툴
호출에 한 줄로 돌아온다. 기록하지 못한 경우까지.
조용히 사실과 다른 말을 하던 자리
- argus_amend가 하지도 않은 계정 푸시를 했다고 답했다. 문장만 고친 수정은
아무 호출 없이 account_synced:true를 반환했다.
- 만료된 계정 연결을 "연결 안 됨"이라 보고했다 — 즉 침묵. 그래서 봉인과
정산이 계정에 안 닿는 동안 모든 화면이 평소와 같았다.
- argus_check_in이 ARGUS_TOKEN만 봤다. 정상 경로로 연결한 사용자는 계정에
살아 있는 결정을 두고 "아무 데도 없다"는 답을 받았다.
- argus_sync가 "여기서 이미 정산됨"과 "쓰기 실패"를 한 continue로 삼켰다.
기록이 상할 수 있던 두 가지
- 영수증을 fsync 없이 썼다. 원장은 fsync하므로, 크래시 창에서 원장은
"정산됨"인데 영수증은 0바이트가 될 수 있었다.
- 기록된 텍스트가 스파인 줄을 위조할 수 있었다. 브랜드 토큰을 출력에서만
이스케이프한다 (원장의 사용자 문장은 손대지 않는다).
- 정산 카드가 다른 원장을 겨냥할 수 있었다. 경로가 이제 결과와 함께 온다.
아무것도 안 보고 있던 게이트
- 배터리의 적대 입력 시나리오는 답을 출력만 했다. 이제 왕복 무손실·닫힌
손잡이 집합·평결값 부재를 단정한다. 그 과정에서 위조 결함이 잡혔다.
- 픽커 응답이 한국어 문구에 묶여 있어 카피 한 줄만 바뀌어도 조용히 다른
질문에 답하고 있었다. 스키마로 라우팅하고, 모르는 픽커는 하드 실패.
- month/dismiss 버킷과 거절된 연기는 어디에도 단정이 없었다. 셋 다 이제
문장이 아니라 원장을 확인한다.
- 밖에서 뜨는 물음에는 eval이 아예 없었다 — 사용자가 부르지 않는 유일한
표면인데. evals/ambient-picker.mjs가 실서버를 몰아 8개 약속을 검증한다.
- 호스트 행렬에 hostile-error(선언하고 거부하는 호스트)와 text-only(글만
쓰고 enum은 접힌 채 두는, 가장 그럴듯한 실제 제스처)가 추가됐다.
유실 금지 불변식이 6개 중 1개가 아니라 모든 물음에 걸린다.
검증: 14 게이트 초록 + 심은 회귀 12개 전부 적발.
단위 1124 · 호스트 306 checks/0 · 카드 28 제스처 · 밖의물음 21/0 · 배터리 92콜/0 RED
claude 프로세스 킬 차단 훅을 레포에 커밋한다 (지금까지 로컬 파일이었다).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* eval: import the closed handle set instead of re-typing it
My first draft of the battery's spine check hand-copied NEXT_ACTIONS and got it
wrong in both directions — it invented `argus_sync` and dropped `skip`, so it
would have raised a false alarm on a legitimate surface while waving through a
handle that does not exist. This repo bans copied enums everywhere else for
exactly this reason; the gate does not get an exemption.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* plugin: /doctor knows all three confirm surfaces, and a gate keeps it that way
1.15.0 added the MCP Apps `card`, and `/argus:doctor` still knew only `one_tap`
and `text_fallback`. On Claude Desktop — the one host where the card actually
renders — the item did not read as broken; it simply was not there. The producer
gained a case and the consumer did not, which is the exact silent drift this repo
keeps catching in this shape.
`picker-surface-parity.test.mjs` derives the value list from the MCP source and
fails, naming the missing value, if any plugin surface that has to explain it
cannot. Verified by adding a fourth value to wireFacts() and watching both
consumers turn red.
Also: `workflow_dispatch` on CI. Actions events have stalled for this repo twice
now, and with no run at all there is no way to tell "no green yet" from "red".
A button is cheaper than the argument.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* docs: the verify README says what actually runs again
The three ported observatories and the self-tests were not in it, so the file
described a smaller product than the command checks.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* blueprint: record the open-question picker becoming unreachable on the 2.0.0 surface
§8 대기 목록, not a fix. The elicitation path that let a user close their own
open question in their own words cannot be reached now that the public schema
requires `decision`. Naming it where the founder will see it beats leaving it
as a comment in an eval nobody reads.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: the new recovery lines pointed at an action that does not exist
The CONNECTION_EXPIRED copy I added an hour ago told the user to run
`argus_settings action="connect"`. There is no `connect` action — connecting is
a CLI command (`npx argus-decision-mcp connect`, or /argus:connect in the
plugin). Following that sentence lands on INVALID_INPUT: a dead end, introduced
by the fix FOR dead ends, in the same session that exists to remove them.
Fixed in all four places, and NOT_CONNECTED now names the one-tap browser flow
instead of a token paste (the token path stays, for CI).
`recovery-paths-exist.test.ts` derives the callable tool names and every
`action` literal from the server itself and fails when shipped copy names one
that is not there. It found a second stale reference on its first run
(argus_seal's description still cited argus_open_decision, gone since 2.0.0).
verify: 15 gates + 13 planted regressions caught + tree restored. 1096 unit.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(ui): the picker and the card are now readable by a person, not by us
Everything until now verified the wiring — resource listed, args carried, ask
fires. None of it asked whether a human could read the result. I rendered both
surfaces and looked.
- every outcome button printed the raw enum under its Korean label
(예측대로/held, 걱정 피함/avoided, 일부만/partial, 아직/later, 빗나감/missed):
our filing system, shown to a user who never asked for it, at the moment we
ask them to commit — and no help with the one pair people confuse.
- the settle picker never said WHICH prediction it was asking about.
- it pointed at the wrong handle: "아직 모르겠으면 Decline" (Decline records
nothing and re-asks; still_pending moves the date).
- "아직" sat among the four verdicts, inviting "no answer yet" to be filed as
an answer.
- Korean prose was set in monospace.
- the escape hatch was the least legible thing on screen.
- the five labels existed in three hand-kept copies; now one module.
Two new gates, each verified by re-planting the defect: the card may not show an
enum value where a human reads, and a picker must name the record it asks about.
verify: 15 gates + 13 planted regressions caught + tree restored. 1105 unit.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(ui): the deferred screen printed the raw enum, and my new gate could not see it
Screenshotting the states I had NOT looked at found a defect I created an hour
earlier: moving still_pending out of the outcomes table into its own handle made
the label lookup fall through, so the deferral confirmation headlined
"still_pending" in gold. The gate I had just written read the label tables, not
the rendered screen, so it stayed green.
Same screen, two more: it stamped the closing anchor on a deferral (a loop that
did not tie), and left "5일 지남" in the header after the date had moved.
The card gate now DRIVES the after-the-click states in the VM host and fails if
any enum value reaches the DOM, if a deferral is worded as a result, if the
anchor appears without a settle, or if the header keeps the stale date. Verified
by re-planting the enum and watching it go red (self-test ⑬).
verify: 15 gates + 14 planted regressions caught + tree restored. 1105 unit.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* chore: drop a probe script I left in the tree
_surfaces.mjs was a throwaway I used to dump what the pickers actually render.
It has no business in the repo.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>