Skip to content

✨ feat: sdk: run a Go extension from one value - #47

Merged
pascalandy merged 1 commit into
mainfrom
m02/t3-extension-entry
Oct 12, 2026
Merged

pascalandy merged 1 commit into
mainfrom
m02/t3-extension-entry

Conversation

@pascalandy

@pascalandy pascalandy commented Oct 11, 2026 •

Copy link
Copy Markdown
Owner

Why

M02-T3 asks for sdk/extension: one value that runs a Go extension, parses its call with its manifest, runs the handler of the command, and prints the answer by the kind of its result, so that it answers exactly like a core command.

Scope

  • sdk/extension: Extension, Handler, Call, Main, and Check, with the API of SDK. Main refuses a manifest or a handler that does not match and answers RUNTIME. Check also applies the catalog rules
  • contract.Registry replaces the package-level Decide and Finish: contract.Core for core, and Manifest.Registry for an extension, whose own codes compete as Output orders them
  • Outcome.HumanNeeded marks a report whose findings need a human, which answers on stdout with exit 78
  • On a signal, the SDK ends the handler's context, its runner stops the programs, and the answer keeps the handler's keys. A handler that has not returned after cleanup and the drain limit, 17 seconds, gets no more time
  • The fixture testdata/extensions/hello, with cmd/ncly-hello. The test binary runs it as hello-external from a script in the data folder, and runs the scenarios of every extension folder. just lint, just fmt-check, and just test name ./testdata/extensions/...
  • pseudo strips nested marks and accepts any word in the form of a code, because an extension's codes are an open set

Tradeoffs

The spec gave a handler 15 seconds. The runner sends SIGKILL to a stubborn program at 15 seconds, so the handler could not return its keys before the SDK gave up. The SDK now also waits for the 2-second drain limit, and the spec says so.

Verification

  • sdk_extension.txtar: through ncly, a success, a warning, a usage error, HELLO_GRUMPY, both reports, and a text result, as JSON, as text, and under the pseudo-locale. Run directly, the same answer, and after SIGTERM, TERMINATED with the handler's key, the program and its grandchild gone, then the SIGKILL path and the stubborn handler
  • sdk/extension tests: Check rejects each fault that the card lists. The registry test orders own codes
  • sdk/contract tests: a failed item gives the batch verdict, RUNTIME or TEMPORARY, unless a signal's code leads, and core's answer check accepts what the SDK renders for a failed batch beside an own code, CONFIG_INVALID, INTERRUPTED, or TERMINATED
  • just check passes on Linux and on macOS

Checklist

  • 1. Spec: SDK in contract.md now says when a stubborn handler gets no more time and what the SDK's catalog holds
  • 2. Scenario: sdk_extension.txtar fails without sdk/extension
  • 3. Code: the scenario and the tests pass
  • 4. Text: the fixture's text and outcome.extension_broken are catalog entries
  • 5. Checks: just check passes
  • 6. Docs: M02-T3 is done, and the code map says that just test names the fixture
  • 7. Sign off: just signoff runs at the merge, as the Merge section of AGENTS.md says

by Opus-5.5 via Claude Code

@pascalandy pascalandy self-assigned this Oct 11, 2026
@pascalandy
pascalandy force-pushed the m02/t3-extension-entry branch from 0b40d25 to ed1a812 Compare October 11, 2026 14:56
@pascalandy
pascalandy force-pushed the m02/t2-import-boundary branch from 2c1a796 to 6d416b2 Compare October 11, 2026 14:56
@pascalandy

Copy link
Copy Markdown
Owner Author

2nd pass: this layer added the SDK's copy of four pieces of core, so it now shares them instead: command.AddManifest builds the tree of a manifest for core and the SDK, command.Finish ends a command line in both, contract.StopSignal names the stopping signal for the dispatch and the SDK, and command.ModuleVersion gives both their build version. No behavior change; just check passes on every layer.

by Opus-5.5 via Claude Code

@pascalandy
pascalandy force-pushed the m02/t3-extension-entry branch from ed1a812 to 16f0350 Compare October 11, 2026 15:05
@pascalandy
pascalandy force-pushed the m02/t2-import-boundary branch 2 times, most recently from 4b5e3ab to a9da72e Compare October 11, 2026 15:22
@pascalandy
pascalandy force-pushed the m02/t3-extension-entry branch from 16f0350 to 34f3e5d Compare October 11, 2026 15:22
@pascalandy

Copy link
Copy Markdown
Owner Author

Review round 1 (Codex GPT-6.1-Sol, xhigh): 6 findings, all fixed in this layer. Each new case fails on the old code.

  1. Lost --json answer: when stdout refused a success answer, ncly exited 1 with an empty stderr. command.Report now retries the RUNTIME failure on stderr (full.sh writes to /dev/full, Linux only).
  2. Programs saw a pipe instead of the terminal: command.Output exposes File(), and the runner passes a wrapped file as it is (sdk/runner test).
  3. Global flags of a direct run never reached the handler: the SDK sets the variables that ncly would set (Globals.Vars, which core's extensionVars shares now). The fixture prints a diagnostic under NCLY_VERBOSE, through ncly or directly.
  4. Warnings of a text command were lost: a successful text result now prints its warnings as text on stderr (hello say with no name).
  5. Check accepted a nil handler: it now rejects it (unit test case).
  6. --version won over --help: help wins, as in core.

by Opus-5.5 via Claude Code

@pascalandy
pascalandy force-pushed the m02/t2-import-boundary branch from a9da72e to ae0d86f Compare October 11, 2026 15:26
@pascalandy
pascalandy force-pushed the m02/t3-extension-entry branch from 34f3e5d to 2b06bdb Compare October 11, 2026 15:26
@pascalandy
pascalandy force-pushed the m02/t2-import-boundary branch from ae0d86f to 36efdda Compare October 11, 2026 15:35
@pascalandy
pascalandy force-pushed the m02/t3-extension-entry branch from 2b06bdb to beba624 Compare October 11, 2026 15:35
@pascalandy
pascalandy force-pushed the m02/t2-import-boundary branch from 36efdda to e10ec5d Compare October 11, 2026 15:42
@pascalandy
pascalandy force-pushed the m02/t3-extension-entry branch from beba624 to 15d3e75 Compare October 11, 2026 15:42
@pascalandy

Copy link
Copy Markdown
Owner Author

Review round 2 (Codex GPT-6.1-Sol, xhigh): the six round 1 fixes hold. It found 2 new medium findings, both fixed in this layer:

  1. The lost-answer fallback dropped the data: when stdout refused the answer, Report replaced the whole outcome with the write failure, which dropped output_dir, results, and warnings. It now keeps them and replaces only the errors (full.sh now checks the greeting key).
  2. A failed batch with a command error led with that error: Decide let an own code such as HELLO_GRUMPY lead, which answer-check rule 8 rejects. This came from M00's Decide, and it shows first here, where the SDK renders an extension's answer. A failed item now makes the verdict the batch's (RUNTIME, or TEMPORARY when every error is temporary and repeating is safe), and the command's errors follow. There are two new TestDecide cases, plus TestCoreAcceptsTheSDKsAnswerOfAFailedBatch, which runs the SDK's answer through core's CheckAnswer. Both fail on the old code.

by Opus-5.5 via Claude Code

@pascalandy
pascalandy force-pushed the m02/t2-import-boundary branch from e10ec5d to a298133 Compare October 11, 2026 15:44
@pascalandy
pascalandy force-pushed the m02/t3-extension-entry branch 2 times, most recently from 2101caa to a7134bb Compare October 11, 2026 15:52
@pascalandy
pascalandy force-pushed the m02/t2-import-boundary branch from a298133 to 2e3c4f5 Compare October 11, 2026 15:52
@pascalandy

Copy link
Copy Markdown
Owner Author

Review round 3 (Codex GPT-6.1-Sol, xhigh; the last round): both round 2 fixes hold. It found one medium finding, fixed in this layer:

  • A signal's error lost its lead in a failed batch. A handler that returned TERMINATED or INTERRUPTED from a nested command, beside a failed item, got exit 1 led by RUNTIME, and core rejected that answer, because 143 and 130 are more cautious than 1. Decide now lets a signal's code lead a failed batch too, as the batch rule of Output says: on cancellation, 130 or 143 takes precedence. A new TestDecide case and two new codes in TestCoreAcceptsTheSDKsAnswerOfAFailedBatch fail on the old code and pass now.

No further per-PR round: this was the third.

by Opus-5.5 via Claude Code

@pascalandy
pascalandy force-pushed the m02/t3-extension-entry branch from a7134bb to feb109b Compare October 11, 2026 15:57
@pascalandy
pascalandy force-pushed the m02/t2-import-boundary branch from 2e3c4f5 to ce4e632 Compare October 11, 2026 17:13
@pascalandy
pascalandy force-pushed the m02/t3-extension-entry branch from feb109b to e790826 Compare October 11, 2026 17:13
@pascalandy

Copy link
Copy Markdown
Owner Author

Cross-stack review, round 1 (Codex GPT-6.1-Sol, xhigh): one change dismissed. It reworded the comments of releaseVersion and ModuleVersion, on the grounds that the code reads no VCS setting. Since Go 1.24, go build derives Main.Version from the VCS tags, so the current comments are accurate and say more.

by Opus-5.5 via Claude Code

@pascalandy
pascalandy force-pushed the m02/t3-extension-entry branch from e790826 to da86277 Compare October 12, 2026 14:02
@pascalandy

Copy link
Copy Markdown
Owner Author

Fixed RDOC1's P2 finding on the report bullet of the SDK section in docs/north-star/contract.md, in this layer's commit, now da86277.

The bullet now says what each mode prints. In human mode, the handler writes its report to Stdout. With --json, the SDK prints the findings from the outcome's data on stdout, in an answer without errors. Before, it said only that the SDK answers on stdout, so a handler that followed it alone printed no human report. I checked each rule against the code before I wrote it:

  • command.Report takes the exit code from Registry.Decide in both modes, so HumanNeeded exits 78 in human mode too. sdk_extension.txtar already proves this with exits 78 ncly hello check.
  • With --json, handle discards the handler's Stdout, and WriteJSON writes an answer without errors to stdout, with ok false at exit 78.

The fix is docs only, so no scenario changed. #48 and #50 are restacked on it, and every layer passes just check on Linux and macOS (table on #50).

by Opus-5.5 via Claude Code

@pascalandy

Copy link
Copy Markdown
Owner Author

Fixed PRIS1's two findings in this layer's commit, now a3372ad.

P2: data race on Output's error. When a handler ignores cancellation, handle stops waiting once cleanup and the drain limit have passed. It then reads out.Err(), while the handler goroutine may still be making its first failed write. Output now holds a mutex while it reads or sets err, in both Write and Err. The write to the file stays outside the lock, so a write that blocks never delays the timeout path. The type's comment now says that Write and Err are safe for concurrent use.

  • Before: PRIS1's probe runs under go run -race with a handler that ignores SIGTERM, then writes to a closed pipe. It reports WARNING: DATA RACE between answer.go:28 and :41, through extension.go:248.
  • After: the same probe reports no race in 3 runs on this layer and 1 run on the top of the stack. It still ends with the expected RUNTIME write failure and exit 1.
  • I added no test. just check runs without -race, and without the race detector an unsynchronized read of the error never fails reliably, so a test would pass even without the fix.

P3: AddManifest's comment. It said that a nil run prints help, but a nil run panics as soon as a command runs. It now says that a command for which run returns nil prints its help. The change is to the comment only.

#48 and #50 are restacked on this commit, and every layer passes just check on Linux and on macOS:

PR Branch Head Linux macOS
#40 m01/t1-manifest 80bcdf4 ok ok
#41 m01/t2-dispatch 68c8db1 ok ok
#42 m01/t3-answer-check e13039f ok ok
#43 m01/t4-cancellation 2c51849 ok ok
#44 m01/t5-stubborn 7bf9d6f ok ok
#45 m02/t1-sdk-packages 6737fb2 ok ok
#46 m02/t2-import-boundary ce4e632 ok ok
#47 m02/t3-extension-entry a3372ad ok ok
#48 m02/t4-bundled-transport ac4f5fe ok ok
#50 m02/docs 9cceec0 ok ok

by Opus-5.5 via Claude Code

- Purpose: M02-T3 asks for sdk/extension, whose Main parses a call with the manifest, runs the handler of the command, and prints its answer by the kind of its result, and whose Check catches what Main would refuse and what breaks the catalog rules
- Impact: a Go extension answers like a core command, with its own codes ordered as Output says and a report that needs a human on stdout with exit 78. On a signal, the SDK ends the handler's context, stops its programs, and keeps the handler's keys. The fixture hello proves it through ncly and run directly, and the test binary runs the scenarios of every extension folder

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@pascalandy
pascalandy force-pushed the m02/t3-extension-entry branch from a3372ad to 0e653ba Compare October 12, 2026 14:49
@pascalandy
pascalandy force-pushed the m02/t2-import-boundary branch from ce4e632 to 776e4b8 Compare October 12, 2026 14:49
@pascalandy
pascalandy changed the base branch from m02/t2-import-boundary to main October 12, 2026 15:25
@pascalandy
pascalandy merged commit 0e653ba into main Oct 12, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant