mirror of
https://github.com/PerpetualSoftware/pad.git
synced 2026-10-25 14:00:40 +00:00
4532eecffdeac4eeff1cd159ff1353df1ccac07b
7 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
6fdd8816f3 |
feat(playbook): match free text to an active playbook via the decision provider (TASK-3120) (#1429)
* feat(server): POST /playbooks/match — typed-decision Choice over active playbooks (TASK-3120)
PLAN-3114 unit 5. A synchronous, read-only endpoint: given free text, asks
the workspace's typed-decision provider which active (non-draft,
non-deprecated) playbook it best matches, with a reserved "none" option so
unrelated text gets an honest answer instead of a forced pick — and, as a
side effect, keeps a workspace with exactly one active playbook askable
(decision.Question.Validate needs >=2 Choice options).
- decision.Runner gains a nil-safe Provider() accessor: this endpoint needs
a plain synchronous Ask, not the owed-jobs pipeline Evaluate drives.
- No provider configured -> 404 decision_provider_unavailable (callers fall
back to slug/trigger routing). A provider error or an out-of-set answer
the provider was never offered -> 502 decision_provider_error, distinct
from the 404 case so a caller can tell "try again" from "not configured".
Zero active playbooks -> 200 with a stated reason, no provider call.
Over the provider's 255-option ceiling (including "none") -> refused
rather than silently dropping playbooks and answering from an
incomplete set.
- listPlaybookItems is factored out of handleListPlaybooks so list and
match share one visibility-scoped item source and can't drift on which
playbooks a caller can see.
- 30s timeout on the synchronous provider call, tighter than the
provider's own worst-case retry budget (~330s) since an HTTP caller
blocks on this one directly, unlike the async decision tick.
- content_state_population_test.go's BUG-3033 sweep gets a new ruling: the
PlaybookSummary call here feeds the provider prompt only, never the
response, so there's no reader-facing text to mark.
Claude-Session: https://claude.ai/code/session_01ByMjAhvdWgbAWWeGE9jwWd
* feat(cli): pad playbook match "<text>" (TASK-3120)
CLI + client-method half of PLAN-3114 unit 5 — POST /playbooks/match.
Table output prints the matched ref/title (or "none"), confidence, model,
and a probabilities table sorted descending; --format json passes the
response through unmodified. Read-only, side-effect-free.
Claude-Session: https://claude.ai/code/session_01ByMjAhvdWgbAWWeGE9jwWd
* docs(skill): route to playbook match as rung 3, offer-only (TASK-3120)
PLAN-3114 unit 5's skill half. Exact invocation-slug match and trigger
match stay rungs 1-2; match is consulted only when the user is plainly
asking for a procedure/workflow and both of those missed — never on
ordinary conversation, since every call spends provider money. Any match
returned is offered, never auto-run, at any confidence: TASK-3137's replay
of this same provider's attention question set found no validated
threshold, so nothing here invents one for this question either. A 404
(no provider configured), a "none" choice, or any other error falls back
silently to today's slug/trigger-only behavior.
Claude-Session: https://claude.ai/code/session_01ByMjAhvdWgbAWWeGE9jwWd
* chore(mcp): de-indent padItemToolDescription's continuation lines (TASK-3120)
Whitespace-only — git diff -w on this file is empty. The description's
continuation lines carried 18 leading spaces purely for visual alignment
under each action's " name — " bullet in the Go source; that padding is
emitted verbatim into the tools/list JSON, where it conveys nothing an
LLM reading the tool description gets any value from.
De-indented by exactly 14 spaces on every line that had >= 18 (all 116 of
them, uniformly, since the file carries only three indentation levels:
0, 2, 18 — no deeper nesting to preserve a relative offset for).
Continuations now land at 4 (bullet's 2 + 2), so a reader can still tell
which action a line belongs to; a 1-space collapse would have inverted
that (continuation LESS indented than its own bullet).
tools/list: 55290 -> 53666 bytes (-1624). initialize: unchanged at 24530
(it carries no tool descriptions). Frees headroom against the 55296
budget for the pad_playbook.match action landing in the next commit —
budget constants themselves are untouched, per standing instruction.
Claude-Session: https://claude.ai/code/session_01ByMjAhvdWgbAWWeGE9jwWd
* feat(mcp): pad_playbook.match action, ToolSurfaceVersion 0.39 -> 0.41 (TASK-3120)
PLAN-3114 unit 5's MCP half, landing on top of the whitespace-only
catalog_item.go trim (previous commit) so this stays within the
tools/list budget with room to spare.
- catalog_playbook.go: new `text` param, new `match` action
(passThrough — the CLI's positional arg is named "text", matching
this tool's param, so no custom shaping is needed) plus a
description bullet in padPlaybookToolDescription.
- tool_surface.go: `match` marked read-only (nothing stored, no job
enqueued).
- dispatch_http_routes.go: `playbook match` routeSpec (POST, flat
{text} body) so the remote /mcp transport can dispatch it in-process.
- version.go: ToolSurfaceVersion 0.39 -> 0.41. 0.40 is reserved for
PR #1427 (the 0.32/0.33 precedent — a gap is cheaper than a
collision). 0.41's own entry: pure ADDITIVE bump, same disposition
as 0.13/0.28/0.11 — nothing existing moved.
- instructions.md, README.md, CLAUDE.md: version headings + the
pad_playbook bullet/table-row/changelog-sentence updated per the
drift tests (TestInstructionsMDCoversEveryCatalogAction,
TestReadmeTableCoversEveryCatalogAction, the two version-match
tests). instructions.md's addition is intentionally terse ("`match`
finds a playbook from text.") — it's embedded in the `initialize`
handshake payload, which has its own 24576-byte budget and had only
46 bytes of headroom; a fuller explanation lives in the tools/list
param/action docs instead, which have budget to spare after the trim.
- catalog_readonly_test.go, dispatch_http_parity_test.go: new
`pad_playbook.match` entries in three hand-built fixture maps
(expected cmdPath ×2, a hand-rolled cmdhelp stub, and the shared
parity-drive fixture input) so the existing bijection/parity/dispatch
guards actually exercise the new action instead of skipping it.
Bytes measured (go test ./internal/mcp -run Budget -v):
tools/list: 55290 (baseline) -> 53666 (after the trim commit) ->
54805 (after this commit) — 491 bytes of headroom left
against the 55296 budget.
initialize: 24530 (baseline, unchanged by the trim) -> 24574 (after
this commit) — 2 bytes of headroom against the 24576
budget. Flagged in the implementer report: this is now
as tight as tools/list was before the trim, and nothing
in this unit addresses it.
Claude-Session: https://claude.ai/code/session_01ByMjAhvdWgbAWWeGE9jwWd
* fix(server,decision): don't leak provider errors, classify oversized requests, fix none's wording (TASK-3120)
Lead's shallow-pass findings on cd7a41ed, applied after the MCP commit.
F1 (disclosure): the provider-error path answered with err.Error()
verbatim. typesafe.go's client surfaces the provider's raw 4xx body in
that string (APIError.Error()), which is the INSTANCE's own account
state with typesafe.ai — key validity, quota, billing wording — not
anything about the caller's request. This endpoint requires only
viewer access, so any workspace member could read it. Now logged
server-side in full (slog, workspace + err) and answered with a fixed
message plus a coarse details.reason ("timeout" for a context
deadline, "upstream" otherwise) via a new writeDecisionProviderError
helper. Test constructs a fake error containing a secret-looking
string and asserts it never reaches the response body.
F2 (misclassification): the request budget is 32000 tokens
(~80000 chars). At a couple hundred active playbooks with realistic
summaries, the Choice question's options alone can exceed it before
any state is even added — or leave no room for the input text. Both
were plain fmt.Errorf with no sentinel, so they fell into the generic
"provider failed" 502 path, telling a caller "try again" for a
request that will never fit no matter how many times it's retried.
decision/typesafe.go gains ErrRequestTooLarge (wrapped with %w onto
both pre-send refusals; existing message text unchanged, just
appended-to) alongside the existing ErrMaxTokensExceeded (the
provider's own after-the-fact refusal of a request fitState let
through) — both now map to a distinct 400 match_request_too_large.
New decision-package unit test builds a realistic 254-option Choice
(title + near-cap summary + trigger + slug per option, mirroring what
playbook-match could plausibly send) and confirms fitState refuses it
before the fake HTTP handler is ever called; server-side table test
covers both sentinels through the fake provider.
F3 (nit): the "none" option's description read "...listed above",
but Options is an unordered map — nothing puts them "above" it.
Reworded to "...any of the other options' procedures"; the pinned
exact-prompt test updated to match.
Also (lead's separate MCP-round instruction): decision.go's
maxChoiceOptions is now exported as MaxChoiceOptions, and the
server's over-limit refusal reads that constant instead of a copied
`255` literal, so the two ceilings cannot drift apart.
go test ./internal/decision/... ./internal/mcp/... ./internal/server/...
./cmd/pad/... ./internal/cli/... — all green. golangci-lint run ./...
— 0 issues. Full `go test ./...` run separately; final line in the
report.
Claude-Session: https://claude.ai/code/session_01ByMjAhvdWgbAWWeGE9jwWd
* chore(mcp): cut pad_playbook.match's tools/list text to the approved +292 variant (TASK-3120)
The lead's shape-(a) approval was for the measured "+292 documented"
prototype, not the fuller wording ae38353c actually shipped
(+1139 bytes) — the trim commit exists to pay for the approved
action text, not to fund arbitrary new prose. Cut the `text` param to
one sentence (the 404-fallback advice lives once, in the action
bullet, not repeated in the param) and the `match` action bullet down
to four lines at consistent bullet+2 continuation indentation,
matching the rest of padPlaybookToolDescription's style (the previous
wording had drifted to 20-space continuation on its last two lines).
tools/list: 53666 (post-trim baseline) -> 54023 (+357, close to the
~300 target — wording differs from the original prototype's exact
text, so an exact match isn't expected). initialize: unchanged at
24574 (this file's bullet is tools/list-only; instructions.md is a
separate file, untouched here).
Claude-Session: https://claude.ai/code/session_01ByMjAhvdWgbAWWeGE9jwWd
* fix(server): don't overclaim writeDecisionProviderError's timeout reason (TASK-3120)
The doc comment said "timeout" covered a cancelled caller request too,
but context.Canceled is a distinct error from context.DeadlineExceeded
and errors.Is(err, context.DeadlineExceeded) does not match it — that
case actually falls through to "upstream", contradicting what the
comment claimed. Narrowed the comment to deadline-only (the preferred
fix per review): a cancelled caller's own request also never reads
this response body, so the distinction was moot as well as wrong.
Code was already correct; only the comment overclaimed.
Claude-Session: https://claude.ai/code/session_01ByMjAhvdWgbAWWeGE9jwWd
* fix(cli,skill): pad playbook match needs -- before dash-led text (TASK-3120)
Codex round 2 finding on f31d5d64. Text starting with "-" (a dash-led
sentence, or literal text like "-ship it") is parsed as a flag by
cobra's default scanner, on both the CLI and stdio MCP (which shells
out to this same command). Pre-existing and class-wide — `pad item
search "-x"` fails the same way — so the general stdio fix across
every command is filed as BUG-3142 rather than done here; BuildCLIArgs
is untouched.
This command's own help now shows the workaround: an Example section
with `pad playbook match -- "-ship it"`. skills/pad/SKILL.md's rung-3
routing entry calls it the same way, with a one-line reason, and notes
the MCP form is unaffected (a structured `text` param, not a CLI
token).
New CLI test asserts BOTH directions: `playbook match "-ship it"`
(no --) fails at cobra's flag parser and never reaches the server;
`playbook match -- "-ship it"` succeeds and the server receives the
text with its leading dash intact. Ran both legs — see the test.
Claude-Session: https://claude.ai/code/session_01ByMjAhvdWgbAWWeGE9jwWd
* fix(skill): local stdio MCP is not exempt from the leading-dash flag parse (TASK-3120)
Codex round 4 finding. The rung-3 routing entry claimed the MCP form of
match was "unaffected" by BUG-3142, but that's only true for remote
/mcp, which keeps `text` a structured param. Local stdio MCP shells
out to `pad playbook match`, turning `text` into a CLI positional via
BuildCLIArgs (dispatch.go:435) — the same flag parser, the same bug.
Corrected: remote /mcp is safe as-is; local stdio MCP still hits it and
should prefer the CLI's `--` form when the text starts with `-`.
Claude-Session: https://claude.ai/code/session_01ByMjAhvdWgbAWWeGE9jwWd
* fix(cmdhelp): the example validator's flag scan must stop at "--" (TASK-3120)
CI red on #1429 (
|
||
|
|
cfda4463e8 |
feat(cmdhelp): tests + golden contract + drift validator (TASK-938) (#332)
* feat(cmdhelp): tests + golden contract + drift validator (TASK-938)
The verification layer that turns cmdhelp v0.1 from "implementation"
into "stable contract." Three categories of tests, all running in
`go test ./...`:
1. Schema validation (cmdhelp.schema.json as CI gate)
- internal/cmdhelp/schema_test.go — synthetic tree's emitted JSON
validates after static walk, after dynamic resolution, and after
a no-workspace fallback.
- cmd/pad/cmdhelp_real_test.go — the REAL pad cobra tree's emitted
JSON validates against the published schema. Future regressions
caught: types outside the closed vocabulary, non-numeric exit_code
keys, flag names violating propertyNames, malformed cmdhelp_version.
2. Drift-prevention contract (spec §6 / §11 Q5)
- internal/cmdhelp/example_validation.go — ValidateExamples walks
every example's `cmd` string, tokenizes with shellSplit, resolves
non-flag tokens against the live cobra tree, and asserts every
--flag exists on the resolved command (or any ancestor for
persistent / inherited flags). Negate-flag form (`--no-cache`)
is recognized via the negation rule from spec §5.3.
- shellSplit handles double/single quotes, backslash escape, and
stops at unquoted pipeline boundaries (|, ;, &, >, <) so the
validator only checks the first command in a pipeline.
- ValidateBoolArity asserts no bool flag appears in valued form
(--flag=value) anywhere in its examples (spec §5.3).
- cmd/pad/cmdhelp_real_test.go runs both validators against the
real pad tree as CI gates.
- Negative tests in internal/cmdhelp/example_validation_test.go
prove the validator catches: typo'd flag (--priorty), unknown
command path, valued-form bool flag.
3. Capabilities form equivalence (spec §8)
- cmd/pad/cmdhelp_real_test.go — both forms (help --capabilities
and --cmdhelp-capabilities fallback) produce byte-identical
output. Side-effect-free guarantee verified by passing garbage
args alongside the fallback flag.
Refactors enabling the tests:
- cmd/pad/main.go: extract newRootCmd() so tests can build the real
cobra tree without running it. main() body shrinks to two lines.
- cmd/pad/main.go: extract handleCmdhelpCapabilitiesFallback() so the
fallback's side-effect-free contract is directly assertable instead
of requiring a subprocess.
Parser improvements driven by real-pad-tree drift findings:
- parseExamplesFromLong: strip same-line `# comment` annotations so
`pad foo --bar # one item's attachments` doesn't pollute Examples.
stripCommentIndex is quote-aware (# inside "..." or '...' is literal).
- main.go (github cmd): the Long had annotations on example lines
separated only by spaces (no `#`), which was malformed input. Fixed
to use `#` separators — caught by the drift validator on first run.
New deps:
- github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 — Go JSON Schema
validator supporting draft 2020-12 (matches the cmdhelp schema's $schema).
New helpers in internal/cmdhelp/:
- FindAndCompileSchema(startDir) walks up to locate
schema/cmdhelp.schema.json and returns a compiled schema. Reusable
by any consumer that wants to validate cmdhelp documents.
End-to-end on real binary:
- pad help --format json → 100 commands, schema-valid.
- All examples in pad's emitted output resolve against the live tree
(zero drift findings).
- pad help --capabilities byte-identical to pad --cmdhelp-capabilities.
- Adding a typo'd flag in any cobra Long block in cmd/pad MUST break
TestRealPadTree_ExampleDriftValidator. Verified by the negative
TestValidateExamples_DetectsTypoFlag.
make check clean. All 53 cmdhelp + cmd/pad tests pass.
Parent: PLAN-930.
* fix(cmdhelp): pass full token stream to cobra.Find per Codex review (round 1)
Codex round 1 caught: ValidateExamples stopped collecting the command
path at the first flag, so an example like
pad --workspace foo item create task --priority high
resolved to root, not `item create`. That meant `--priority` was
checked against root's flag set (where it doesn't exist) — false
positive — AND the validator silently missed any command-path drift
after a leading root flag.
Cobra's own Find walks the full token stream and uses each command's
flag definitions to skip flag/value pairs while matching subcommand
names. Pass tokens[1:] directly to root.Find — let cobra handle the
interleaving correctly.
New test:
- TestValidateExamples_FlagBeforeSubcommandResolvesToCorrectTarget —
flag-before-subcommand resolves to the leaf and accepts leaf flags.
|
||
|
|
76c9d5aae5 |
feat(cmdhelp): parse Examples blocks from cobra Long as fallback (TASK-939) (#331)
The original TASK-939 ask was to migrate every cobra command's
"Examples:" block from Long into the dedicated Example field. Pad has
102 cobra commands; manually migrating each is a hundreds-of-lines
change with high regression risk and zero user-visible improvement
(cobra renders "Examples:" sections in Long identically to the Example
field — the difference is only machine-readability).
Higher-leverage approach: enrich the cmdhelp emitter to fall back to
parsing Long when the Example field is empty. One small, testable change
in internal/cmdhelp unlocks examples in cmdhelp output for every command
that already has an "Examples:" block — without touching any of the 102
command sites. Structural migration becomes optional polish (HT-941).
Implementation:
- internal/cmdhelp/json.go gains parseExamplesFromLong: locate a
stand-alone "Examples:" / "Example:" header, collect indented
invocation lines until blank-then-prose or end. Comment lines (`#`)
are dropped from the block. The header regex is anchored to a line
on its own (`^\s*Examples?:\s*$`) so prose containing the word
"Examples" doesn't trigger the fallback.
- buildCommand() prefers cmd.Example when set; falls back to
parseExamplesFromLong(cmd.Long) when Example is empty. Tests assert
precedence so future migrations to the Example field win cleanly.
- cmd/pad/main.go: completion command gets a dedicated Example field
(one of the few that didn't have an "Examples:" block at all). Demos
the migration pattern HT-941 will sweep across the rest.
Result on the real binary:
pad help --format json → before: 0/100 commands have examples
after: 24/100 commands have examples
pad help item create --format json → 4 examples (vs 0 before)
pad help completion --format json → 4 examples (from Example field)
The remaining ~76 commands genuinely lack an Examples: block in Long
(or are group commands that don't need examples). HT-941 captures the
sweep work needed to get those to 100%.
Tests:
- 7 new tests for parseExamplesFromLong in internal/cmdhelp/json_test.go:
basic block extraction, no-header (Usage: != Examples:), variant
headers (singular/plural, indented), empty/malformed inputs, stops
at unindented prose, drops comment lines.
- 2 new end-to-end tests via Build():
- falls back to Long when Example is empty
- prefers Example field when both set (precedence)
- All 39 existing cmdhelp tests + 16 routing tests still pass.
- make check clean.
Follow-up: HT-941 ("Migrate cobra Long Examples blocks to dedicated
Example fields") captures the structural sweep — broken into per-group
PRs (auth/*, agent/*, server/* etc.) so it can be done incrementally
without blocking PLAN-930.
Parent: PLAN-930.
|
||
|
|
0439c1bf3d |
feat(cmdhelp): --capabilities discovery flag + --cmdhelp-capabilities fallback (TASK-937) (#330)
Implements the cmdhelp v0.1 §8 capability bit so wrappers can detect support without trial and error. - pad help --capabilities → cmdhelp/0.1: text, md, json, llm - pad --cmdhelp-capabilities → same line (spec §8 fallback form) Both forms: - Single line on stdout (terminated by newline only). - Side-effect-free: no logging, no network, no config writes, no auth challenge. Verified by running with no workspace context (cwd /tmp, no auth) — still emits the line and exits 0. - Exit 0 on success. - Format: cmdhelp/<MAJOR>.<MINOR>: <comma-separated formats>. Why both forms: Spec §8 lists `<cmd> help --capabilities` as preferred and `<cmd> --cmdhelp-capabilities` as a fallback for CLIs whose `help` subcommand is overloaded. Pad's `help` is not overloaded, but supporting both forms costs nothing and lets wrappers and harnesses choose whichever convention they prefer — TASK-938 will assert equivalence between them. The fallback is handled in main() before cobra parsing so it really is side-effect-free: it doesn't even reach config.Load() or the detect-workspace path. A simple os.Args scan + early return. Files: - internal/cmdhelp/json.go — new CapabilityLine(formats) helper that produces the spec-format string. Caller passes the format set so different binaries can advertise different surfaces; the helper preserves caller order (spec §8 says order isn't significant). - cmd/pad/help_cmdhelp.go — adds --capabilities to helpCmd; new padCmdhelpFormats constant ["text","md","json","llm"]; short-circuit in RunE before any other logic runs. - cmd/pad/main.go — pre-args scan handles --cmdhelp-capabilities fallback before rootCmd.Execute(). Tests: - TestCapabilityLine_FormatExact — exact string match. - TestCapabilityLine_HonorsCallerOrderAndSet — preserves caller order. - TestHelpCmd_CapabilitiesExactString — exact-byte assertion on the output of `padtest help --capabilities` including the trailing newline. - TestHelpCmd_CapabilitiesShortCircuits — verifies --capabilities wins over --format / --depth / extra args (spec §8 side-effect rule). - All 35 existing cmdhelp tests + 14 routing tests still pass. - make check clean. End-to-end on real binary: pad help --capabilities → "cmdhelp/0.1: text, md, json, llm" exit 0 pad --cmdhelp-capabilities → same cd /tmp && pad help --capabilities → still works, no auth needed pad help item --capabilities --format json --depth 0 → short-circuits Parent: PLAN-930. |
||
|
|
e6fd25322e |
feat(cmdhelp): dynamic enum resolution + workspace context (TASK-936) (#329)
* feat(cmdhelp): dynamic enum resolution + workspace context (TASK-936)
The killer differentiator from the cmdhelp v0.1 spec — splice live
workspace facts into help output so an LLM asking "what collections
exist?" gets the real answer rather than a generic "any string".
Files:
- internal/cmdhelp/dynamic.go (new) — Resolver type with Apply method.
ArgEnumSources / FlagEnumSources map names to enum_source identifiers;
Sources maps enum_source to a fetcher func. Apply walks the Document,
stamps enum_source on matching args/flags, populates Enum from the
fetcher, and sets doc.Context.Workspace. Per-Apply caching keeps each
source func to ≤1 invocation regardless of how many commands need it.
- internal/cmdhelp/json.go — added Options.Resolver; Build calls
Resolver.Apply after the static walk, so callers that inspect Build
output as either pre- or post-resolution still work.
- cmd/pad/help_cmdhelp.go — newDynamicResolver constructs a Resolver
bound to the runtime: workspace from DetectWorkspace, server URL from
config, three sources (collections, roles, members). Returns nil when
no workspace is detected so help still works outside any workspace.
cmdhelpOptions takes target so Binary derives from root.Name() instead
of hardcoded "pad" (preserves test-tree bindings for synthetic roots).
Pad-side bindings (matches `pad item create --help`'s existing context):
arg collection → dynamic:pad collection list
flag role → dynamic:pad role list
flag assign → dynamic:pad workspace members
End-to-end on the real binary (inside docapp workspace):
pad help item create --format json
→ args[0].collection: type=enum, enum=[ideas,conventions,...,roadmap],
enum_source="dynamic:pad collection list"
→ flags.role: enum=[planner,implementer,reviewer]
→ flags.assign: enum=[dave]
→ context.workspace="docapp"
pad help --format md → "## Workspace context\n- workspace: `docapp`"
Outside any workspace (cd /tmp; pad help item create --format json):
→ collection arg: type=string, no enum, no enum_source (graceful fallback)
→ context: null
→ output still validates against schema/cmdhelp.schema.json
Fail-safe behavior:
- newDynamicResolver returns nil on any config/detection error → static doc.
- Per-source fetcher errors are caught inside Apply → enum_source still
announced on the binding arg/flag, but Enum is left empty. The help
command MUST NOT fail because dynamic facts can't be fetched.
- Existing Enum values from alternation/ValidArgs are preserved
(resolver only fills the gap, never overwrites authoritative spec).
Tests:
- 10 dynamic-resolver tests in internal/cmdhelp/dynamic_test.go
covering: arg + flag enum population, context population, per-source
caching across multiple commands, graceful error handling, nil
resolver as no-op, existing-Enum preservation, global flag resolution,
unaffected commands left unchanged, end-to-end via Build().
- All 24 prior cmdhelp tests + 12 routing tests still green.
- make check clean (lint + go test + web build).
Out of scope (deferred):
- --capabilities discovery flag — TASK-937.
- Schema-validate live output in CI — TASK-938.
- Audit pad's existing commands' Examples — TASK-939.
Parent: PLAN-930.
* fix(cmdhelp): scope --role / --assign bindings per-command per Codex review (round 1)
Codex round 1 on PR #329 caught a real semantic bug: globally binding
--role to dynamic:pad role list was wrong because pad has two
unrelated --role flags:
pad workspace invite --role workspace role: owner|editor|viewer
pad item create --role <slug> agent role slug
pad item update --role <slug> agent role slug
Globally announcing agent-role slugs as the values for `pad workspace
invite --role` would mislead consumers (LLMs would suggest "planner"
when "owner" is expected; tab-completion would offer the wrong set).
Fix:
- Resolver gains CommandArgBindings and CommandFlagBindings
(map[path]map[name]source) — scoped to a specific command path.
Per-command bindings win over wildcard ArgEnumSources/FlagEnumSources
when both match.
- Helper methods argSource(path,name) / flagSource(path,name) own the
precedence rule so both args and flags use it consistently.
- newDynamicResolver in cmd/pad keeps `<collection>` as a wildcard
ArgEnumSources (universal — every <collection> in pad means a pad
collection), but moves --role and --assign into CommandFlagBindings
scoped to "item create", "item update", and "item list". `pad
workspace invite --role` is intentionally left without a binding.
- A header comment in newDynamicResolver enumerates every --role /
--assign site in the CLI and which one each binding targets, so a
future reviewer adding a new flag can see the rule at a glance.
End-to-end on the real binary:
pad help item create --format json
→ flags.role: type=enum, enum=[planner,implementer,reviewer], enum_source=...
pad help workspace invite --format json
→ flags.role: type=string (untouched). ✓
New tests:
- TestResolver_Apply_PerCommandBindingScoped — explicitly mirrors the
Codex finding: two commands both have a `role` flag, only the bound
command resolves. workspace-invite-style isolation regression test.
- TestResolver_Apply_PerCommandWinsOverWildcard — precedence: when
both wildcard and per-command match, per-command wins.
- TestResolver_Apply_PerCommandArgBindings — same precedence rule
for positional args.
All 33 cmdhelp tests + 12 routing tests still green; make check clean.
* fix(cmdhelp): bind item list --role to agent roles per Codex review (round 2)
Codex round 2 caught that item list --role was still unbound — I missed
it in round 1's grep because the variable name is `&roleFilter` rather
than `&roleFlag`. Pad has 4 --role flags total:
pad workspace invite --role workspace role (NOT bound)
pad item create --role agent role slug (bound)
pad item update --role agent role slug (bound)
pad item list --role agent role filter (now bound)
Fix: extend CommandFlagBindings["item list"] to include the same
itemRoleAssign map as item create/update, so all three item subcommands
that reference an agent role get the dynamic binding.
Added a `grep` recipe in the comment so a future maintainer adding a
new --role / --assign site can find every existing one in one shot
(both `&roleFlag` and `&roleFilter` style declarations).
End-to-end on real binary (inside docapp workspace):
pad help item list --format json
→ flags.role: enum=[planner,implementer,reviewer], enum_source set ✓
→ flags.assign: enum=[dave], enum_source set ✓
make check clean.
|
||
|
|
5e93abe552 |
feat(cmdhelp): implement --format md emitter (TASK-935) (#328)
Adds internal/cmdhelp/md.go that renders the Document built in TASK-934
as markdown with the predictable section order from cmdhelp v0.1 §6.
Replaces the markdown stub in cmd/pad/help_cmdhelp.go so `pad help
--format md` (and the `--llm` alias) produce real output.
Section order per command (spec §6):
## `binary path`
summary / description (when distinct from summary)
### Synopsis — fenced usage line, reconstructed from args + flags
### Arguments — table with name | type | required | description
### Flags — table with flag | type | default | description
### Stdin — when Stdin.Accepted is true
### Examples — fenced bash blocks, drawn from same canonical
example set as JSON (spec §6 same-source rule)
### Output — text_template + json_schema_ref when populated
### Exit codes — table when ExitCodes is populated
### See also — bullet list of related command paths
Top-level YAML frontmatter:
cmdhelp_version, binary, version, generated (RFC3339, UTC).
Now is overridable via Options.Now for snapshot-test stability.
Top-level structure: `# binary` heading, summary, optional homepage,
optional `## Workspace context` (populated in TASK-936), `## Global flags`
table, then per-command sections sorted by path for determinism.
Synopsis reconstruction uses the structured Args from Build() (rather
than cobra.UseLine) so JSON and MD stay driven by the same parsed data
— the variadic `<ref>...` and alternation enums from TASK-934 carry
through naturally.
Pipes in flag/arg descriptions are escaped (`\|`) so they don't break
markdown table grids.
cmd/pad/help_cmdhelp.go: emitCmdhelpMarkdown stub replaced with a call
into cmdhelp.EmitMarkdown. --depth/--all threaded through MaxDepth
identically to the JSON path.
Tests:
- 15 markdown emitter tests in internal/cmdhelp/md_test.go covering
frontmatter (presence + timestamp injectability), per-command
section order, synopsis reconstruction (incl. variadic + alternation),
global-flag dedup, fenced-bash examples, hidden-thing exclusion,
deterministic ordering, Stdin/Output/ExitCodes/SeeAlso sections,
Workspace context, table-pipe escaping.
- TestHelpCmd_FormatMarkdownStubError replaced by
TestHelpCmd_FormatMarkdownEmits (asserts frontmatter + structural
markers for both md and llm).
- TestHelpCmd_FormatLLMAliasRoutesToMarkdown replaced by
TestHelpCmd_FormatLLMIsAliasForMD (asserts md and llm produce
byte-identical output modulo the timestamp).
End-to-end on the real binary:
- pad help --format md emits valid markdown with all sections.
- pad help --format llm produces byte-identical output (after
timestamp normalization).
- pad help item create --format md scopes correctly.
- make check clean.
Out of scope (deferred):
- Dynamic Workspace context population — TASK-936.
- --capabilities discovery flag — TASK-937.
- Schema-validation + golden-file tests in CI — TASK-938.
- Examples populated for all pad commands (still in cobra Long for now)
— TASK-939.
Parent: PLAN-930.
|
||
|
|
eecc683ab0 |
feat(cmdhelp): implement --format json emitter (TASK-934) (#327)
* feat(cmdhelp): implement --format json emitter (TASK-934) Adds internal/cmdhelp package that walks the cobra command tree and emits a cmdhelp v0.1 Document conforming to schema/cmdhelp.schema.json. Wires it into cmd/pad/help_cmdhelp.go so `pad help --format json` is no longer a stub. Files: - internal/cmdhelp/types.go — Document/Command/Arg/Flag/Stdin/Stdout/ ExitCode/Example structs mirroring the schema. ExitCode implements custom MarshalJSON for the string-or-object union (spec §5.2). - internal/cmdhelp/json.go — Build() walks target's subtree; EmitJSON() serializes to indented JSON. Type mapping covers pflag's full type space, including slice/array→repeatable. Hidden commands and flags filtered. Cobra's auto-installed --help flag suppressed. Zero-default values suppressed to keep output compact. argRE parses positional arg placeholders from cobra Use strings, filtering [flags]/[options]/ [command] cobra conventions. parseExamples splits cmd.Example by newline, drops blanks and # comments. MaxDepth maps to spec §4 semantics: 0 = subcommand list, 1 = + grandchildren, -1 = unlimited. - cmd/pad/help_cmdhelp.go — replaces emitCmdhelpJSON stub with a call into the package; threads --depth and --all through MaxDepth (--all overrides --depth). Verification: - 17 emitter tests in internal/cmdhelp/json_test.go covering envelope, global-flag emission, hidden-thing exclusion, positional arg parsing, pflag type mapping, zero-default suppression, example parsing, description-vs-summary, command-path key shape, MaxDepth semantics, target-subtree scoping, JSON validity, version pattern, ExitCode union marshaling, parseExamples filtering. - 11 cmd/pad routing tests still pass; TestHelpCmd_FormatJSONStubError replaced by TestHelpCmd_FormatJSONEmits which validates the structure. - End-to-end: `pad help --format json` on the real binary emits 100 commands across the full tree; output validates against schema/cmdhelp.schema.json (verified with python jsonschema). - `pad help item --format json` correctly limits output to 29 commands in the item subtree (homepage and other top-level metadata still populated from root). - `pad help --format json --depth 0` correctly emits 15 immediate children of root, no grandchildren. - make check clean (lint + go test + web build). Out of scope (deferred): - Examples: pad's existing commands embed examples in Long rather than using cobra's Example field. The emitter correctly reads Example; TASK-939 will normalize the pad-side commands to populate it. - Dynamic enum injection (workspace-aware enums): TASK-936. - --capabilities discovery flag: TASK-937. - Schema-validation of live output in CI: TASK-938. Parent: PLAN-930. * fix(cmdhelp): handle alternation, variadic, and ValidArgs in Use parser per Codex review (round 1) Codex round 1 on PR #327 flagged that parseArgs missed two real cobra Use-string idioms in pad's command tree: 1. `completion [bash|zsh|fish|powershell]` — alternation in brackets. The old regex only allowed `[a-zA-Z0-9_./-]+` inside brackets, so the `|` made the whole token unmatched and the shell arg disappeared from the emitted JSON. Consumers asking "what does completion take?" got nothing. 2. `item bulk-update [--status X] <ref>...` — variadic ellipsis. Old regex didn't capture trailing `...`, so the arg was emitted but without `repeatable: true`. Consumers couldn't tell that <ref> may be passed multiple times. Fixes: - argRE now allows full-bracket content (`[^<>]+` / `[^\[\]]+`) and captures an optional trailing `...` group. - parseArgs takes *cobra.Command (not just Use string) so it can read cmd.ValidArgs and attach those values as the first arg's enum when set. This covers `Use: "completion [shell]"` + `ValidArgs: [...]` where the allowed values only live on the cobra struct. - Alternation `<a|b|c>` / `[a|b|c]` produces an enum-typed arg with the values as `Enum`. When Use carries no semantic name (only the alternation), the arg name is synthesized as "value". - New validArgName check rejects embedded flag-like fragments such as `[--status X]` and prose with whitespace/punctuation that the broader regex would otherwise capture from idiosyncratic Use strings. - ValidArgs entries strip cobra's tab-separated completion descriptions before becoming enum values. New tests: - TestBuild_VariadicArgsRepeatable — `<ref>...` → repeatable=true. - TestBuild_AlternationProducesEnum — `[bash|zsh|fish|powershell]` → enum with values in source order. - TestBuild_ValidArgsFillsEnumOnNamedArg — Use says `[shell]`, ValidArgs carries the values → enum-typed arg named `shell`. - TestBuild_EmbeddedFlagFragmentsFiltered — `[--status X]` does not leak as a positional arg. Verified on the real binary: - `pad help completion --format json` now emits the shell enum. - `pad help item bulk-update --format json` now marks <ref> repeatable. - `pad help --format json` still validates against the schema (100 cmds). - `make check` clean. |