- schemaPublishedShortcutCount: 461→468 after merging aisearch/contact/live
shortcuts from upstream/main
- Fix P2: datasource-update --field-ids doc says "不传时同步全部字段" but
actual behavior keeps existing field config; fixed in usage guide and
reference
Rebuild the exact-entry reviewedCompatibilityExceptions carve-out in the
base-owned schema-compat checker for the three destructive batch-remove
tools whose confirmation PR #1085 tightens from not_required to
user_required (doc/doc.remove_permission, drive/drive.permission_remove,
wiki/wiki.remove_member). Because the compatibility gate builds its
checker from the PR merge-base, this carve-out has to land on main before
PR #1085 can pass; the entry set is exact (tool + field + old -> new), so
any other confirmation drift, including weakening a reviewed tool back to
not_required, still fails.
Address the P1 review finding on PR #1085: --members lets one call remove
up to 30 USER/DEPT/CONVERSATION/TAG members, where departments, chats,
and role groups can indirectly affect many more users, yet the remove
branches called the MCP tool right after argument parsing with Safety
confirmation=not_required.
- drive permission remove, doc permission remove, and wiki member remove
now declare confirmation=user_required. DeclareLeafMetadata installs
the ConfirmSafety gate automatically (deferred to the first
deps.Caller.CallTool so flag validation still fails first), so an
unconfirmed invocation exits with the typed confirmation_required
error and performs zero MCP calls; --yes, an interactive yes, or
--dry-run previews remain the supported paths.
- Pass framework confirmation errors through WrapErrorWithOperation
verbatim (new apperrors.IsConfirmationRequired). Text classification
misrouted them: command paths containing "permission" (drive/doc
permission remove) were re-reported as AUTH_PERMISSION_DENIED while
other paths (wiki member remove) lost their reason and degraded to
UNCLASSIFIED.
- Tests: TestPermissionMemberRemoveRequiresConfirmationBeforeToolCall
covers all three entry points for both --members and legacy --users —
unconfirmed rejects with zero MCP calls, --yes dispatches exactly one
call with the complete precise arguments, --dry-run previews without
calls. Existing remove tests inject root --yes for the assembly
assertions; blank --users still fails validation before confirmation.
Address the P2 review finding on PR #1085: NO_PERMISSION is a generic
code name also returned by non-document tools — attendance
get-self-setting (bossAttendStatNotify) and event-subscription attempts
have both been observed returning it — so keying drive permission
apply-* guidance on it would mislead those products, defeating the goal
of the P1 scoping fix. Only the drive-specific forbidden.* domain codes
(forbidden.no.auth / forbidden.accessDenied) and the role-threshold
message wording remain document signals; a bare NO_PERMISSION still
classifies as AUTH_PERMISSION_DENIED but now keeps the product-neutral
suggestion, and NO_PERMISSION combined with document wording still gets
apply guidance.
Add regression tests for the non-document NO_PERMISSION case and update
the changelog fragment; changed-code coverage stays at 100%.
Address the two P1 review findings on PR #1085:
- Permission suggestions: the drive permission apply-* guidance is now
limited to document/wiki-specific errors (node access codes
NO_PERMISSION / forbidden.no.auth / forbidden.accessDenied and the
role-threshold wording). Permission failures from other products keep
their product-specific suggestion (e.g. the mail mailbox hint) or fall
back to a product-neutral hint instead of being told to run document
permission commands that cannot fix their problem.
- Null rendering: the null->{} adaptation is limited to the four tools
with a confirmed empty-response-means-success contract
(update_permission / remove_permission / update_member /
remove_member). Every other tool keeps its raw null output so the
shared machine-output contract stays unchanged.
Update tests and the changelog fragment accordingly; changed-code
coverage stays at 100%.
The server rejects the legacy maxResults path; the CLI now validates
--limit (1-50) and sends it as pageSize at runtime. Schema-compat
rejects a non-empty property redirect (maxResults -> pageSize), so
declare --limit as a CLI pagination input via the reviewed mapping
exclusion ledger (property omitted, provenance
reviewed_mapping_exclusion) on doc.list_permission,
drive.list_permission, and wiki.list_member.
- --notify now defaults to false and is omitted from the server request
unless passed explicitly (help updated accordingly)
- forbidden.accessDenied / permission-denied bodies classify as
AUTH_PERMISSION_DENIED with apply-permission guidance
- user/member validation failures intercepted before RESOURCE_NOT_FOUND
with --members corpId suggestion
- business error display appends backend code/logId for traceability;
literal null tool responses render as {}
- drive/doc permission list + wiki member list declare cursor pagination
(next-token) in Contract; cobra.NoArgs hardening on permission leaves
- cross-platform coverage tests and release fragments updated
- Add trimNonEmpty for --table-ids in +datasource-sync and --task-ids in
+datasource-sync-status, matching the existing field-ids pattern
- Add 4 test cases: whitespace-only rejection and trim-through for both
- Fix usage guide: typical workflow and notes no longer equate result
with processCode; correctly describe result as JSON to parse for
approvals[].processCode/name/iconUrl/url
--field-ids is declared as FlagStringSlice, but DatasourceCreate and
DatasourceUpdate previously called rt.Str to check whether the flag
was empty. RuntimeContext.Str delegates to cobra's GetString, which
returns an empty string on slice-typed flags, so the empty-value
guard rejected every explicit --field-ids input and the downstream
MCP tool never received fieldIds.
Switch to rt.StrSlice, sanitize through a new trimNonEmpty helper
(drop whitespace-only / empty entries) and pass the cleaned slice
to MCP. Add success-passthrough tests for both create and update,
plus a whitespace-only rejection case, and enhance the mock caller
to record MCP arguments so fieldIds can be asserted.
- Update --field-ids description in create/update shortcuts and the
helper-layer datasource update to clarify that omitting the flag
keeps existing config (create defaults to all fields), matching the
actual update overwrite semantics.
- Rename datasource shortcut coverage tests to the
TestCrossPlatformCoverage* prefix so they are picked up by the
macOS platform coverage gate.
Align shortcut layer validation with helper layer to prevent empty slices
from being sent to MCP, which could clear sync field selection due to
datasource update's overwrite semantics.
- Add empty string checks for --field-ids in both create and update shortcuts
- Add empty string checks for --auto-sync-setting in both create and update shortcuts
- Add regression tests verifying MCP is not called when empty values are rejected
- Both public entry points now have consistent validation behavior
Fixes P1 auto-CR issue for empty flag bypass vulnerability.
Make +datasource-sync-status consistent across shortcut and native
commands: --task-ids is now required, descriptions focus on querying
by taskId, and optional/IDLE semantics are removed. Update tests,
usage guide, reference doc, and SKILL description accordingly.
Native datasource create/update now expose --field-ids and
--auto-sync-setting, matching the shortcut-layer capabilities:
- flags registered on both commands
- Contract Parameters updated
- values mapped to MCP tool args
- JSON validation for --auto-sync-setting
- no-change update guard now counts the new flags
Also fixes the missing required name in the usage-guide update example.
+datasource-update now requires at least one mutable option
(--source-config, --auto, --field-ids, or --auto-sync-setting)
before calling update_datasource_config, preventing accidental
sync triggers. The native datasource update command enforces the
same guard for its supported flags. Also adds the required name
field to the +datasource-get-fields doc example.
Omitting --auto on +datasource-update previously sent auto=false to
MCP, silently disabling auto-sync for existing datasources. Now auto
is only included in tool args when the flag is explicitly provided,
so --auto=true and --auto=false work while omission preserves the
existing setting. Updated flag descriptions and added tests.
MCP requires the auto field in create_datasource / update_datasource_config
requests. Previously CLI only sent it when --auto was explicitly changed,
causing failures when users omitted the flag. Now both shortcut and helper
layers always include auto=false by default.
Also update flag descriptions and docs to clarify that the field is always
sent downstream, and add test assertions for the default-false behavior.
- docs/datasource-usage-guide.md: clarify that list-sources result is a
JSON string containing approvals[]; add missing --auto-sync-setting
parameter table rows and a dedicated autoSyncSetting format section
using the correct scheduled/daily/weekly/monthly enums.
- skills/references/aitable/aitable-datasource.md: fix autoSyncSetting
enums (schedule/day/week/month -> scheduled/daily/weekly/monthly) and
update the create example accordingly.
The OA approval source-config contract requires processCode, name,
iconUrl, and url to be passed through unchanged from +datasource-list-sources.
Published examples for +datasource-create, +datasource-update, and
+datasource-get-fields were missing `name`, and the usage guide marked it
as optional. Fix all examples in the shortcut layer, helper layer, and
docs; update flag descriptions to mention name; and add a contract test
that validates every delivered example's source-config JSON contains the
required members.
Both the shortcut (+datasource-sync, +datasource-sync-status) and
helper (datasource sync, datasource sync-status) layers now validate
that table-ids contains 1-5 IDs and task-ids contains at most 5 IDs
before calling MCP, matching the declared contract.
ValidateRequiredFlags calls GetString which returns empty for
StringSlice flags, causing the examples test to report --table-ids
as missing. Switch to String + parseCSVValues to match the codebase
convention used by record-ids and other comma-separated flags.
- Add 7 datasource leaf commands to internal/helpers/aitable.go so
coverage test can find tool name literals (fixes TestAllShortcutsAssemble)
- Add 7 entries to semantic_catalog_aitable.json and update catalog count
from 93 to 100 (fixes TestCrossPlatformCoverageAITableSemanticCatalog)
- Update publicShortcutCount/schemaPublishedShortcutCount/publiclyDelivered
from 422/447/422 to 429/454/429 (fixes TestDeliverySchemaCoversOrExactly)
- Fix Contract.Selection.AgentSummary and UseWhen[0] in datasource.go to
match Description and Intent exactly as required by schema contract test
- Fix autoSyncSetting enum: scheduled/daily/weekly/monthly; mark
selectedMonthDays/selectedWeekdays as required for monthly/weekly
- Remove splitParentTableField from --source-config user-settable fields;
add note that splitParentTableField/enableDataSyncOaDetailList are
internal downstream fields not to be passed
- Prepend sync-is-fire-and-forget notice to DatasourceSync descriptions
- Remove --conflict-strategy flag (syncConflictStrategy not in MCP schema)
- Add --auto-sync-setting flag to DatasourceCreate (was only in Execute, not in Flags)
- Expand DatasourceSync description: add 文档链接, errorCode=4014 幂等冲突, 非数据源表参数错误
- Simplify DatasourceGetFields description: remove field property enumeration to match snapshot
- Add --auto-sync-setting flag (JSON string) to +datasource-create and
+datasource-update, validated and passed through as raw string.
- Update +datasource-update --source-config desc to reflect full
replacement semantics ("传入时整体覆盖") and spell out required /
optional fields with defaults.
- Append "仅支持 OA 审批数据源 (datasourceType=OA)" to
+datasource-get-config description.
- Simplify +datasource-list-sources / +datasource-get-fields
descriptions to concise Chinese aligned with snapshot wording.
- Update SKILL.md shortcuts table and add datasource usage guide.
Add 5 data source sync management shortcuts to the aitable service:
- +datasource-create (create_datasource): create sync config + first sync
- +datasource-update (update_datasource_config): update existing sync config
- +datasource-sync (run_datasource_sync): trigger manual sync (max 5 tables)
- +datasource-sync-status (get_datasource_sync_status): query sync task status
- +datasource-get-config (get_datasource_config): get sync config details
Each shortcut declares a full Contract (Identity/Interface/Selection),
Safety, Flags, and Execute that calls rt.CallMCPData on the "aitable"
MCP server. datasource-type is passed through without CLI enum check;
source-config is validated as a JSON object via parseJSONObject.
Sync the permission CRUD overhaul from the internal CLI (MR 28965577):
- drive/doc permission list and wiki member list now accept --next-token
to follow the server cursor (totalCount/hasMore/nextToken); --limit maps
to pageSize capped at 50 instead of the rejected maxResults=200 path
(fixes#1065)
- permission add/update/remove and wiki member add/update/remove accept a
--members JSON array (USER/DEPT/CONVERSATION/TAG grantee types, each with
its own roleId) with optional --notify; legacy --users/--role stays
- cursor/page-token hidden cross-product aliases now resolve to next-token
- regenerate param_aliases_generated.go; wiki member list override no
longer blocks cursor
- update mono/multi skill references and add change fragment
A stamp-shaped directory name is not ownership proof: pruneSkillBackups
counted and RemoveAll'd any 20260819-120000-shaped entry under
~/.dws/skill-backups, so a user or tool that created such a directory
lost its contents once DWS held five backups, and the Go backup path
(MkdirAll) adopted a same-named foreign root outright. The PowerShell
installers already implemented the correct contract; every other
surface now matches it.
Go stamps a freshly created root with the exact marker bytes the
install scripts write (.dws-skill-backup = "dws skill backup v1")
before any payload moves in, claims the root with mkdir so an existing
unproven root bumps to a collision suffix instead of being adopted,
and prunes only roots whose marker verifies — unmarked or wrongly
worded stamp-shaped directories are foreign data, preserved and never
counted against the keep limit. The shell installers (install.sh,
install-skills.sh, install-event.sh, install-devapp.sh) and the npm
installer apply the same rule in their backup collision loops, with
roots recorded as created by the running process exempt from marker
re-verification so a mid-run marker permission failure still reuses
this run's own root and keeps the sibling payload intact.
Regression tests cover every surface: pruning an unmarked/wrongly
marked stamp-shaped directory alongside marked ones, refusing to adopt
a foreign root (payload moves to a suffixed root, foreign data and its
nonexistent marker untouched), same-stamp reuse of a proven root, and
marker-write failure cleaning the empty fresh root.
copy_tree published staged children with mv, which replaces a
concurrently created same-name directory (POSIX rename succeeds over an
empty target) and whose rollback moved every dest child back — including
a concurrent writer's different-named entries — before deleting the
staging tree. Children now publish through kernel-level no-clobber
primitives (mkdir claim + recursion for directories with the recorded
mode restored, ln for regular files, ln -s for symlinks), a manifest
records exactly what this transaction published, the rollback retracts
only those entries in reverse order, and each level re-counts the
destination so a foreign different-named entry aborts the publish with
the destination retained. Read-only staged directories (0555 skill
trees) are made owner-writable for the move; the backup restore uses the
same discipline so a concurrent writer is refused without partially
draining the backup.
Regression tests cover both scripts: a concurrently created same-name
empty child directory and a different-named foreign entry mid-publish
are retained with the original backup kept; both fail against the
previous mv-based implementation.
The platform coverage gates execute only TestCrossPlatformCoverage-named
tests, so the child-move error and dispatch branches that the full local
suite covered incidentally were reported as uncovered changed code on
Windows (96.78% vs the 100% target). Adds a seam-driven edge suite for
the child-move fallback — source/claim/child stat and read failures,
per-child link and symlink collisions and publish failures, rollback
rename failure, foreign-entry abort, mode-restore failure, source shell
removal failure, nested-directory and simulated-symlink children, and
post-rename content drift — plus the retained-destination notice for a
dependent uncertain target in skill setup. The POSIX file identity impl
now consults the lstat seam so its degradation branches are coverable
the same way. Verified against the gate's own changed-line computation:
zero uncovered changed statements in internal/upgrade.
The publish-confirmation and tunneled-replacement tests physically
removed and reseeded the destination to simulate a concurrent swap. On
NTFS the recreation can immediately reuse the freed MFT record, making
the file ID (volume serial + file index) compare equal and the proof
pass against a replaced object — the Windows coverage gate observed the
confirmation falling through to the fingerprint branch instead of the
identity branch. Both tests now force the replacement through the two
primitives the platform proof consults (os.SameFile on Unix, the file-ID
seam on Windows), matching the technique the tunneled-rollback case
already used for Unix inode recycling.
skill_publication_identity_linux_test.go pinned the remote line's
Statx/birth-time identity design (skillPathStatx seam); the merged head
proves ownership with dev:ino plus the fingerprint backstop instead, so
the test no longer compiles on Linux. Caught by CI's Linux lint job,
which builds what macOS-local vet skips behind the linux build tag.
- Reject --request payloads with a missing, empty, or non-string
processCode; the backend answers a bad processCode with success:true
and an empty list, so validate client-side like startTime
- Add regression cases to keep changed-code coverage at 100%
Reconciles the two parallel evolutions of PR #996 with this session's
publication design as authoritative:
- internal/upgrade, internal/app: ours — mkdir-claim identity witness
(dev:ino on POSIX, volume file ID on Windows), three-state ownership,
ErrSkillPathPublicationUncertain, copy-fallback short-circuits. Drops
the remote line's xattr publication-mark design and its six follow-up
fixes (retract contracts, Statx token); skill_publication_mark_*.go
removed accordingly.
- scripts/, build/npm/, test/scripts/, docs/rfc: theirs — same replayed
install hardening plus main's evolution and the npm no-clobber child
moves; no xattr dependency, consistent with the claim model.
- .changes: their npm/Shell/PowerShell narrative with the Go-design
sentences rewritten for the uncertain-publication contract.
Verified: go build, go vet (tests compiled), gofmt, and package tests
for internal/upgrade, internal/app, test/scripts all green on this tree.
The mkdir->rename->remove->rename directory fallback had a TOCTOU window
between the second remove and the second rename: a concurrent writer
creating an entry at the destination was silently clobbered. The fallback
now claims the destination once with mkdir and never unlinks it: the
fast-path rename publishes over the claim (Linux), and platforms that
refuse directory renames (macOS, Windows) move the staged children into
the claim through atomic no-clobber primitives (mkdir/os.Link/os.Symlink),
consuming the emptied source shell on success.
renameSkillPathNoReplace now returns the mkdir-claim identity captured by
the child-move path. PublishSkillPathNoReplace uses it as a three-state
ownership witness: the atomic/fast paths keep the staged-inode proof, the
child-move path proves dest is still the mkdir claim, and a mismatch
reports the new ErrSkillPathPublicationUncertain sentinel with the
destination retained. The witness is real on POSIX now: darwin and linux
report the dev:ino file identity instead of the empty no-op.
Upstream consumers honor the sentinel: the mono/multi upgrade copy
fallbacks no longer retry over an uncertain destination (the retry would
displace the concurrent writer's object), and skill setup reports the
retained destination instead of claiming a rollback.
Rewrites the fallback tests that pinned the removed remove-and-retry flow
and adds regression coverage: concurrent claim entries abort with the
destination retained, wholesale replacement after child-move reports the
uncertain sentinel, staged-set transactions pass the sentinel through,
and both copy fallbacks short-circuit (ablation-verified).
- Reject --request payloads missing startTime (documented required) so
endTime can no longer bypass validation when startTime is absent
- Align --request time ordering with simple mode: endTime must be
strictly after startTime
- Cover all five previously uncovered branches (pageSize absent,
startTime absent, malformed endTime, valid time pair, empty --start
flag) to reach 100% changed-code coverage
- Add dws oa approval list-by-admin leaf with simple flags and
advanced --request modes backed by get_process_instances_by_admin
- Send startTime/endTime as yyyy-MM-dd HH:mm:ss strings per the
2026-08 MCP contract update; ISO-8601 flag inputs auto-convert
- Enforce pageSize cap (20) and string time format/order client-side;
PreRunE reports flag-group violations in Chinese before Cobra's
built-in English validation
- Extend coverage tests and document the command in mono/multi OA
skill references
Windows coverage gate only runs TestCrossPlatformCoverage*, and the
xattr mark helpers are Unix-only. Inject seams so marked dest is
retracted on owned drift, left in place when the mark is gone, and
the helper error paths are exercised on every platform.
Linux overlayfs recycles device+inode, so SameFile and a lone inode
token treated a replacement as owned and retracted it. Stamp staged
inodes with an xattr mark, prove Linux/Darwin identity with birth
time, and make shell copied-set rollback check dest first with inode
plus child names.
Match the Go dest-first identity check so a concurrent replacement is
never moved into .rollback-*; only a post-quarantine mismatch is
restored with no-replace. Cover both races in the npm smoke suite.
Record dest on occupy and retract it when confirmation, verify, or
staging cleanup fails. Restore unmatched quarantine with a no-replace
publish. Event/devapp copy uses mkdir-claim; shell rollback claims dest
before delete. Release copy now says npm/PowerShell create junctions and
Go uses os.Symlink, with copy fallback when linking is unavailable.
Cross-filesystem Skill moves now record publication identity as soon
as the staging path is renamed onto dest. A later mode-restore, copy
verification, or staging-cleanup failure retracts that proven dest so
retries are not blocked by an untracked leftover. A failed retract
reports an uncertain state naming both retained locations.
The shell mono/multi set publishers staged each Skill directory and
published it with a plain mv after the backup; anything another process
created at the destination between the backup and the move was silently
replaced, and restore_multi_skill_set then blind-deleted manifest paths,
so a concurrently replaced object could also be destroyed during
rollback. Publish through an atomic mkdir claim instead — EEXIST refuses
any occupant, staged children move into the claim one by one, and a
failed child move relocates them and removes only the claim. The
published manifest now records <dest>:<inode>, and rollback deletes a
destination only when its inode still matches the publication, skipping
concurrently replaced paths with a warning. Also fixes a latent
unbound-variable expansion where a shell variable was followed directly
by a full-width parenthesis in a message. Regression tests publish a
first Skill, replace it with a foreign directory, fail the second
publication, and assert rollback retains the foreign object untouched
while restoring the rest from backups.
The RFC section on filesystems that reject the atomic no-replace rename
still described the retired existence-check-plus-plain-rename fallback
and its accepted race window. The implementation (and the npm and shell
surfaces) claim the destination with mkdir or a hard link — or create
the link directly at the destination — and never release the claim mid
transaction, so a concurrently created object is refused rather than
overwritten. Record that contract and its only relaxed property (child
moves are not all-or-nothing visible) so future maintainers do not
port the racy description back into code.
- add success and failure outcomes for three attachment commands
- define business data schemas and mark downloadUri as sensitive
- migrate attachment commands to unified result output
- verify compact and full Schema result projections
- cover success, malformed response, and tool error paths
to #666
The POSIX shell installers staged shared Skill links and published them
with mv after an existence check; a file or symlink another process
created at the destination between the check and the move was silently
replaced, and the inode confirmation could not detect the loss. Publish
by creating each link directly at its destination instead — symlink(2)
refuses an occupied path with EEXIST, so the creation itself is the
atomic no-replace check. A directory that appears at the destination
turns ln -s into a container; the nested link is removed after an
identity check and the transaction rolls back, leaving the foreign
directory untouched. Applied to install.sh, install-skills.sh,
install-event.sh, and install-devapp.sh. Also covers the remaining
retraction branches of the Go shell-removal fallback so changed-code
coverage is complete. Regression tests inject a concurrent occupant at
the publish instant for regular-file and directory cases and assert the
foreign object and its contents stay completely unchanged.
On filesystems without atomic no-replace rename, the degraded
publication moves the source children into a fresh claim and leaves an
emptied source shell for the caller to remove once the move is
confirmed. If that removal failed, moveSkillPathRecoverably reported a
plain failure claiming both locations were preserved while the data
existed only at the destination, so backupAndRemoveSkillDir never
recorded the backup and the original path was left empty. Move the
children back into the shell and withdraw the destination instead; a
failed retraction reports the data location explicitly. Restores the
contract that a failed move keeps the source intact.
publishCanonicalLinkNoReplace checked the destination with lstat and
then published, leaving a window the comment claimed did not exist: on
Windows renameSync replaces a concurrent object outright (libuv passes
MOVEFILE_REPLACE_EXISTING), and on POSIX ln -P source target links INTO
a directory that appeared at the target, leaving a stray link inside
foreign data that the rollback list never recorded. Create the symlink
or junction directly at the destination instead — link creation fails
with EEXIST when anything occupies the path and never treats the target
as a container, so the publication itself is the atomic no-replace
check. Identity confirmation re-reads the live link before the
publication enters the rollback list. Covered by injected concurrent
creators at the publish instant on POSIX and simulated Windows,
asserting the foreign object and its contents stay completely
unchanged.
The mono and multi set copy publishers checked destination existence
with lstat and then called Node's rename, which replaces the target on
every platform (libuv passes MOVEFILE_REPLACE_EXISTING on Windows). A
file, symlink, or empty directory created between the check and the
rename was silently overwritten, and the identity confirmation could not
recover it because the publication record only proved the staged object
arrived. Claim the destination with mkdir — which fails with EEXIST if
anything occupies the path, so the claim itself is the existence check —
and move the staged children into the claim, restoring the source mode
on it. A failed child move relocates the children back and removes only
the claim. Covered for mono, multi, and simulated Windows, including an
injected concurrent creator at the claim instant.
The no-replace file fallback links the destination and then removes the
source. If the removal fails, the caller treats the publish as failed,
but no publication record exists to roll the new destination back, and
a backup restore would refuse the occupied path. Remove the destination
behind an identity check — only the proven linked object may be deleted
— and report when the retraction itself fails or the destination was
concurrently replaced.
The previous wrapper returned the first observed ID for both paths, so
expected == actual still held on Windows and the proof accepted the
swap. Return a distinct ID for the second probe.
The constant file-ID stub made both IDs equal, so the Windows proof
(expected == actual) accepted the publication and the subtest failed
there; Unix stayed green because its proof ignores the ID strings and
the swapped os.SameFile seam already forced the failure. Return the
first observed ID for both paths so staged and published identities
differ on every platform while real IDs still flow through the wrapper.
The physical same-content swap relied on the recreated destination
getting a fresh inode, but CI runners' ext4/overlayfs recycle inodes
eagerly, so the swap was undetectable on Linux and the subtest failed
there (while passing on macOS). Swap the same-file identity seam instead
so the confirmation's fast-path rejection contract is pinned on every
platform.
The four standalone installers pruned the oldest excess stamp
directories regardless of origin, so a migration retiring more than
five batches destroyed its own rollback material mid-run — the same
data loss already fixed for Go via the run-root registry and present
in install.js/install.ps1 as currentRunBackupRoots. Every installer now
records the stamp directories it creates and pruning only removes
earlier-run batches, which is what the changelog already promises.
The degraded directory publication claimed the destination with mkdir, then
— on platforms whose rename refuses to replace a directory (macOS refuses
even an empty target, verified empirically) — removed the claim and retried
a plain rename. Between the unlink and the retry a foreign directory could
appear at the destination and be silently overwritten, breaking the
no-replace contract the fallback exists to provide.
Hold the claim for the whole transaction instead: rename over the claim
where the platform permits it (Linux), otherwise move the source children
into the claim one by one. The destination is never unlinked, so a
concurrent creator can only ever lose the mkdir race; every child rename
targets a nonexistent path inside the empty claim, and a failed move
restores the children and removes only the claim.
The child move legitimately changes the publication's identity, which the
confirmation now handles: a rename that consumed the staged path is still
proven by identity, while a child move is proven by the pre-rename content
fingerprint. The emptied source shell doubles as the signal distinguishing
the two shapes; moveSkillPathRecoverably removes it to keep move semantics.
The backup stamp has second precision and pruning kept only the newest 5
stamps, so a canonical migration that retires copies across many Agent
roots deleted its own earlier backups mid-run. That silently voided the
reversibility guarantee the transaction depends on for rollback: a probe
retiring 8 paths lost 3 of them permanently.
Record every stamp directory this process creates, keyed by normalized
absolute path, and prune only the oldest foreign stamps.
Creating the staged symlink usually succeeds, so the link strategy really
fails at publish time: renameSkillPathNoReplace has no atomic no-clobber
primitive for a symlink source and refuses it whenever the kernel flag is
unavailable (NFS, FUSE, overlayfs). Gating the copy fallback on staging
alone therefore left every non-universal Agent unconfigured on exactly the
filesystems the fallback exists to support.
Retry the whole target transaction as a direct copy after a failure in any
phase, but only when the failed attempt fully restored the originals. The
converter also re-adds the replacement backups the link plan deliberately
skips for destinations already pointing at canonical, which a copy must
replace and no-replace publication would otherwise reject with EEXIST.
skillPathSameFileIdentityImpl on Windows always returns false and is
never reached through skillPathIdentityProven (which uses file IDs
exclusively). Add a direct seam call with synthetic os.FileInfo to
exercise the Windows return-false path and the Unix os.SameFile path
with nil Sys().
Restructure skillPathFileIdentityImpl to use nested if-err-nil with a
named return and skillPathIdentityProven to use a single expression.
Error conditions now fall through to the bare return instead of
occupying separate coverage blocks, eliminating 5 uncovered statements
that the Windows coverage gate flagged at 99.4193%.
Add FILE_FLAG_OPEN_REPARSE_POINT to the Windows CreateFile call in
skillPathFileIdentityImpl so symlinks are opened as reparse points
rather than followed to their target. Staged symlinks carry relative
targets computed for the final destination, which may not resolve from
the staging directory; following them caused CreateFile to fail,
yielding an empty file ID that rejected publication and broke canonical
skill layout migration on Windows.
Make skillPathSameFileIdentity a seam variable so the tunneled
replacement test can deterministically simulate the identity change on
Unix. On tmpfs (used by Linux CI runners), os.SameFile can return true
for a recreated file due to inode reuse, making the test flaky. On
Windows the swap is a no-op because skillPathIdentityProven compares
file IDs from GetFileInformationByHandle and ignores
skillPathSameFileIdentity.
NTFS file tunneling can restore the original creation time for a
recreated same-named object, which defeated the creation-time
incarnation check and allowed rollback to delete a concurrent
replacement. Replace the platform-specific identity pair with a single
skillPathIdentityProven function:
- Unix: delegates to os.SameFile (inode/dev), ignoring file ID strings
- Windows: compares VolumeSerialNumber:FileIndexHigh:FileIndexLow from
GetFileInformationByHandle, which uniquely identifies the file on the
volume for its lifetime and is unaffected by tunneling
When the file ID cannot be obtained at publish time, identity is not
proven and the auto-delete is refused. Add a regression test that
simulates tunneled creation time and verifies rollback still refuses
the concurrent replacement.
On Windows isNoReplaceRenameUnsupported always returns false, so the
fallback is never entered from the invalid-path test. Force the fallback
and swap skillPathLink to a non-EEXIST error to cover line 94 on all
platforms.
Add tests for mkdir non-EEXIST error, remove failure after rename
failure, first-rename-succeeds path (Linux behavior), retry-rename
path, and non-regular source safe-fail. All 24 changed executable
statements now covered on both macOS and Windows.
The fallback path for filesystems without RENAME_NOREPLACE/EXCL (NFS,
FUSE, overlayfs) used Lstat-then-Rename, which could overwrite a
concurrently created destination between the check and the rename.
Replace the TOCTOU-prone check with truly atomic no-clobber primitives:
- Directories: os.Mkdir atomically claims the destination (fails with
EEXIST if occupied). On Linux rename(2) replaces the empty dir
directly; on Darwin/Windows rename refuses existing dirs so the empty
dir is removed and the rename retried — any concurrent creation
between remove and rename is detected by the second rename failing.
- Files: os.Link atomically fails if the destination exists, then
os.Remove completes the move.
Add concurrent-creation test covering the mkdir→rename race window.
The !os.IsNotExist(statErr) branch in renameSkillPathNoReplace was
uncovered on Windows. Inject errNoReplaceRenameUnsupported for the
atomic rename and os.ErrPermission for skillPathLstat so the stat-error
path is exercised on every platform.
The smoke test creates temp home directories with .config/kimchi markers
for agent detection. On Linux CI runners XDG_CONFIG_HOME may point to the
runner's real config path, causing resolvedAgentTargets to look outside
the temp home. Unset it so detection resolves against the test's temp dir.
Restrict backup pruning to directories whose names match the DWS stamp
format (YYYYmmdd-HHMMSS with optional -N suffix) across all 8 installer
surfaces (Go, npm, 4 shell, 2 PowerShell). Unknown directories in
~/.dws/skill-backups are now preserved. Also fixes Windows coverage test
portability and covers the remaining macOS changed-code gap (retire
warning loop in runUpgrade).
A universal Agent whose obsolete private copy cannot be retired installs
nothing there, yet every entry point counted that retirement failure as an
install failure — aborting `npm install`, `dws skill setup`, and the shell
installers even when the canonical store and all links published correctly,
and skipping the skills-state write. Route retirement failures to a separate
warning path across all surfaces (Go upgrade + skill setup, npm, PowerShell,
install.sh, install-skills.sh, install-event.sh, install-devapp.sh).
Also:
- Add a checked-rename fallback for filesystems that reject the atomic
no-replace flag (NFS, FUSE, overlayfs); the no-clobber contract is kept and
the previously unsupported platforms build and work.
- PowerShell multi-mode links only bundle skills, never the shared canonical
store, so third-party/user skills are no longer fanned into every Agent root.
- Prune ~/.dws/skill-backups to the newest 5 on every surface; encode
HOME-relative backup names on PowerShell to preserve origin.
- Add simulated-win32 junction coverage and rewrite the tautological
no-replace test; remove dead code whose tests gave false coverage.
- Soften the overstated Windows ownership-proof comment (NTFS tunneling).
- extend the silent-rollback contract to install-event.sh
- static contract: Restore-MultiSkillSet removes published paths lexically
(section-scoped so identity-anchor refactors keep the guarantee) and link
staging dirs are cleaned via Remove-LinkStageRoot / Remove-DevLinkStageRoot
- install-event.sh integration test: an uninstallable agent target is
skipped loudly while later agents still receive links
- pwsh probe: Test-SamePhysicalSkillRoot must dereference junctions and
symlinks (junction idempotency asserted where junctions are creatable)
- install.ps1: remove published junctions lexically in Restore-MultiSkillSet
(Windows PowerShell 5.1 follows reparse points during Remove-Item -Recurse
and could delete canonical store contents); clean link staging dirs
lexically in Publish-CanonicalSkillLinks and Move-SkillPathRecoverably
- install.ps1: Test-SamePhysicalSkillRoot now dereferences junctions via
Get-PhysicalSkillPath (mirrors EvalSymlinks/realpathSync/cd -P), so reruns
recognize already-published junctions instead of backup churn
- install-event.sh: replace silent 'mv ... 2>/dev/null || true' rollback with
the loud backup-retained failure contract already enforced for devapp
- event/devapp sh+ps1: link→copy fallback and per-agent failures now degrade
per agent like install.sh (skip loudly, continue, report at the end)
instead of aborting mid-loop or swallowing errors
- tests: junction-lexical removal contract, event per-agent degrade
integration test, pwsh junction physical-root recognition + rerun
idempotency (no backup churn)
A failed canonical publish only failed the upgrade when
hasDependentSkillRoot reported a non-universal link target; that helper
explicitly skipped universal agents, which are exactly the direct consumers
of ~/.agents/skills. On a universal-only machine (e.g. only Codex
installed), UpgradeSkillLocations* returned a nil error with nothing
installed, contradicting the documented "canonical publication is
mandatory and fails the upgrade loudly" contract.
Canonical publish failures now return an error unconditionally in both the
mono and multi branches, and hasDependentSkillRoot is removed. The test
that pinned the old standalone-does-not-fail-fast behavior now asserts
error propagation in both modes.
The allowSystemApps gate (homeDir == systemHome) was effectively a no-op in
production: systemHome came from os.UserHomeDir, which honors the $HOME env
override just like homeDir, so the two were always equal and the gate never
fired when $HOME was overridden.
ResolveSystemHomeDir now prefers the OS user database (getpwuid on Unix),
which is independent of $HOME, falling back to $HOME only when the user record
cannot be resolved. Production behavior is unchanged (a real $HOME still
matches); an isolated/overridden HOME now correctly skips machine-wide
/Applications discovery for zcode/minimax. The app surface references the same
shared resolver.
This is the correct fix for the hermeticity concern (machine-wide state leaking
into an isolated HOME): there is no cross-surface production inconsistency to
port — script installers always operate on the real user HOME in practice, so
they need no gate.
skillPathSameFileIdentityImpl on Windows always returns false and is
never reached through skillPathIdentityProven (which uses file IDs
exclusively). Add a direct seam call with synthetic os.FileInfo to
exercise the Windows return-false path and the Unix os.SameFile path
with nil Sys().
Restructure skillPathFileIdentityImpl to use nested if-err-nil with a
named return and skillPathIdentityProven to use a single expression.
Error conditions now fall through to the bare return instead of
occupying separate coverage blocks, eliminating 5 uncovered statements
that the Windows coverage gate flagged at 99.4193%.
Add FILE_FLAG_OPEN_REPARSE_POINT to the Windows CreateFile call in
skillPathFileIdentityImpl so symlinks are opened as reparse points
rather than followed to their target. Staged symlinks carry relative
targets computed for the final destination, which may not resolve from
the staging directory; following them caused CreateFile to fail,
yielding an empty file ID that rejected publication and broke canonical
skill layout migration on Windows.
Make skillPathSameFileIdentity a seam variable so the tunneled
replacement test can deterministically simulate the identity change on
Unix. On tmpfs (used by Linux CI runners), os.SameFile can return true
for a recreated file due to inode reuse, making the test flaky. On
Windows the swap is a no-op because skillPathIdentityProven compares
file IDs from GetFileInformationByHandle and ignores
skillPathSameFileIdentity.
NTFS file tunneling can restore the original creation time for a
recreated same-named object, which defeated the creation-time
incarnation check and allowed rollback to delete a concurrent
replacement. Replace the platform-specific identity pair with a single
skillPathIdentityProven function:
- Unix: delegates to os.SameFile (inode/dev), ignoring file ID strings
- Windows: compares VolumeSerialNumber:FileIndexHigh:FileIndexLow from
GetFileInformationByHandle, which uniquely identifies the file on the
volume for its lifetime and is unaffected by tunneling
When the file ID cannot be obtained at publish time, identity is not
proven and the auto-delete is refused. Add a regression test that
simulates tunneled creation time and verifies rollback still refuses
the concurrent replacement.
On Windows isNoReplaceRenameUnsupported always returns false, so the
fallback is never entered from the invalid-path test. Force the fallback
and swap skillPathLink to a non-EEXIST error to cover line 94 on all
platforms.
Add tests for mkdir non-EEXIST error, remove failure after rename
failure, first-rename-succeeds path (Linux behavior), retry-rename
path, and non-regular source safe-fail. All 24 changed executable
statements now covered on both macOS and Windows.
The fallback path for filesystems without RENAME_NOREPLACE/EXCL (NFS,
FUSE, overlayfs) used Lstat-then-Rename, which could overwrite a
concurrently created destination between the check and the rename.
Replace the TOCTOU-prone check with truly atomic no-clobber primitives:
- Directories: os.Mkdir atomically claims the destination (fails with
EEXIST if occupied). On Linux rename(2) replaces the empty dir
directly; on Darwin/Windows rename refuses existing dirs so the empty
dir is removed and the rename retried — any concurrent creation
between remove and rename is detected by the second rename failing.
- Files: os.Link atomically fails if the destination exists, then
os.Remove completes the move.
Add concurrent-creation test covering the mkdir→rename race window.
The !os.IsNotExist(statErr) branch in renameSkillPathNoReplace was
uncovered on Windows. Inject errNoReplaceRenameUnsupported for the
atomic rename and os.ErrPermission for skillPathLstat so the stat-error
path is exercised on every platform.
The smoke test creates temp home directories with .config/kimchi markers
for agent detection. On Linux CI runners XDG_CONFIG_HOME may point to the
runner's real config path, causing resolvedAgentTargets to look outside
the temp home. Unset it so detection resolves against the test's temp dir.
Restrict backup pruning to directories whose names match the DWS stamp
format (YYYYmmdd-HHMMSS with optional -N suffix) across all 8 installer
surfaces (Go, npm, 4 shell, 2 PowerShell). Unknown directories in
~/.dws/skill-backups are now preserved. Also fixes Windows coverage test
portability and covers the remaining macOS changed-code gap (retire
warning loop in runUpgrade).
typedJSONValue marshaled a typed value and then routed the result through
rawJSONValue, which runs json.Valid before decoding. On that path the input is
whatever json.Marshal has just produced, so the validation scan can only ever
succeed: it re-read every marshaled document for nothing.
The decode step is now shared by both entry points. rawJSONValue keeps its
json.Valid check, because it still accepts untrusted input, while typedJSONValue
decodes what it marshaled directly. Across the 1121-tool set this removes about a
third of the Schema Catalog projection work: the internal/app schema suite goes
from 26.0s to 17.2s uninstrumented, and from 291.1s to 241.0s under -race.
The delivered Catalog is byte-for-byte unchanged. check-generated-drift,
check-schema-catalog and check-schema-binary each regenerate the same
source_hash sha256:93b8d44eb163bd2898c78397d22af92d378e3dc4e20f56b33277b51e4342e2e6,
and the two error contracts are preserved: typedJSONValue still rejects a value
json.Marshal cannot encode, and rawJSONValue still rejects invalid JSON.
The five internal/app partitions ran end to end inside one job, so the app
shard's wall clock was the sum of all five: 780s in CI, of which the schema
partition owned 357s. Each partition is now its own matrix shard, so they run
concurrently and the shard's wall clock is set by its slowest partition rather
than by their total. Every partition shard still selects the same single
internal/app package, so the impacted-package query maps the shard name back to
app and the partition only chooses which tests run.
The helper gains a partition argument and a list-partitions mode. APP_PARTITIONS
is the single source of truth for the set, and the discovery pass still runs in
every job, so each one independently verifies that the partition patterns cover
every top-level test exactly once before running the one it was asked for.
Two fail-closed checks guard the split, because the helper's own coverage check
can no longer prove the whole package ran once the partitions are separate jobs:
- The helper cross-checks APP_PARTITIONS against the coverage counters in both
directions, so a counted partition that nothing dispatches and a dispatchable
partition with no counter both fail instead of silently skipping tests.
- TestCIAppRacePartitionMatrixMatchesHelper pins the workflow's app-<partition>
shards to list-partitions output in both directions, so a partition cannot
lose its job while every job stays green.
The discovery loop variable is renamed from partition to spec: it would
otherwise shadow the partition requested on the command line, which run mode
reads after the discovery pass completes.
The schema partition's 52 tests assert structural Schema-to-Cobra contracts over
a single goroutine: none of them call t.Parallel or start a goroutine, so the
race detector has no concurrent access to observe there. The process-global lazy
metadata that does need race coverage (schema_source_root's atomic.Value, the
parameter-binding lazy loaders) is exercised by internal/cli's concurrent tests,
which stay instrumented.
The instrumentation was not free here. The partition shares a single sync.Once
Catalog build whose work is allocation-heavy, and -race made it roughly 11x
slower: 26s -> 291s locally, and 357s of the app shard's 780s in CI. Within that
partition TestFinalSchemaToolsHaveExecutableBaseCommands alone accounted for
262s, not because the test is expensive but because it is the first caller to pay
for the shared snapshot; its 1121 subtests together measure 0.00s.
run_partition now takes the instrumentation mode explicitly and fails closed on
an unrecognized value, so a typo cannot silently drop -race from a partition that
is supposed to carry it.
The workflow contract pinned the focused path by literal: the job name
`Test (changed packages)`, the unsharded
`list "$TEST_BASE_REF" "$TEST_HEAD_REF"` call, and a single
`go test -timeout=15m` line standing in for internal/app's package-level
headroom. Sharding the job changed all three literals, so `Test (workflow
and release contracts)` failed on this branch even though every shard
selection test passed.
Each invariant the contract guarded still holds, so the assertions are
updated to the new shape rather than relaxed:
- the focused job must still exist, now as the matrix job, named the way
the contract already names `Test (race: ${{ matrix.shard }})`;
- package selection must still derive from the authoritative synthetic
merge base/head, now with an explicit shard argument, so pointing it at
any other ref still fails the contract;
- internal/app's headroom is asserted through the process-isolating
helper and the per-shard budgets, mirroring the assertions already
applied to test-race. That is stronger than the old single -timeout: it
pins the mechanism that keeps the suite inside its budget rather than
the number alone. release-scripts membership is asserted too, because
its dedicated job only runs at full-suite or release-sensitive scope,
so losing it here would silently stop testing test/scripts changes.
The shard comparisons in the focused job are quoted so that job reads
verbatim like test-race's.
Ablating the implementation one change at a time turns the contract red
in all five cases: removing the app helper call, dropping release-scripts
from the matrix, selecting from HEAD~1, collapsing the matrix back to a
single unsharded job, and dropping the cli/smoke timeout budget.
Reading the package list with `mapfile < file` has unambiguous line
semantics. Routing it through a step output and a here-string instead
would append an extra empty array element if the value ever carried a
trailing newline, and that element would reach go test as an empty
package argument. The step output now carries only a single-line boolean,
and the list travels through RUNNER_TEMP. An explicit empty-entry guard
fails closed if the file is ever malformed.
This job cannot execute on its own pull request — editing a workflow
routes the revision to full_suite, which skips the focused path — so the
implementation deliberately avoids depending on platform-specific
trailing-newline behavior that local verification cannot observe.
The focused path tested every impacted package in a single job with a
plain `go test -race`, so internal/app ran inside one long-lived process
alongside all of its reverse dependencies. That is exactly the shape
scripts/ci/run-app-race-tests.sh exists to avoid: a single app test
process retains every constructed command tree in framework registries,
so the run grows to 900s and the job stays alive long enough to be
reclaimed by the runner. Recent focused runs failed with SIGTERM after
9-10 minutes without a single test failure, and one earlier run failed
at `internal/app 902.651s`, 2.65s past the package timeout.
Fan the same package plan across the shard matrix test-race already
uses, and run each shard the way test-race runs it: internal/app through
the process-isolating helper, cli/smoke with their wider package budget,
release-scripts without race and with archive tooling.
changed-test-packages.sh gains `list-shard`, which intersects the
impacted set with scripts/ci/test-packages.sh shard membership so shard
definitions stay single-sourced — and so an unknown shard name aborts
there rather than reporting an empty selection, which would let a
mistyped shard skip every test while reporting success.
release-scripts is in the matrix on purpose: its dedicated job only runs
at full-suite or release-sensitive scope, so omitting it here would stop
testing test/scripts changes altogether. A test pins that the shard
selections partition the impacted set exactly, so shard-plan drift
cannot silently shrink focused coverage.
Every neighbouring string in the root help listing — service
descriptions, utility descriptions, global flag usage — is hardcoded
Chinese. Routing only the feedback label through i18n therefore rendered
it in English on any host whose LANG is not zh_*, leaving a lone English
line inside an otherwise Chinese screen.
Hardcode the label and drop the two locale entries it needed. A test
assertion now pins the Chinese label so the indirection cannot return
unnoticed.
`dws --help` now closes with a Feedback section that links the
user-experience survey form, tagged with source=dws-cli so submissions
arriving through the CLI can be told apart from other channels.
The entry is deliberately root-only: this CLI is driven mostly by AI
agents, and repeating a survey link in every subcommand help would be
pure context noise. A guard test pins that boundary.
The URL is printed on its own unwrapped line — it is longer than the
help rule width, and breaking it would stop terminals from recognizing
it as a clickable hyperlink.
A universal Agent whose obsolete private copy cannot be retired installs
nothing there, yet every entry point counted that retirement failure as an
install failure — aborting `npm install`, `dws skill setup`, and the shell
installers even when the canonical store and all links published correctly,
and skipping the skills-state write. Route retirement failures to a separate
warning path across all surfaces (Go upgrade + skill setup, npm, PowerShell,
install.sh, install-skills.sh, install-event.sh, install-devapp.sh).
Also:
- Add a checked-rename fallback for filesystems that reject the atomic
no-replace flag (NFS, FUSE, overlayfs); the no-clobber contract is kept and
the previously unsupported platforms build and work.
- PowerShell multi-mode links only bundle skills, never the shared canonical
store, so third-party/user skills are no longer fanned into every Agent root.
- Prune ~/.dws/skill-backups to the newest 5 on every surface; encode
HOME-relative backup names on PowerShell to preserve origin.
- Add simulated-win32 junction coverage and rewrite the tautological
no-replace test; remove dead code whose tests gave false coverage.
- Soften the overstated Windows ownership-proof comment (NTFS tunneling).
Fifth-review addition: declaring InputFile silently claims the whole
@-prefixed value space, which matters in this product because at-mention
style values are common (--at-user @zhangsan would report a file read
failure), and declaring InputStdin makes a literal "-" unreachable. Both
are decided at declaration time and cannot be fixed downstream, so record
them next to the confirmation rule in the author rules.
Fourth-review fix: the FlagSpec sub-field table in the homology doc is
the named authority for "what each field does and whether it reaches
Schema parameters", and RFC §5.0.2 asserts declaration fields embed into
dws.schema.*. Input satisfied neither entry, leaving its deliberate
non-projection indistinguishable from an oversight. Add the table row and
the §5.0.2 exception note so the capability stays a declared fact (Usage
prose) rather than inviting an invented annotation.
Third-review fix for a CI blocker: run-platform-coverage-gate.sh only
executes ^(TestAllShortcuts|TestCrossPlatformCoverage) yet enforces 100%
coverage of changed production lines, so the TestResolveInputFlags names
left every new input.go statement reported as uncovered. Rename them to
the gate prefix, drop three unreachable pflag Set error branches that no
test could ever cover, and add the reachable stdin read-failure case.
Verified: changed code coverage 100.0000% (67 statements).
Second-review fix: explicitInputFlagName judged usability with an
unconditional TrimSpace while rawValue only trims when Trim is set. For
a non-Trim flag a whitespace main value is usable and shadows a changed
alias; the resolver could then rewrite the shadowed alias (and fail on
its @path) while the fallback chain still read the main value. Mirror
rawValue's usable() exactly and pin the shadow case with a regression
test whose alias path does not exist.
Self-review fixes: a Trim flag receiving " @path" judged usability on the
trimmed value (rawValue) while the source prefix check saw the raw value,
so the token would ship as a literal. Trim before the prefix check. Also
build the file-read error once with a conditional hint option, and pin
the default-value/env passthrough plus Trim edge with regression tests.
Document the landed corecmd.Input transitional form: declaration shape
(FlagSpec/LeafFlag/shortcut.Flag), runtime resolution semantics and
ordering, author rules (help prose, confirmation interaction with
stdin, construction-time validation), and the delta table against the
target typed InputSource design.
Port the lark-cli Flag.Input capability: a KindString flag may declare
Input sources ("file" for @path, "stdin" for -) and the framework
rewrites the explicit token into the payload content before
required/enum/constraint/Validate checks. @@value escapes to a literal
@value; a single stdin consumer per invocation is enforced; a leading
UTF-8 BOM is stripped. Shortcut.Flag gains the same declaration and the
adapter maps it through; LeafSpec inherits it via the LeafFlag alias.
Split chat message and group references by task, update intent routing and context budget, distinguish accepted card updates from verified writes, and explain the ambiguous chat --from flag.
- extend the silent-rollback contract to install-event.sh
- static contract: Restore-MultiSkillSet removes published paths lexically
(section-scoped so identity-anchor refactors keep the guarantee) and link
staging dirs are cleaned via Remove-LinkStageRoot / Remove-DevLinkStageRoot
- install-event.sh integration test: an uninstallable agent target is
skipped loudly while later agents still receive links
- pwsh probe: Test-SamePhysicalSkillRoot must dereference junctions and
symlinks (junction idempotency asserted where junctions are creatable)
- install.ps1: remove published junctions lexically in Restore-MultiSkillSet
(Windows PowerShell 5.1 follows reparse points during Remove-Item -Recurse
and could delete canonical store contents); clean link staging dirs
lexically in Publish-CanonicalSkillLinks and Move-SkillPathRecoverably
- install.ps1: Test-SamePhysicalSkillRoot now dereferences junctions via
Get-PhysicalSkillPath (mirrors EvalSymlinks/realpathSync/cd -P), so reruns
recognize already-published junctions instead of backup churn
- install-event.sh: replace silent 'mv ... 2>/dev/null || true' rollback with
the loud backup-retained failure contract already enforced for devapp
- event/devapp sh+ps1: link→copy fallback and per-agent failures now degrade
per agent like install.sh (skip loudly, continue, report at the end)
instead of aborting mid-loop or swallowing errors
- tests: junction-lexical removal contract, event per-agent degrade
integration test, pwsh junction physical-root recognition + rerun
idempotency (no backup churn)
The Coverage context was the PR critical path (~17 min end to end):
coverage-current re-ran the whole suite serially (-p 1, ~13 min) and
coverage-baseline re-ran it again at the merge-base (~13 min) although
that profile is a pure function of the base commit.
- coverage-current now owns only the scoped (standard-tier) profile;
full-suite candidate profiles come from a 5-way shard matrix
(app/cli/generators/helpers/remaining) that keeps -p 1 inside each
shard on isolated runners. scripts/ci/test-packages.sh list-coverage
defines the shards and verify proves the union equals the previous
single-run package set exactly once.
- the aggregate Coverage job reassembles the disjoint shard profiles
into coverage.txt before make coverage-gate, failing closed when a
shard file is missing, so gate semantics (100% changed-code +
scope-matched overall non-regression) are byte-compatible.
- coverage-baseline restores the merge-base full-suite profile from an
exact-key cache (merge-base SHA + resolved Go version) written by the
last green main push; any miss falls back to recomputing in the
merge-base worktree. Exact key only - no prefix fallback, a near-miss
profile would compare the candidate against the wrong commit.
- new contract tests pin the shard matrix, the assembly step, the
exact-key cache pair, and the absence of restore-keys; the package
plan test also covers the coverage shard partition.
- Fix Example indentation (tab -> 2 spaces)
- Remove unsubstantiated default en-US from --language help/docs
- Add test asserting calendarId/language are omitted when only --id is passed
A failed canonical publish only failed the upgrade when
hasDependentSkillRoot reported a non-universal link target; that helper
explicitly skipped universal agents, which are exactly the direct consumers
of ~/.agents/skills. On a universal-only machine (e.g. only Codex
installed), UpgradeSkillLocations* returned a nil error with nothing
installed, contradicting the documented "canonical publication is
mandatory and fails the upgrade loudly" contract.
Canonical publish failures now return an error unconditionally in both the
mono and multi branches, and hasDependentSkillRoot is removed. The test
that pinned the old standalone-does-not-fail-fast behavior now asserts
error propagation in both modes.
The allowSystemApps gate (homeDir == systemHome) was effectively a no-op in
production: systemHome came from os.UserHomeDir, which honors the $HOME env
override just like homeDir, so the two were always equal and the gate never
fired when $HOME was overridden.
ResolveSystemHomeDir now prefers the OS user database (getpwuid on Unix),
which is independent of $HOME, falling back to $HOME only when the user record
cannot be resolved. Production behavior is unchanged (a real $HOME still
matches); an isolated/overridden HOME now correctly skips machine-wide
/Applications discovery for zcode/minimax. The app surface references the same
shared resolver.
This is the correct fix for the hermeticity concern (machine-wide state leaking
into an isolated HOME): there is no cross-surface production inconsistency to
port — script installers always operate on the real user HOME in practice, so
they need no gate.
The new root-type guard shifted `filepath.WalkDir`'s outer error branch into
the diff, and neither the macOS nor the Windows runner reaches it naturally —
raising Windows coverage to 99.9365% and blocking the gate. Add a
`statusWalkDir` seam and a `TestCrossPlatformCoverage` regression that swaps
in a WalkDir returning a sentinel error, asserting it is surfaced unchanged.
Verified locally: changed code coverage back to 100.0000%.
Two follow-ups to the latest CR:
* push/sync uploads (`pushUploadFilePinned`): the PUT-time check pinned inode,
size, and mtime before dispatch but nothing rechecked the source after PUT
succeeded — only the root itself. An editor overwrite, truncate-rewrite, or
mmap-in-place during transfer would land a mixed old/new byte stream in OSS
and still be committed, corrupting the remote file in overwrite/local-wins.
Now stat the still-open handle again before `commit_upload`; any change in
inode/size/mtime aborts the commit. Post-PUT stat failures also abort.
* status root (`walkLocalTree`): `filepath.WalkDir` refuses to follow the root
when it is itself a directory symlink and reports it as a non-regular entry,
so the walker silently returned an empty local index and status flagged
every remote file as `new_remote`. Fail closed before the walk: the root
must be a real directory; symlinks and non-directories are rejected with a
clear message. A `statusRootLstat` seam keeps the rejection regressible on
platforms that cannot create directory symlinks (Windows without admin).
Both fixes come with `TestCrossPlatformCoverage*` regressions and take the
platform coverage gate from 99.9356% back to 100.0000% (1553 statements).
The Windows coverage gate reported changed-code coverage at 99.9360% because
drive_push.go:471-473 — the branch that surfaces an error passed to the
fs.WalkDir callback as its third argument — was not exercised. macOS runners
happen to exercise it via directory-lstat failures, Windows runners do not.
Add walk_callback_receives_error under
TestCrossPlatformCoverageDrivePushFinalWalkAndCommandGates, which swaps
walkPinnedLocalFS to invoke the callback with a non-nil err and asserts the
error is bubbled up unchanged.
Verified locally that the new subtest hits drive_push.go:471.17,473.4 with
count=1.
Windows keeps the pinned directory locked while a handle inside it is open
(os.Root plus the pull temp file or the upload source), so renaming that
directory fails with a sharing violation. Every "pinned root/ancestor was
swapped" reproduction in the drive mirror tests relied on such a rename, so 13
tests failed on windows-latest. That, not a coverage shortfall, is why
Coverage (Windows) exited 1 before the gate ever ran.
Each reproduction now falls back to injecting the equivalent identity change
when the rename is refused. pinnedPullRoot.verify() and verifyParent() read
current identity only through pullPathStat / pullRootLstat, so pointing those
seams at another directory hits the same fail-closed branches. Unix still
performs the real move and loses no strength.
Assertions that need an actual replacement tree now branch on the helper's
return value. forcePinnedFallbackForTest makes the fallback path itself
regressible on any platform, and a dedicated test covers it.
Verified locally with the fallback forced on: all 13 tests pass and changed
code coverage stays at 100%.
The platform coverage gate runs only TestAllShortcuts and
TestCrossPlatformCoverage*, so several changed statements had no platform
test exercising them:
- drive_pull.go: the smart-policy re-check that skips publication when the
target is refreshed in place (same inode) while the download is running.
- drive_pull.go: the post-publish verifyParent failure, where the result is
already on disk and must not be rolled back.
- drive_replace_unix.go: rename(2) replacement of an existing target; the
Windows side already had the symmetric test.
- drive_status_windows.go: the filepath.Clean rewrite guard had no input
reaching it, because isSafeRemoteSegment filters separators upstream.
macOS changed-code coverage: 99.8053% -> 100.0000% (1541 statements).
release_version was interpolated into an awk regex, where '.' matches any
character. Version 1.0.1-beta.1 therefore also admitted
.changes/released/1x0x1-betaX1/, letting the archive drift from the
CHANGELOG version while every other seal assertion still passed and
breaking the documented audit trail.
Compare the archive prefix with index() and split the basename off with
substr(), matching the literal-comparison idiom already used throughout
check-changelog-pr.sh. Only the basename, whose character class is fixed,
stays a pattern.
Both trigger predicates ran the same git diff, which the script already
avoids elsewhere by staging --name-status into $tmp_root/status. Write the
path list once and let each awk predicate read it, matching that idiom.
Git records no diff entry for a directory itself, so adding
.changes/foo/bar.md only surfaced the nested path, which the single-level
trigger regex skipped. The entry validation and the renderer were both
bypassed, letting a nested directory reach main and break every later
fragment render with 'unexpected directory'.
Trigger the top-level tree validation on any .changes change outside
.changes/released/ (which keeps its own immutability and release-seal
checks), and assert .changes itself is still a tree so replacing it with a
blob or symlink cannot empty the child listing unnoticed.
Re-rendering stays keyed on fragment changes so a README-only edit does
not fail on an empty fragment set.
Bind each accepted marker to the exact workflow run attempt, immutable artifact, source comment, and current PR head so a historical successful run cannot authorize a different payload.
The fragment gate only ran validation when the changed path matched the
legal fragment name pattern, so `.changes/Foo.md`, `.changes/notes.txt`
and a symlinked fragment slipped through untouched and then broke the
next PR that added a legal fragment. The trigger now fires on any
top-level `.changes/` change other than README.md and rejects every
entry that is not README.md, released/, or a 100644 blob named
^[a-z0-9][a-z0-9._-]*\.md$.
The renderer had the same hole from the other side: `find -type f`
is false for symlinks, so a symlinked fragment was silently dropped
from the rendered notes, and the `[a-z0-9]*.md` glob only constrained
the first character so `chat reply.md` passed. It now walks every
top-level entry and fails on symlinks, unexpected directories,
non-regular files and illegal names. Both scripts pin LC_ALL=C so the
ASCII ranges cannot match uppercase under a different collation.
Adds regression coverage for illegal names, non-markdown entries,
symlinks and executable modes on both the gate and the renderer.
Address P1 finding: extract_payload now strictly requires the parsed JSON
to be a dict, and validates each field's type and format:
- pr_number: string of digits
- pr_head_sha: 40-char lowercase hex string
- products: alphanumeric with commas/dots/hyphens/underscores only
- run_id: string of digits
- cases_ref: string (may be empty)
validate_run_id also guards against non-string input.
Added tests for: integer/array/string/null JSON, numeric field types,
invalid SHA format, injection in products, missing required fields.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Address P1 lint finding: structured eval-dispatch comments could be
forged by unauthorized users. Add three-layer consumer-side validation:
1. comment.user.login == 'github-actions[bot]' (platform-enforced identity)
2. comment.performed_via_github_app.slug == 'github-actions' (App signature)
3. payload.run_id verified against actual successful workflow run via API
Also adds:
- eval_poll_validate.py: consumer validation module (in-repo, auditable)
- test_eval_poll_validate.py: unit tests proving forged comments are rejected
- Go security contract test updated to assert run_id and validate reference
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The GitHub Actions runner cannot reach internal Aone CI API (structural
network isolation). Replace the curl-to-internal step with a structured
HTML comment (<!-- eval-dispatch: {...} -->) that an internal Devix
polling service picks up every 3 minutes to trigger the Aone CI pipeline.
This eliminates the EVAL_TRIGGER_URL/EVAL_TRIGGER_TOKEN secrets dependency
from the GitHub side — those can be removed once verified.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- /eval on one's own PR may omit sha=: the guard auto-pins the
dispatch-time head (commenter == PR author leaves no third-party
swap window); dispatching another author's PR still requires the
explicit reviewed SHA (keeps the P1-2 TOCTOU remedy where the
threat lives)
- cases= is now validated structurally per git check-ref-format
semantics (leading/trailing//double slashes, '..', dot-leading
components, .lock suffixes) and rejects '-'-leading values to
prevent git fetch option injection (review P2)
Users listed in .github/eval-allowlist.txt (default branch, PR-reviewed)
may dispatch /eval for their own PRs only; write/maintain/admin retain
dispatch for any PR. Fail-closed on permission API 404/network errors.
The two Minutes notes (permission apply --policy int typing and the skill
reference updates) landed in the released 1.0.58-beta.2 section after the
branch merged main. That rewrites published release notes and would drop
both notes from the next release generated out of Unreleased. Move them
verbatim into a Changed subsection under Unreleased; the beta.2 section is
byte-identical to main again.
Auto-CR (P1) correctly flagged that the carve-out's "nothing else changed"
guard was keyed on len(otherFailures) == 0, which only observes changes the
gate already judges incompatible. Several parameter contract changes are
individually compatible and so produce no failure at all: relaxing required or
cli_required, clearing required_when, widening enum, clearing interface_type,
and clearing property through a reviewed mapping exclusion. Any of those could
have ridden along with a reviewed type migration, leaving the exemption wider
than both its documentation and what the entry actually reviewed.
Replace the failure-list heuristic with a real equality check over every
published field except Type. Comparing the struct also means a field added to
parameterSchema later is covered automatically, instead of silently widening
every existing entry. The type check moves back ahead of the field loop because
it no longer needs to observe the other findings.
Add a rejection case for each individually-compatible direction. Each case first
asserts that the drift alone really is compatible, so it keeps exercising the
equality guard instead of quietly duplicating one of the incompatible-bundle
cases.
Verified against the previous implementation: with the old guard all six new
cases fail while the nine incompatible-bundle cases still pass, which is exactly
the gap that was reported.
Address the two P1 review findings and the coverage-gate CI failures:
- Every install/upgrade path that removes a skill dir (opposite-mode
leftovers, stale dingtalk-* / dws-shared, and same-name refreshes) now
moves the directory to ~/.dws/skill-backups/<stamp>/ first across
install.sh, install-skills.sh, install.ps1, install.js, `dws skill
setup`, and `dws upgrade`. A backup failure preserves the original
directory and never removes it.
- Remove --yes from every copyable `dws skill setup` example and document
what the command may remove; add regression tests that declining the
confirmation performs no removal and that the confirmation previews
every directory slated for backup+removal.
- Rename the skill-mode tests to the TestCrossPlatformCoverage* prefix so
the platform coverage gate selects them, and add edge tests for the
backup/prune/cleanup fallback branches, restoring changed-code coverage
to 100%.
Co-authored-by: Cursor <cursoragent@cursor.com>
schema-compatibility is the third check in the same Interface Integrity job,
after the two CLI interface gates. It also rejected every published parameter
type change outright. Because the earlier gates failed first and `set -e`
stopped the step from ever running, this one never surfaced in CI, so the
previous exemption only covered two thirds of the problem.
checkParameterCompatibility now consults a precise allowlist: the tool path,
parameter name and both type values must match exactly, making it
direction-sensitive by construction, and it applies only when nothing else the
gate checks about the parameter moved (default, interface_default, format,
property, interface_type, required, cli_required, required_when, enum). The type
check moved to the end of the function so the carve-out can see those findings;
ordering is unobservable because the result is sorted.
The only entry is "minutes/minutes.apply_minutes_permission" parameter "policy"
migrating from "string" to "integer" (for #912). That type is projected from the
Cobra flag type (provenance cobra_flag_type), so it describes how the CLI accepts
a value. Consumers build a command line from it, and "--policy 4" is the same
argv under either declaration — a quoted "--policy \"4\"" still reaches pflag as
4 — while RunE keeps enforcing the same [2,4] domain. The parameter maps to
property "policyId", which the command has always sent as a number, so "integer"
is closer to the actual request than "string" was.
Table values must be the canonical form schemaType emits: the JSON encoding of
the type keyword, so `"string"` with its quotes rather than a bare string. The
guard test recomputes both through schemaType and checks the decoded name
against the closed JSON Schema type set — reviewedInterfaceRefRedirect was
silently disabled twice by exactly this class of spelling mistake.
mergedFlagContractOtherwiseChanged was only ever exercised on the path where
every condition holds still, because `||` short-circuits: with no reviewed
entry the first operand already decides the outcome and the function is never
called at all. That left its five regression branches uncovered and put
changed-code coverage at 88.0952% against a 100% target.
Add the merge-path counterpart of the checkCompatibility bundled-regression
table, pairing each of shorthand / required / hidden / no-opt / scope with the
reviewed type change and requiring the type failure to reappear. Changed-code
coverage is now 100%.
The authoritative interface baseline and command-compatibility gates
rejected every flag type change on a historical command, with no review
channel — even when the new type only moves the same validation from RunE
to flag parsing. Both now consult a precise allowlist.
An entry must match command path, flag name and both type names exactly,
so it is direction-sensitive by construction, and it applies only when
nothing else about the flag moved (shorthand, required, hidden, no-opt,
scope). A bundled regression re-reports the type change.
The first and only entry is "dws minutes permission apply --policy" moving
from string to int (for #912): the old RunE parsed with
strconv.ParseInt(v, 10, 64) and enforced [2,4], the new one lets pflag
parse with base 0 and still enforces [2,4], so the historical set of
successful invocations is a subset of the new one. Base 0 additionally
accepts spellings like "0x3", which widens rather than narrows. Defaults
are excluded from the guard because the migration necessarily changes one.
In the snapshot gate the exemption resolves against the canonical
Command.Path, never the alias-expanded accepted path: an aliased command is
compared once per accepted spelling, so keying on that would let every
alias bypass the table.
The table is duplicated because check-authoritative-interface-baselines.sh
copies the whole scripts/policy/interface-baseline directory into a
worktree checked out at a historical revision and builds it there, so that
copy cannot import a package this branch adds. A guard test fails if the
two copies drift.
Upstream reorganized the multi-skill layout (#887: long-tail skills folded
into dingtalk-misc, dws-shared renamed to dingtalk-shared). Conflict
resolution keeps this branch's multi-by-default semantics (install.sh /
install.ps1 / skill setup default to multi; interactive prompts list multi
first) and adapts the cleanup paths to the rename: cleanup predicates now
recognize both dingtalk-shared (new bundle name, covered by the dingtalk-
prefix) and the legacy dws-shared so full installs and mode switches remove
pre-rename leftovers.
Co-authored-by: Cursor <cursoragent@cursor.com>
--ranges validated the position of "!" in the raw string and then returned the
trimmed halves, so " !A1:B2" was accepted and produced a set_cell_range /
clear_range operation carrying sheetId: "". Depending on how the server treats
an empty sheetId, the whole batch_update fails, or — worse — the operation lands
on the default worksheet instead of the one the user named, while the command
reports success.
Both halves must now be non-empty *after* trimming. batch-clear grew the same
hole independently (it duplicated the split inline); it now shares
splitSheetPrefixedRange, so the invariant holds by construction rather than by
being repeated correctly in two places.
batch-set-style --batch had the same gap at the JSON level: it only rejected
sheetId == "", so " " passed. It now judges the trimmed value but still sends
the raw one — sheetId may be a worksheet *name*, and names may legitimately
carry leading or trailing spaces, so trimming on the user's behalf would target
a different sheet. The --ranges form cannot express such a name anyway, which is
what --batch is for.
TestBlankSheetIdentifierIsRejectedBeforeAnyRemoteCall covers all three entry
points with calls == 0; TestBatchStyleSheetIDIsSentVerbatimNotTrimmed pins the
no-normalisation half.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Both capabilities were added as flags on an existing leaf, and in both cases
the leaf's published interface stopped describing what the command did:
- `sheet create --values/--sheets/--styles` orchestrates create → probe →
resolve default worksheet → write → read back → optional styles, yet the
leaf still published `interface_mode: mcp` + `create_workspace_sheet`.
- `sheet export --export-format csv` reads `get_range_as_csv` and never
invokes `submit_export_job`, yet the leaf published `submit_export_job`.
Each moves to its own command, declaring the interface it actually uses:
`sheet create-with-data` is `composite` with a reviewed reason and no
`interface_ref`; `sheet export-csv` is `mcp` + `get_range_as_csv`. Both are
pinned in the interface-disposition contract test.
`sheet create` and `sheet export` are restored byte-for-byte to main, so the
compatibility gates see two `command_added` additions instead of four
locked-field changes. The split also removes a user-visible trap: `--range`
without `--export-format csv` used to be silently discarded and the whole
workbook exported; the cross-format flags no longer exist, pinned by
TestSheetExportAndExportCsvFlagsDoNotLeak.
Drops the 8 now-stale mapping-ledger exclusions that covered the flags on
`sheet.create_workspace_sheet` / `sheet.submit_export_job`, shrinking this
branch's exclusion surface. Skill references and CHANGELOG follow the split.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
parseBorderStyles read only style and a string color, so every other key and
any non-string color was silently dropped:
{"top":{"style":"solid","colour":"#f00"}} succeeded and drew a border with no
colour, and color: 123 did the same. That contradicts the unknown-key
rejection this PR applies to --sheets and --styles — a partially applied
style reported as success is harder to notice than an error.
Each edge now accepts only style/color, rejects near-miss spellings with the
canonical key, and fails when style or color is present with the wrong type
or empty. All three entry points (set-style --border-styles-json,
batch-set-style --ranges/--batch, and create --styles border_styles) share
parseBorderStyles, so one fix covers them; tests assert the rejection happens
before any MCP call on every path.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sweeping the same class the last review round hit: limits and behaviour this
PR added that only reached the Go long help, not the skill references an
agent actually reads.
- batch-set-style: the 200000-cell cumulative cap across all ranges was
missing (the 100-range cap and the atomic rollback were already there).
- sheet create --styles: size must be a positive integer (a fraction is
rejected rather than silently truncated) and the row/column range forms
reject trailing characters.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
json.Decoder.Decode returns after one value, so `--values '[[1]] trailing'`
was accepted as a valid matrix and the document got created anyway. A paste
that ran long, leftover shell concatenation, or two JSON values glued
together would silently drop the tail and still create a document the user
never asked for — and creation cannot be rolled back atomically. Require
EOF after the first value on both flags, following decodeOARequest.
docs(sheet): document the fail-closed CSV export and --allow-truncated
The skill references still claimed an oversized table is truncated with a
warning on stderr, and omitted the flag. The command now aborts before
writing anything when the server reports hasMore, so an agent relying on the
skill would misread the result and had no way to learn how to opt in.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
sheet create promises that every structural check happens before the first
MCP request, but each sheet spec was only checked for object type and name:
columns/data/dtypes/formats/startCell were left to table_put, so a bad type
created and renamed the remote document first and failed at write time,
leaving behind a document the user never successfully asked for. Validate
every provided field against table_put's input contract up front, and reject
the malformed {"sheets":"bad"} wrapper instead of treating it as one spec.
Also fixed while auditing the same flow:
- unknown/misspelled keys are now rejected in both --sheets and --styles.
The server DTOs are fixed beans, so a stray "datas" was silently dropped
and the read-back probe landed on the header row: full data loss reported
as success. Near-miss spellings get the canonical key in the message.
- columns is required (the server requires it), non-blank and trim-unique;
dtypes/formats keys must resolve to a column, since the server looks them
up by trimmed name and silently ignores the rest.
- sheetId inside a spec is rejected: the document does not exist yet.
- the read-back probe now honours header:false, mode:append (a fresh sheet
appends at row 1, the startCell row is ignored) and $-absolute/lowercase
startCell refs, which the server accepts after uppercasing.
- --values cells must be scalars; a map used to be written as "map[a:1]".
- the 30000-cell and 2000000-char write limits are enforced locally.
Docs: the --styles top level only accepts snake_case (camelCase aliases are
inner-field only), and the read-back probe is not pinned to A1.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The --export-format csv branch reads get_range_as_csv, but the sheet export leaf
still declares interface_ref: submit_export_job, so discovering the csv
capability through Schema yields the wrong backing interface. Audit metadata
only — interface_ref is not read at runtime and routing is unaffected. Recorded
here so the limitation reaches release notes rather than living only in a code
comment; accurate attribution is tracked as follow-up.
The allowlist added in the previous commit keyed the reviewed
sheet.range_set_style migration by bare RPC name ("update_range"), but
interface_ref holds the canonicalized JSON that parseTool produces via
canonicalRawJSON. The lookup therefore never matched, the carve-out was
effectively disabled, and the real gate failed with
`schema tool "sheet/sheet.range_set_style" changed interface_ref`.
The existing redirect test did not catch this: it registered the fixture entry
using its own value and then asserted with the same value, so any format would
have passed. That is the same mistake as keying a probe on the author's field
spelling instead of the wire contract.
- The allowlist entry now uses the compact canonical JSON, taken from the gate's
own output rather than from pretty-printed `dws schema`.
- TestCrossPlatformCoverageReviewedRedirectKeysAreCanonicalJSON recomputes every
registered key through canonicalRawJSON, so a bare name or a pretty-printed
variant fails locally instead of only in CI.
sheet export keeps its reviewed mcp + submit_export_job declaration. The comment
now records the known trade-off explicitly: --export-format csv is a mutually
exclusive branch that reads get_range_as_csv and never invokes the declared
submit_export_job, so this declaration does not cover the csv branch's backing
interface. Attributing that branch is left out of scope for this change.
Verified against the real gate, not just unit tests: all four Interface
Integrity checks pass (authoritative-interface-integrity,
check-command-compatibility, schema-compatibility, skill-command-integrity).
Two review findings, plus a same-class defect found by sweeping for it.
1. compatibleInterfaceRefRedirect accepted any mcp tool repointing from any
non-empty interface_ref to any other, as long as no other check for that tool
failed. Schema shape cannot prove two RPCs share business semantics,
permissions, error behaviour, or side effects, so that would have let every
future backend swap bypass the gate it exists to enforce. It is now keyed on
an explicit reviewedInterfaceRefRedirect allowlist of exact tool + old→new
pairs, currently holding only the reviewed
sheet.range_set_style: update_range → set_cell_range migration. Every other
ref change is reported again.
2. sheet export --export-format csv only printed a stderr warning when
get_range_as_csv returned hasMore=true, then wrote --output and reported
success with exit code 0. Automated callers, and anyone not watching stderr,
would treat an incomplete file as a complete export, and an existing target
file was overwritten with truncated data. Truncation now fails before the
write (leaving any existing file untouched) unless --allow-truncated is
passed; with the opt-in the success line states the data is incomplete.
--allow-truncated is registered in the reviewed mapping ledger as a local
policy input.
3. Swept for the same classes and found sheet_dimension.go repeating the
fmt.Sscanf("%d") prefix-parse hole in three places: insert_dimension,
delete_dimension, and update_dimension all accepted --length "3x" as 3, so a
malformed value silently operated on the wrong row/column count — the delete
direction is not rollbackable. All three now use strconv.Atoi. (Checked and
cleared: sheet csv-get also surfaces hasMore, but it has no --output and only
returns the flag in its JSON payload, so it is not the same fail-open shape.)
Tests: allowlist rejection cases (unreviewed target ref, and the same pair on a
tool absent from the allowlist, asserted outside the table so the registration
survives until checkCompatibility runs); truncation fail-closed with a
pre-existing output file asserted byte-for-byte unchanged; --allow-truncated
write-through; and an untruncated read needing no opt-in. Changed-code coverage
stays at 100% (939 statements).
parseRowColRange gates --styles row_sizes/col_sizes before the document is
created. The row branch parsed with fmt.Sscanf(a, "%d", &r1), which consumes
only the leading digits and does not require the whole token, so "1x:3" was
silently accepted as row 1 and "2foo" as row 2. Such input passed pre-flight,
then update_dimension was sent to the wrong row after the document and data had
already been created — an unrollbackable wrong edit, the opposite of the gate's
purpose. The row branch now parses with strconv.Atoi, which requires the entire
token to be a valid integer.
The column branch had the same class of hole via a different path: parseA1Cell
appends "1" to the column token, so "A5" became "A51" and was accepted as
column A. A new isAllLetters pre-check requires the column token to be non-empty
and letters-only before parsing; genuine multi-letter columns like "AX" still
pass. With that guarantee the subsequent parseA1Cell can no longer fail, so its
now-dead error branch is removed.
Adds trailing-character rejection cases to TestParseRowColRange: "1x:3",
"2foo", "1 2:3" (rows), "A5:C", "A1", ":C" (columns), plus "AX:C" to confirm
multi-letter columns remain valid. Changed-code coverage stays at 100%.
Three review P1s.
1. CHANGELOG no longer asserts a breaking Schema change this PR does not ship.
sheet export / sheet create deliver main's mcp + interface_ref and
schema-compatibility reports ok (0 changed fields), so the "declared as
composite (breaking)" entry was false and is removed; the set-style entry
drops the "breaking" framing (accepted as compatible by the reviewed
mapping-exclusion carve-out); the duplicate ### Changed heading is merged.
2. firstNonEmptySheetSpecCell reads the start cell via
pickStr(spec, "startCell", "start_cell"). Sheet specs are forwarded verbatim
to table_put, whose wire fields are camelCase in this repo. The prior
snake_case-only read meant a user passing the real startCell would have data
written at the offset while the probe read A1 — a false "写入未生效" on a
successful write. camelCase preferred, snake_case kept for tolerance.
3. planStyleOps now parses cell_merges range with parseA1Range, matching the
cell_styles branch. planStyleOps is dry-run once before create_workspace_sheet
as the up-front structural gate, so an invalid range like "not-a-range" is now
rejected before any RPC instead of failing only at the final merge_cells call
and leaving an unrollbackable partially-completed document. merge_cells' range
contract is A1:B3-style, which parseA1Range covers (and it strips a Sheet1!
prefix).
Tests: cell-merges-invalid-range added to TestSheetCreateValidatesBeforeCreating
Document (asserts calls == 0); TestFirstNonEmptySheetSpecCell covers both
startCell and start_cell. Changed-code coverage stays at 100%; targeted sheet
suites pass; CHANGELOG has a single Changed section with no false breaking claim.
Adds coverage for the branches introduced by the per-sheet read-back:
resolveSheetIDsByName's RPC-error and unparseable-response paths, the create
--sheets path surfacing a list-fetch failure with the nodeId, and the
single-value data row in sheetSpecGrid. Changed-code coverage back to 100%.
The --values branch already reads back its first non-empty cell after writing,
to defend against the new-document initialization race where a write returns
success but the data does not land. The --sheets branch called table_put and
reported success with no read-back, so the same race would let the command exit
successfully while one or more sheets silently lost their initial data.
After table_put, the --sheets branch now:
- re-fetches get_all_sheets to build a name -> sheetId map (table_put reuses the
renamed default sheet and auto-creates the rest by name), and
- for every spec that actually has content, reads back its first expected
non-empty cell and fails if the read-back is empty or the sheet is missing.
firstNonEmptySheetSpecCell mirrors firstNonEmptyValuesCell: it treats columns as
the header row followed by data rows, honours start_cell, and returns
hasContent=false for a name-only spec so a legitimately empty sheet is not
misreported as data loss. Failures carry the nodeId and point at
sheet table-put for recovery, matching the --values branch's error shape.
Tests:
- TestSheetCreateWithSheetsVerifiesEachSheetLanded covers an empty read-back
(errors, naming the sheet + nodeId + table-put), a sheet missing from the
post-write listing, and a name-only sheet that must not trigger a read-back.
- TestFirstNonEmptySheetSpecCell covers header/data origins, an empty first
header cell, a start_cell offset, and content-less specs.
- Existing --sheets tests updated for the added get_all_sheets + per-sheet
read-back calls.
Reverts the interface_mode of sheet create and sheet export from composite back
to mcp with a single interface_ref, matching upstream/main and the existing
convention for multi-tool leaves (doc.create_document declares mcp +
create_document even though it also calls update_document).
Rationale:
- interface_ref is audit / traceability metadata; nothing reads it at runtime
(verified: rebuilding with a bogus interface_ref still routes to the correct
tool). Declaring the primary tool and treating the orchestration as an
implementation detail is the pattern main already uses.
- sheet export was mcp + submit_export_job on main; it already orchestrated
submit_export_job + query_export_job without declaring the poll. Adding an
--export-format csv branch does not change that shape, so it does not warrant
flipping to composite.
- sheet create was a genuine single-RPC command on main (create_workspace_sheet
only). The --values / --sheets / --styles orchestration I added runs after the
document exists; per the doc.create_document precedent it stays mcp.
This takes the sheet schema-compatibility failures from 4 to 0 without a waiver
or an admin override: the declarations now equal main's.
Also updates the wording of the seven new mapping-exclusion reasons for these
two commands (submit_export_job CSV params, create_workspace_sheet
values/sheets/styles) from "Composite ... input" to "Wrapper ... input", so the
reason text no longer collides with the interface_mode value now that both
leaves are mcp. The reasons are otherwise unchanged and still describe where
each value actually goes. range_batch_set_style keeps its "Composite" wording
because it genuinely stays interface_mode=composite.
Removes the two composite entries for these leaves from the interface
disposition contract test.
No execution path changes; targeted sheet suites, the disposition contract test,
and the schema-compat policy tests all pass; check-schema-catalog is green;
sheet-scoped schema-compatibility reports 0 failures.
Two compatibility carve-outs, written alongside the existing interface_type
retirement allowance. Both cover declarative provenance metadata that nothing
reads at runtime: the tool a leaf invokes is decided in the CLI source, so a
stale interface_ref or property misinforms a reader rather than misrouting a
call. Neither carve-out can mask a change to the surface callers depend on.
Cleared property through a reviewed mapping exclusion. A leaf whose backing RPC
moves to a nested payload has no honest flat property to publish. The two
alternatives are worse: keep naming a field the request no longer contains, or
let assembly fall back to flag_name_inference and publish a name that appears in
no request at all. Accepted only when the old value was non-empty, the new value
is empty, and the new value resolved through reviewed_mapping_exclusion. A
redirect to a different non-empty value, a clearing by inference or native
annotation, a clearing with no recorded source, and populating a previously
empty property all stay incompatible. The exclusion table cannot be abused to
wave arbitrary clearing through: internal/cli/schema_parameter_bindings.go
verifies every parameter claiming an exclusion really does deliver an empty
property, and every entry carries a non-empty reviewed reason.
Redirected interface_ref with an unchanged CLI contract. Accepted only when
interface_mode is unchanged and stays mcp, both refs are non-empty, and no other
compatibility failure was recorded for that tool. That last condition is the
operative definition of "the contract is unchanged" — it is measured, not
asserted, so it automatically covers a lost parameter, a newly required one, a
moved type / default / format / enum, a tightened constraint, a positional or
dry_run change, and any effect / risk / confirmation / idempotency move. Any one
of them re-reports the redirect, so a surface change cannot ride along behind a
backend move. Moving to or from composite is a change in kind rather than a
redirect and stays reported; so does removing a ref outright.
Deliberately still incompatible: mcp -> composite with the ref dropped. That is
a leaf declaring it now orchestrates several RPCs, which is a semantic upgrade
rather than a like-for-like substitution, and it belongs in review.
For this branch the two carve-outs take schema-compatibility from 17 changed
fields to 4 — the twelve sheet.range_set_style property clearings and its
update_range -> set_cell_range redirect are now accepted. The remaining four are
the interface_mode plus interface_ref pairs on sheet.create_workspace_sheet and
sheet.submit_export_job.
Tests: TestCrossPlatformCoverageSchemaCompatPropertyClearingExclusion and
TestCrossPlatformCoverageSchemaCompatInterfaceRefRedirect assert the accepted
shapes plus twelve neighbouring shapes that must stay incompatible; the drift
table gains cases for clearing without an exclusion and redirecting despite one.
P1 from automated review. --values decoded with plain json.Unmarshal, so every
number became a float64 and integers beyond 2^53 were rounded before anything
was written. The read-back only checks that the probe cell is non-empty, so the
corruption was reported as a successful write. Order numbers and snowflake IDs
are ordinary spreadsheet data.
Measured before the fix:
1234567890123456789 -> 1234567890123456768 snowflake id, tail rewritten
12345678901234567890 -> 12345678901234567000 20-digit order number
9007199254740993 -> 9007199254740992 2^53+1
--sheets had the same defect, which the review did not mention: its records and
data are forwarded verbatim to table_put, and the float64 round trip rewrote
1234567890123456789 as 1234567890123456800 before the request left the CLI.
Both channels now decode with json.Decoder.UseNumber, and cellToString emits a
json.Number through its String method so no float conversion happens on the way
to CSV. --styles keeps plain Unmarshal on purpose: its numbers are font sizes and
pixel dimensions, already constrained to int32 by pickNum, with no large-integer
case.
Tests assert the payload the CLI actually sends, not a recomputed decode:
- TestSheetCreatePreservesLargeIntegerLiterals checks the csv argument of
set_range_from_csv and the marshalled table_put arguments. Both halves also
assert the rounded forms are absent, so removing UseNumber fails the test
instead of passing on a lucky substring match.
- TestCellToStringKeepsJSONNumberVerbatim covers large, negative, fractional and
exponent literals, and keeps the existing float64 behaviour for other callers.
The shared scriptedToolCaller keeps only the last call, and the write is the
fourth of five, so the assertion needs an intermediate call. Rather than extend
that shared helper, this adds a callRecorder local to this file: InitDeps takes
the edition.ToolCaller interface, so embedding *scriptedToolCaller and
overriding CallTool is enough.
Changed-code coverage stays at 100.0000% (850 statements) per
check-coverage-gate.sh --changed-only.
Two things, both in the create-with-data path.
Unique worksheet names. parseCreateSheetSpecs accepted a --sheets payload with
repeated names, so table_put created several worksheets sharing one name. The
style tools locate a worksheet by "id or name", so --styles would then land on
whichever duplicate the server picked, and --styles runs after the document
already exists and cannot be rolled back. Duplicates are now refused before
anything is created, naming the first occurrence:
--sheets[1].name="一月" 与 --sheets[0] 重复;工作表名必须唯一,...
Case-only differences are still accepted: the server distinguishes them and the
CLI should not tighten that. --styles needs no equivalent check because it
already requires name equality with the corresponding --sheets entry, and those
are now unique.
Coverage. The previous commit added a precise pickNum error path to the standard
and auto branches of planSizes, but no test reached it: the existing cases used
an integer size, which stops at "不能同时给 size" before the numeric check runs.
The CI coverage gate therefore reported 99.7619% on changed code
(sheet_create_with_data.go:512-514 and :521-523). Two cases now drive
type=standard and type=auto with size 28.5.
That pair is not only about the percentage. It pins the error precedence: a
fractional size must report "size=28.5 必须是整数", which points at the field
actually written wrong, rather than the generic "不能同时给 size". Swapping the
two checks would make the message misleading and now fails the tests.
Changed-code coverage measured locally at 100.0000% (845/845 statements), with
no uncovered blocks in the diff against upstream/main.
Tests:
- TestParseCreateSheetSpecsRejectsDuplicateNames covers adjacent, non-adjacent
and {"sheets":[...]}-wrapped duplicates, plus three payloads that must pass.
- Three new cases in TestSheetCreateValidatesBeforeCreatingDocument
(sheets-duplicate-name and the two fractional sizes), each asserting calls == 0.
P2 from automated review. pickNum ran int(n) straight on the float64 that JSON
decoding always produces, so font_size: 12.9 and row_sizes.size: 28.5 were
silently rewritten to 12 and 28 and then executed as a valid configuration. Both
the help text and the error messages state these fields must be positive
integers, and --styles is a non-atomic sequence that cannot be rolled back, so
truncation left a sheet that did not match what the caller asked for.
Measured before the fix:
font_size=12.9 -> emitted fontSize=12
size=28.5 -> emitted pixelSize=28
size=1e20 -> emitted pixelSize=9223372036854775807
The overflow case was worse than reported: int(1e20) saturates to MaxInt64 and
was still sent.
pickNum now returns an error and rejects a non-integral value, a NaN or Inf, a
magnitude outside int32, and a non-numeric type. A missing key and an explicit
null still report "not provided" without an error, so optional fields keep
working. Every call site propagates the error, which means the whole --styles
payload is refused before the document is created. Confirmed through the real
CLI: font_size=12.9, col_sizes size=120.5 and size=1e20 all fail with a specific
message while font_size=12 still passes.
Tests:
- TestPickNumRejectsNonIntegralAndOutOfRange covers eight rejected inputs plus
five accepted ones, the missing key, an explicit null, and alias-key lookup.
- Four new cases in TestSheetCreateValidatesBeforeCreatingDocument for
cell_styles font_size, row_sizes size, col_sizes size and the overflow, each
asserting calls == 0.
- TestPickStrAndPickNum updated: a bool value now surfaces a type error with
ok=true rather than being reported as absent.
Adds four capabilities and, for the style surface, moves to the interface that
can actually express them.
Added:
- sheet create --values / --sheets / --styles: create a workbook and populate it
in one command. --values takes a 2D array into the default sheet, --sheets
takes typed tables across several sheets, --styles carries cell_styles /
row_sizes / col_sizes / cell_merges. Every structure and enum is validated
before the document is created, so an invalid config never leaves an orphan
empty document behind.
- sheet export --export-format csv: synchronous single-sheet RFC4180 export with
--sheet-id, --range and --value-render-option. --output writes to a file (a
directory gets sheet-export.csv), otherwise the CSV goes to stdout while the
truncation warning goes to stderr, keeping stdout pipeable.
- sheet update-dimension --size-type: pixel / standard (restore the default row
height or column width) / auto (fit row height to content, ROWS only).
- sheet replace --match-formula: search and replace inside formula text.
- sheet range set-style --font-style / --font-line / --font-family /
--border-styles-json.
- sheet range batch-set-style --ranges: stamp one style across several
sheet-qualified ranges.
Changed (breaking Schema change, no CLI break):
- sheet range set-style moves from update_range to set_cell_range. The
update_range style channel exposes exactly eight properties
(backgroundColors, fontSizes, horizontalAlignments, verticalAlignments,
fontColors, fontWeights, wordWrap, numberFormat) and has no slot for italic,
underline/line-through, font family or borders, so the four new dimensions are
not expressible there. interface_ref becomes set_cell_range and the twelve
style flags stop publishing a flat property, because the value now lands in
cells[i][j].cellStyles.* with no single top-level field to name.
- sheet range batch-set-style submits one atomic batch_update instead of looping
update_range, so a partial failure no longer leaves half the ranges stamped.
--continue-on-error becomes a server passthrough. Caps the fan-out at 100
ranges and 200000 cells in aggregate.
- sheet export and sheet create declare interface_mode=composite: both route
across several tools depending on the flags, so a single mcp ref was wrong.
Schema hygiene:
- Nineteen parameters that previously resolved through flag_name_inference into
property names present in no request (bgColor, exportFormat, values, ...) are
now reviewed mapping exclusions with a stated reason, so Schema omits the
property instead of inventing one.
schema-compatibility reports 17 changed fields: 13 on sheet.range_set_style
(interface_ref plus twelve property mappings) and 2 each on
sheet.submit_export_job and sheet.create_workspace_sheet (interface_mode plus
interface_ref). No CLI flag is removed and no command path changes; the same
invocation runs on both the old and the new binary. Landing this needs a
decision on the Schema contract break.
Tests: targeted sheet suites in internal/helpers and the interface disposition
contract tests in internal/app pass; make build and gofmt clean;
check-schema-catalog, check-generated-drift, check-command-surface,
check-skill-commands and check-runtime-confirmation-truth all pass.
Replace the oversized mono Sheet reference with the progressive routing layout, mirror all Sheet topic references across both bundles, and add a paired-tree drift guard.
Update Long and --yes flag text to match the upfront confirmation gate
and automatic sign retry after --yes, consistent with thread trash.
Co-authored-by: Cursor <cursoragent@cursor.com>
Add explicit confirmation_required gate before any share_message_to_chat
call so piped stdin or direct server success cannot bypass --yes. Keep
sign retry after confirmation and add regression tests for zero-call deny,
sign retry, and direct-success paths.
Co-authored-by: Cursor <cursoragent@cursor.com>
Keep schema-compat tests aligned with main after removing the ding
property-correction allowlist; the hard-fail case is already covered elsewhere.
Co-authored-by: Cursor <cursoragent@cursor.com>
Hit the nil-allowlist early return so the platform changed-statement gate
reaches 100% after shared-file overall baseline filtering.
Co-authored-by: Cursor <cursoragent@cursor.com>
Merge of the Deprecated cache refresh surface reintroduced a public root
command; refresh the CLI interface baseline so cli-smoke stays green.
Co-authored-by: Cursor <cursoragent@cursor.com>
Deleting 100%-covered packages such as recovery falsely regressed overall
percent against merge-base. Baseline overall now uses only files still
present in the candidate profile, with a 0.1pp CI tolerance. Also exercise
stripSchemaValueCompact nested map/slice branches.
Co-authored-by: Cursor <cursoragent@cursor.com>
Restore dws cache {refresh,status,clean} as a successful no-op
compat surface so historical scripts/agents keep working after
static-endpoint delivery. Deprecated leaves stay out of Schema via
IsAvailableCommand without schema exclusions.
Co-authored-by: Cursor <cursoragent@cursor.com>
Rewrite incomplete `dws devapp +` and non-product `dws finance` mentions
so skill-command-integrity no longer treats them as executable paths.
Co-authored-by: Cursor <cursoragent@cursor.com>
Visible Deprecated recovery stubs are part of the public root command
tree; refresh the CLI interface baseline so cli-smoke stays green.
Co-authored-by: Cursor <cursoragent@cursor.com>
Cover each equals-form flag in its own switch case so the platform
changed-statement coverage gate reliably hits both branches.
Co-authored-by: Cursor <cursoragent@cursor.com>
Deprecated recovery leaves are not public schema leaves
(IsAvailableCommand=false), so listing them as exclusions fails
completeness with stale exclusions.
Co-authored-by: Cursor <cursoragent@cursor.com>
Drop Hidden so authoritative interface integrity still passes, restore
the CI historical command gate, and keep the unsupported notice without
Skill guidance.
Co-authored-by: Cursor <cursoragent@cursor.com>
Keep a Hidden compatibility shim that returns 不再支持 for plan/
execute/finalize, so this release stops supporting the surface while a
later release can delete the shim entirely.
Co-authored-by: Cursor <cursoragent@cursor.com>
Allow intentional removal of public commands (e.g. dws recovery)
without compatibility stubs. Keep Schema and skill-command checks
in the Interface Integrity job.
Co-authored-by: Cursor <cursoragent@cursor.com>
- introduce removeChatToolbarCustomShortcutFn in toolbar_remove_custom.go
as a package-level injection seam; default impl routes through
callMCPToolOnServer against the im server so production behavior is
unchanged
- update RunE to dispatch via the seam instead of calling
callMCPToolOnServer inline
- add two TestCrossPlatformCoverage* tests that swap the seam via
testseam.Swap and verify: (1) without --yes the seam is never called
and a typed confirmation_required error is returned, (2) with --yes
the seam is called exactly once with openCid and shortcutId. The
stub forwards to deps.Caller.CallTool so the user_required contract
gate (leaf.go) still sees the CallTool channel.
Remove EXPERIMENTAL/Preview banners from skill setup, install scripts, README, and misc references so multi mode is no longer framed as unstable preview.
Co-authored-by: Cursor <cursoragent@cursor.com>
Do not expand the default policy gate with mono-multi or skill-commands;
leave them as optional make targets only.
Co-authored-by: Cursor <cursoragent@cursor.com>
- Drop --yes and shell-comment example lines from the Cobra Example
field in chat toolbar remove-custom; keep only the single
non-bypassing command line. Aligns with AGENTS.md "no --yes in
stored examples" and "No shell comments in examples" rules.
- Mirror the change in skills/mono/references/products/chat.md
toolbar remove-custom block: remove the duplicated --yes and
shell-comment lines; keep Flags block and prose note untouched.
- Add two end-to-end confirmation gate tests under
internal/helpers/toolbar_helpers_test.go using the existing
toolbarTestCaller seam (extended with a calls []toolbarCall
slice so the new tests can assert call counts as well as the
most recent call):
* TestCrossPlatformCoverageToolbarRemoveCustomRejectsWithoutYes
asserts confirmation_required and zero MCP calls when --yes
is omitted.
* TestCrossPlatformCoverageToolbarRemoveCustomCallsMCPWithExactArgsWhenYes
asserts exactly one im/remove_chat_toolbar_custom_shortcut
call with openCid=<cid> and shortcutId=<id> when --yes is
set.
- No changes to Contract.Selection.Examples (already compliant),
Long prose, or any other toolbar file. Helper field addition is
additive: legacy single-call fields stay so all prior tests
remain green.
Fixes: PR #877 CR P1 (remove-custom confirmation gate).
Risk tier: Standard.
Verification: see PR description.
Prefer schema --compact in agent docs, remove服务发现/cache teaching,
and stop doctor from reporting a no-op cache health item.
Co-authored-by: Cursor <cursoragent@cursor.com>
Drop the no-op dws cache stubs and the doctor cache health item, and
scrub skill/AGENTS guidance that still pointed agents at them.
Co-authored-by: Cursor <cursoragent@cursor.com>
- add 45 public document shortcuts and 2 reviewed expert-only paths
- preserve six historical command and Schema identities alongside canonical leaves
- add safe local download primitives and document access/share orchestration
- keep comment create/reply confirmation backward-compatible
- ensure grant-and-share upgrades insufficient roles before messaging
- return non-zero partial/failure message ledgers and structured partial-write recovery metadata
- enumerate every selection candidate and use rune-safe Unicode keyword contexts
- assert zero-call confirmation boundaries for destructive shortcuts
Validation:
- full Go test suite and repository policy
- real DingTalk E2E for 34 canonical shortcuts, all 8 compatibility-affected entries, READER-to-EDITOR grant-and-share upgrade, and same-block selection ambiguity with zero comment writes
- command compatibility across 1,221 historical nodes and complete Schema compatibility
- 1,152 Agent examples including 62 real Cobra dry-runs
- 100% changed-code coverage across 1,260 executable statements
Complete ding leaf Property/Required from Execute CallMCP keys and live
help semantics, and allowlist the four inference→declare property remaps
so schema-compat does not freeze wrong camelCase flag names.
Co-authored-by: Cursor <cursoragent@cursor.com>
Production already ships an empty pin; remove the leftover mcp_metadata
candidate path so parameter resolution only uses declare/Cobra sources.
Co-authored-by: Cursor <cursoragent@cursor.com>
Host hrbrain, markdown, pat, and profile under dingtalk-misc, retire
their standalone packages, and rename dws-shared to dingtalk-shared
across live paths, coverage, installers, and policy.
Co-authored-by: Cursor <cursoragent@cursor.com>
Point coverage, installers, IM skill-chain, and shared routing at
dingtalk-misc references after retiring the standalone event package.
Co-authored-by: Cursor <cursoragent@cursor.com>
Host personal IM event docs under misc like other long-tail products, and
retire the standalone multi skill package.
Co-authored-by: Cursor <cursoragent@cursor.com>
Point coverage, shortcut generation, installers, and shared routing at
dingtalk-misc references after retiring the standalone packages.
Co-authored-by: Cursor <cursoragent@cursor.com>
Host open-platform app docs and skill-market commands under misc like
other long-tail products, and retire the standalone multi skill packages.
Co-authored-by: Cursor <cursoragent@cursor.com>
Align mono/multi minutes/doc/conference skill facts with live CLI: prefer
--limit/--cursor, drop false participants claims, replace deprecated doc
search, and mark conference as unsupported without dead conference.md links.
Co-authored-by: Cursor <cursoragent@cursor.com>
Sync transport post-recovery hints into en/zh locales, refresh skill QA docs for Phase 1–3/4B, and remove the dead internal/recovery CI high-risk path.
Co-authored-by: Cursor <cursoragent@cursor.com>
Drop the recovery package/commands and related Schema/skill teaching so agents
stop being steered at a dead surface, while keeping mono↔multi content QA work.
Co-authored-by: Cursor <cursoragent@cursor.com>
Specify coverage/structure/drift gates against mono, inventory existing
skill policy tests, and extend the §7 checklist for the QA track.
Co-authored-by: Cursor <cursoragent@cursor.com>
Defer install/upgrade behavior and cherry-picks to a follow-up branch;
keep this branch on multi/mono content layout and content contracts.
Co-authored-by: Cursor <cursoragent@cursor.com>
Limit scope to skill trees and skill install/setup/upgrade framework;
defer non-skill CLI, client pipelines, and full installer rewrites.
Co-authored-by: Cursor <cursoragent@cursor.com>
Capture inventory, port/adapt/reject decisions, and phased work before any
framework implementation on a main-based branch.
Co-authored-by: Cursor <cursoragent@cursor.com>
- Remove --yes from Selection.Examples in toolbar_remove_custom.go
(schema_agent_examples.go forbids --yes in stored examples)
- Fix Confirmation "required" -> "user_required" in toolbar_remove_custom.go
(schema catalog requires enum value from {not_required, user_required})
- Fix Idempotency "not_idempotent" -> "non_idempotent" in toolbar_create_custom.go
(schema catalog requires enum value from {idempotent, non_idempotent, unknown})
Fixes: F1 BLOCK from stability-release-engineer round 5 review
- B1: Fix --sort-index 0 silent drop by using cmd.Flags().Changed()
instead of value comparison in create-custom and update-custom
- W1: Add MarkFlagRequired("shortcut-id") in remove-custom and
update-custom for consistent error messages
- W2: Extend SYSTEM_BUSY error handling to all write commands
(add/hide/create-custom/remove-custom/update-custom)
- W3: Add duplicate key detection in parseExtension to prevent
silent data loss on repeated --extension keys
Add 3 custom shortcut bar CRUD subcommands and update skill docs.
- toolbar_create_custom.go: create custom entry with extension parsing
and org-id-list support (write/medium, not_idempotent)
- toolbar_remove_custom.go: delete custom entry with --yes confirmation
gate (write/medium, confirmation required)
- toolbar_update_custom.go: update custom entry with same parameter set
as create-custom plus shortcut-id (write/medium)
- chat.md: add toolbar command group documentation with all 7 subcommands
Add `dws chat toolbar` command group with shared helpers and 4 basic
subcommands for managing conversation shortcut bar visibility and order.
- toolbar_helpers.go: shared utilities (hasIntersection, isSystemBusy,
parseExtension, toolbarConversationID, toolbarNewSystemBusyError)
- toolbar_helpers_test.go: unit tests for shared helpers
- toolbar.go: command group entry assembling 7 subcommands
- toolbar_list.go: list shortcut entries (read/low)
- toolbar_add.go: add entries to visible area (write/low)
- toolbar_hide.go: hide entries from visible area (write/low)
- toolbar_sort.go: sort entries with intersection validation and
SYSTEM_BUSY error handling (write/low)
- chat.go: mount newChatToolbarCommand() to chat root
When a release zip contains multi/, upgrade one-shot refreshes to the
multi-skill layout and migrates existing mono installs. Docs drop the
cancelled runtime switch / sticky design.
Co-authored-by: Cursor <cursoragent@cursor.com>
After merging #861, use prepareWhiteboardCard and --yes so coverage
tests compile and match fail-closed confirmation + soft pending verify.
Co-authored-by: Cursor <cursoragent@cursor.com>
Flip the agent-skill default from mono (single dws/ dir) to multi
(per-product dingtalk-* + dws-shared) across all distribution faces,
and fix the upgrade path so it no longer re-installs mono alongside
multi (mono+multi co-existence bug).
- upgrade: LocateSkillsRoot prefers the zip multi/ tree; multi refresh
removes mono leftovers and stale skills, refreshes the multi cache
- install.sh/ps1/install-skills.sh/npm install.js: multi real-install
(was print-only), default flipped, mono stays opt-in via DWS_SKILL_MODE
- skill setup: non-interactive default multi; full installs now clean
stale dingtalk-*/dws-shared with confirm-preview disclosure, filtered
(-s/-x) installs stay additive
- mutual exclusion is symmetric and includes dws-shared (previously
leaked through the dingtalk- prefix) on all faces
- install.js: guard empty/corrupt multi trees (fall back to mono),
validate SKILL.md on the mono branch, guard cache refreshes
- docs: roadmap (8/30 back-schedule), migration plan, distribution
mechanism, rollout capability, capability completion, architecture
optimization, wukong comparison (archived; line retired)
Co-authored-by: Cursor <cursoragent@cursor.com>
Capture runMarkdownUnifiedDiff/diffJSONMarshalIndent before spawning the
compute goroutine so testseam restores cannot race a late timeout path.
Co-authored-by: Cursor <cursoragent@cursor.com>
Keep the add-only Wukong compat flag, but publish it as a CLI-local
polymorphic dispatch without a download_file property binding.
Co-authored-by: Cursor <cursoragent@cursor.com>
Exercise markdown diff, mail export/share, drive latest/depth, whiteboard,
and diff-engine edges via TestCrossPlatformCoverage*; add small injectable
seams only where defensive branches are otherwise unreachable.
Co-authored-by: Cursor <cursoragent@cursor.com>
Expose minutes hot-word delete, permission apply, and audio-memo list from
live MCP gaps; add hidden/cross-product flag aliases only (never remove),
with TestCrossPlatformCoverage coverage for the new surfaces.
Co-authored-by: Cursor <cursoragent@cursor.com>
Whiteboard dry-run now stamps preview_kind=plan; mail export/share
drop [DRY-RUN] tags so plan evidence matches declared DryRunSpec.
Co-authored-by: Cursor <cursoragent@cursor.com>
Schema Manual examples forbid confirmation bypass via --yes; also stub
whiteboardSleep in product example coverage so race CI does not hang.
Co-authored-by: Cursor <cursoragent@cursor.com>
Align leftover multi skill banners with the non-experimental wording,
retarget markdown routing off dingtalk-misc, and remove the retired
SAFETY_PREAMBLE_INJECT marker plus the missing extract_media_id.py refs.
Co-authored-by: Cursor <cursoragent@cursor.com>
Port calendar event instances, markdown diff, drive list --latest,
mail calendar/calendar-event/shared-with-me/export/share-to-chat, and
doc whiteboard insert so open edition matches documented Wukong test
command surfaces.
Co-authored-by: Cursor <cursoragent@cursor.com>
Platform coverage only selects TestCrossPlatformCoverage*; name the new
declaration-only visibility regression accordingly and exercise the ForTest
deps restore helper.
Co-authored-by: Cursor <cursoragent@cursor.com>
Outside --dry-run, list_doc_versions uses the normal CallTool channel;
CallReadTool is reserved for the dry-run read path.
Co-authored-by: Cursor <cursoragent@cursor.com>
Declaration-only roots skip injectStaticServers; derive product visibility
from StaticServers directly so reverse completeness is not weakened, and
restore helpers deps via pointer snapshot instead of InitDeps(nil).
Co-authored-by: Cursor <cursoragent@cursor.com>
Homology Execute probes use NewSchemaSourceRootCommand (no InitDeps); skip
deps.Out when unset and InitDeps a throwaway caller on the probe path.
Co-authored-by: Cursor <cursoragent@cursor.com>
Schema assembly must mount the reviewed command tree without InitDeps or
SetDynamicServers, so a live process keeps its ToolCaller and plugin
endpoints. Also restore doc version revert dry-run short-circuit so
--dry-run skips remote version preflight.
Co-authored-by: Cursor <cursoragent@cursor.com>
Drop Windows-only DPAPI export/import names from the darwin -run filter;
those tests skip on macOS and already run under the Windows job.
Co-authored-by: Cursor <cursoragent@cursor.com>
TestLeafArgsOmitsEmptyAndNonPositive called BuildArgs with unset required
CSV flags; after rejecting empty required transforms that path correctly
errors. Satisfy required flags in the omit-empty case and cover the new
RequiredError / scalar-transform branches for the macOS coverage gate.
Co-authored-by: Cursor <cursoragent@cursor.com>
Separator-only inputs like --event-codes ',' passed pre-transform Required
checks, then BuildArgs omitted the key and ConfirmFirst write paths could
still call MCP. Enforce non-empty transform results for Required flags and
add CrossPlatformCoverage regression coverage.
Co-authored-by: Cursor <cursoragent@cursor.com>
Keep the darwin workflow contract from re-accepting the merged
keychain+auth+app invocation that main briefly required.
Co-authored-by: Cursor <cursoragent@cursor.com>
Port OA approval form-schema / forecast-process / create-instance from
main via DeclareLeafMetadata and mapping-ledger exclusions; keep retired
schema pin paths deleted and preserve the CI auth/keychain split.
Co-authored-by: Cursor <cursoragent@cursor.com>
Resolve the macOS race budget conflict in favour of the focused scope.
c1f96241 on main extended the whole-package macOS race step from 10m to
12m and pinned that budget in the workflow contract. This branch removes
the whole-package run instead: ./internal/app is already covered by the
Ubuntu "race: app" shard, and macOS only needs the natively-gated tests.
With the focused scope the job drops from 10m47s to ~3m, so the 12m
budget is no longer needed and the two step timeouts (6m + 5m) fit inside
the 15m job budget with headroom.
The contract assertion c1f96241 added is superseded rather than dropped:
pinning both focused commands locks the per-step timeouts, and the
existing checks still block a whole-package regression and require
./internal/app to appear exactly once.
Narrowing the macOS internal/app -run pattern orphaned
TestValidateNewBinary_RecoversFromUnsignedDarwin: it is the only
runtime.GOOS != "darwin" gated test in the package, the Ubuntu race shard
skips it on Linux, and the platform coverage gate only runs
^(TestAllShortcuts|TestCrossPlatformCoverage). No CI job selected it any
more, so it could never run or fail again.
- Add the self-heal test back to the macOS -run pattern.
- Add the (CrossPlatformCoverage)? group the Windows pattern already has,
which also recovers TestCrossPlatformCoverageAuthMigrateKeychainRemainingBranches.
- Attribute vacuous runs: the two skip paths now name the branch that went
unverified, and DWS_REQUIRE_AMFI_SELF_HEAL=1 escalates such a run to a
hard failure on a host that does enforce amfid. GitHub's hosted macOS
runners do not reproduce the amfid kill, so this test has been silently
skipping there all along.
- Restore step-timeout headroom: 10m + 5m exactly equalled the 15m job
budget, leaving none for setup. The keychain/auth step drops to 6m
(measured 2m43s).
- Add a contract test that couples the macOS -run pattern to the set of
darwin-gated tests in internal/app, so the next narrowing fails loudly
instead of silently orphaning one.
Cover applyInterfaceMetadataFallback success/audit paths and summary edges
that were lost when interface_metadata_test.go was retired, closing the
aggregate overall non-regression gap (~91.22% → above merge-base).
Co-authored-by: Cursor <cursoragent@cursor.com>
Re-enable snapshot.SourceHash vs schemaCatalogSnapshotHash compare in
loadSchemaCatalogSnapshot so tampered serialized catalogs fail closed.
Runtime assembly via assembleSchemaCatalogFromRoot still bypasses decode.
Adjust coverage tests to stamp valid hashes and avoid mutating the cached
delivery snapshot.
Co-authored-by: Cursor <cursoragent@cursor.com>
The exact 848-tool assertion forced manual bumps on every identity change
without adding semantic value; require at least one ContractFinal leaf check.
Co-authored-by: Cursor <cursoragent@cursor.com>
Merge of main added wiki.list_workspace_feeds (wiki feed list); update
the per-command consistency count from 847 to 848 so CI homology gates pass.
Co-authored-by: Cursor <cursoragent@cursor.com>
Incorporate wiki feed list command from main (#862) with
DeclareLeafMetadata declarations; keep retired schema pin paths deleted.
Co-authored-by: Cursor <cursoragent@cursor.com>
Add wiki.feed and wiki.feed.list to the CLI interface baseline so the new
command enters the backwards-compatibility contract and a later change
cannot silently drop it. The wiki root entry gains feed in its command
list; the leaf records the reviewed flags including the hidden
cross-product aliases.
Only the wiki nodes are merged. `make update-interface-baseline` would
also fold in 90 unrelated nodes that main has accumulated for chat,
aitable, doc, drive, and sheet; catching those up belongs in a separate
maintenance change, not in this feature PR.
Replace manual t.Cleanup assignment restore in the schema overview
render-failure coverage case so the schema-catalog policy seam gate passes.
Co-authored-by: Cursor <cursoragent@cursor.com>
Main brought --part-size/--parallel/--no-resume onto drive download leaves
without ParamDecl or mapping exclusions, so Catalog fell back to
flag_name_inference and failed the unpinned-adapter mapping audit.
Co-authored-by: Cursor <cursoragent@cursor.com>
Mirror the dws-wukong Skill updates for `dws wiki feed list` across both
Skill layouts: command reference, intent routing, the feed workflow, and
the nextToken context-passing row. Also refresh the wiki capability
summaries so the feed query is discoverable from the product tables.
Port the knowledge base activity feed capability from the internal
dws-wukong branch feat/pull_knowledge_base_dynamic-wiki (merged there as
58fd118c) to the open-source CLI as `dws wiki feed list`.
The command forwards to the native wiki MCP tool list_workspace_feeds,
mapping --workspace/--limit/--cursor/--exclude-file onto
workspaceId/maxResults/nextToken/excludeFile. Cross-product hidden
aliases come from RegisterCrossProductAliases rather than hand-written
flags, so --workspace-id/--page-token/--next-token/--page-size all
resolve.
Register the reviewed Schema inputs (MCP contract pinned from the live
wiki tools/list payload, CommandRegistry entry, safety and selection
hints, parameter bindings, surface completeness count) and regenerate the
Catalog and Agent metadata projections.
Trim Identity.Path and each alias in the declared identity copy (whitespace
could previously reach the wire), and route the declared Selection through
SelectionSpec.Normalized() so leaf and product pass-throughs share one
normalization. Wire output verified unchanged by the catalog gate.
Honor KindBool aliases in BuildArgs/hasEffectiveValue/constraintProvided;
reject MarkRequired+Aliases combinations at registration; validate
env-sourced values against declared enums; drop dead
ValidationEffective/ProjectDeclaredParameters constants and the unused
AnnotateRuntimeFlag parameter; route confirmationBypass through BoolFlag;
refresh retired-flow comments.
Move the test-only runtimeCommandParameters adapter to the test helper
file; route snapshot-decoded optional slices through cloneOptionalStrings
so wire structs are never aliased into typed specs; drop three dead
annotation aliases; collapse the triplicated catalog/surface hash
stamping into stampSnapshotHashes; make registry-vs-command validation
iterate in sorted order; guard the enum mcp candidate with hasPinned
like its siblings; refresh stale discovery-era comments.
When user interrupts multipart download with Ctrl+C, display a helpful
message indicating checkpoint is saved and download can be resumed,
instead of returning an internal error with context.Canceled.
Relocate seven functions with zero production callers
(schemaProductToolCount, normalizeRuntimeSchemaGroups,
runtimeFlagRequiredState, deliverySchemaCatalogAvailable,
exactSchemaCommand, schemaMap, schemaToolSpecFromPayload) into a
_test.go helper file, shrinking the shipped binary surface.
Make enum and required-flag validation alias-aware via a shared
flagNameProvided/EffectiveValue path so values passed through flag
aliases are no longer skipped; drop the fully-migrated runtime risk/gate
annotation bridge and the orphaned tool-metadata/title annotation
writers, trimming the cli seam re-exports accordingly.
Drop the legacy-metadata overlay path (runtimeToolSpecAllowingLegacy,
assembleSchemaRegistryFromBoundAllowingLegacy, metadata-based agent
selection, dry-run seams) so assembly flows exclusively through
ContractFinal; gate MCP fixture machinery behind a build flag so it can
never enter production assembly. Wire output verified identical before
and after; policy tripwire reports 26 products, 847 tools.
Centralize dynamic-server command-key protection, profile runtime
selection, and endpoint-resolution error construction; drop the dead
dry-run catalog-miss branch; document host_compat stubs as edition-sync
anchors; keep test fixtures out of the package dir via t.TempDir.
- Collapse SetDynamicServers/registerDynamicServer/AppendDynamicServer
into one registration core; the ServerOverride-skip now applies to all
paths and AppendDynamicServer keeps its cmd-key no-overwrite guard.
- Delete executor.NewWorkflowInvocation (never constructed, no gate
accepts the kind) and newLegacyHiddenCommands (always returned nil).
- Route the single-profile branch through the runnerResolveProfile seam,
matching the multi-profile branch.
- Refresh stale discovery-era comments (no wire strings changed).
- Replace context.Background() with cmd.Context() in download and
download-version commands so SIGINT propagates to download goroutines
- Enables graceful interruption of multipart downloads via Ctrl+C
- Add multipart download engine (drive_transfer.go) with Range probe,
resume support, and credential auto-refresh on 401/403
- Add --part-size, --parallel, --no-resume flags to drive download and
download-version commands
- Replace httpGetFile with driveTransferDownload for chunked parallel
downloads in download and download-version commands
- Replace uploadToDrive credential parsing with driveUploadPut
(transparent header pass-through, retry on 401/403)
- Add typed httpStatusError for non-2xx HTTP responses in doc.go
- Add comprehensive unit tests (32 cases) for drive_transfer
- Update drive reference documentation with multipart download behavior
- Add E2E test for multipart download (auto-test/, gitignored)
CR: 28984991
embeddedMCPMetadata only feeds interface validation now; its summary was
projected onto every schema payload as a constant-empty blob. Remove the
SchemaRegistry field, all three payload projections, the snapshot wire
field, the overview copy, the compact strip entry, and the jq policy gate.
Also delete the redundant io.Discard dead-code suppressor in
event_command.go (io has five genuine uses there).
The mcp command now delivers its final surface in NewMCPCommand instead
of root.go overriding Hidden/Short/Long after construction.
CatalogFixtureEnv no longer gates anything once discovery is gone:
endpoint resolution is the dynamic server registry only, so a miss is
terminal by design.
EnvironmentLoader.Load has returned a constant empty catalog since live
discovery was retired, leaving a zombie chain: loader interface, catalog
types, degraded-error semantics kept alive only by a `var _ =` suppressor,
and runner/recovery fallback branches that could never succeed.
- loader.go shrinks to the env constants and CLIFlagHint; the
DiscoveryCatalog / DiscoveryCatalogLoader / DiscoveryDegraded families
are deleted.
- runtimeRunner and recoveryRuntime drop the loader field; a direct-runtime
miss is now terminal through handleCatalogMiss, and recovery endpoint
resolution is directRuntimeEndpoint only. directRuntimeToolEndpoint loses
its sole caller and is removed.
- Tests: loader-injection branches are deleted; live-behavior coverage
(mock mode, direct-runtime hit/miss, recovery resolution, catalog-miss
error path) is rewritten against the new flow.
- NewSchemaCommand / NewMCPCommand / newCatalogCommand no longer accept a
DiscoveryCatalogLoader they always discarded; schema's no-discovery
property is now structural, retiring the panic-loader test guard along
with the trivial root.go wrappers and the unused buildMCPCommandFn seam.
- Delete the lazy sync.Once / atomic counter around the retired MCP pin:
runtimeMCPMetadata only existed so a diagnostic counter could observe a
loader that always returns the constant empty pin. Assembly now calls
emptyPinnedMCPMetadata directly and SchemaMetadataLoadCounts loses the
dead MCPMetadata field (the policy bans keep the retired names from
reappearing).
Complete the in-flight rename: Catalog / CatalogLoader / CatalogDegraded*
become DiscoveryCatalog / DiscoveryCatalogLoader / DiscoveryDegraded* in
internal/cli/loader.go, the minimal stubs no longer live in internal/ir,
and schema_static_test.go's panic loader is migrated so the cli test
package compiles again.
- Add testseam.Protect for save-and-restore seams with no up-front stub
value (e.g. os.Args mutated by the code under test).
- Migrate all 35 remaining manual prev/assign/t.Cleanup-restore trios to
testseam.Swap/Protect across app, auth, cli, event, helpers, output,
pipeline, and shortcut tests; restores can no longer be forgotten.
- check-schema-catalog.sh: fail closed when a manual seam restore
reappears in any *_test.go (internal/testseam exempt).
Seam injection is now a mechanism, not a convention.
- aitable_schema: declarations live in aitable.go, not a (nonexistent)
aitable_schema_decls_generated.go.
- schema_parameter_bindings / mapping_ledger: drop retired 'Track 1 Phase 2'
completion-gate framing; describe present state (ParamDecl.Property owns
delivery, no committed bindings JSON).
- schema_contract_model: the Catalog is runtime-assembled/delivered, not
embedded.
- schema_catalog: BuildSchemaCatalogSnapshot takes no Cobra root because
identity must not be re-derived at the render boundary (no 'reapplying
manual hints').
Comment-only; reviewed audit Reason strings left untouched.
- schema_cobra_binding: the Identity-vs-spec check is now a defensive
self-consistency assertion (the spec is collected from the same
ContractFinal.Identity), not a cross-source pin; name the real drift
anchors (native annotation cross-check, collector uniqueness
self-validation, homology tool-count tripwire, surface/catalog hash
baselines) and relabel the mismatch diagnostic as collected vs declared.
- schema-compat: state explicitly that accepting interface_type clearing
is a deliberate wire-visible policy decision taken with the MCP pin
retirement (missing = unknown; re-population requires ParamDecl).
- schema_command_registry: drop retired bindings audit / MCP pin from the
peer reviewed-inputs comment; note they must not reappear.
- canonical: schema help no longer claims commands must enter a reviewed
registry; identity is collected from ContractFinal.Identity.
- homology: the tool-count tripwire error now says where to bump it after
review.
- Sweep stale 'reviewed registry' wording in corecmd and the help-flag
completeness gate comment; AGENTS.md interface-facts section matches.
Close each owned os.Stdin file when replacing it in the stdin coverage
matrix so Windows TempDir cleanup does not fail on leaked handles. Update
the command registry coverage test for contract_identity source and drop
the retired loadPinnedMCPMetadata loader reference gate from policy.
Co-authored-by: Cursor <cursoragent@cursor.com>
Allow clearing interface_type and expanding constraint group members so
MCP-pin retirement and declare≡execute alias groups stay backward-compatible.
Close platform coverage gaps with TestCrossPlatformCoverage* and bump the
ContractFinal consistency count to 847.
Co-authored-by: Cursor <cursoragent@cursor.com>
Schema Catalog now assembles from Contract/ParamDecl/Interface and Cobra only.
Keep fetch-mcp-metadata as an optional diagnostic dump and ban the retired pin path.
Co-authored-by: Cursor <cursoragent@cursor.com>
- Relocate the registry-agnostic side-guards from the deleted
check-schema-command-registry.sh into check-schema-catalog.sh:
legacy hint/visibility source ban, go:generate single-track checks,
agent-metadata embed/loader bans, lazy-loader reference-count checks,
package-scope eager-initializer ban, internal/app loader ban, and the
two fresh-process laziness tests (TestRuntimeSchemaMetadataLoadsOnlyOnDemand,
TestOrdinaryRootCommandsDoNotLoadSchemaMetadata). Drop the guards that only
protected the retired reviewed registry (JSON Schema/product shard presence,
registry-overwrite go:generate ban, registry-count test runs); the native
materialization ban already lived in check-schema-catalog.sh.
- Rename the wire-visible CommandSpec.Source label from
"reviewed_command_registry" to "contract_identity" (new exported
constant CommandSourceContractIdentity): identity is collected from
ContractFinal.Identity declarations, the registry is gone. Source is a
provenance label excluded from the identity SourceHash (surface hash is
unchanged); the catalog content hash shifts with the delivered bytes as
expected. Updated every assignment and every test pin consistently.
- Update docs/schema-dynamic-endpoint-design.md,
docs/rfc-command-framework-convergence.md and
docs/flag-help-schema-homology.md: collector is the single identity
source, reviewed registry retired; keep genuine historical context.
Drop schema_mcp_service_review disposition gates from policy, outputguard,
and docs. Keep schema_mcp_metadata.json as the only pinned MCP baseline.
Co-authored-by: Cursor <cursoragent@cursor.com>
Keep notify→out_of_surface disposition and snapshot hash alignment as
reviewed Go constants so policy/tests no longer depend on a committed JSON.
Co-authored-by: Cursor <cursoragent@cursor.com>
Update shortcut/app expectations, catalog jq, and schema-compat to accept
full hidden-sibling constraint groups, and bump delivered shortcut count to 216.
Co-authored-by: Cursor <cursoragent@cursor.com>
Phase 3 of identity-deregistry. BuildEffectiveCommandRegistry now builds the
EffectiveCommandRegistry from CollectIdentitySpecs(root) instead of the
embedded reviewed registry, and the registry source is removed atomically:
- delete internal/cli/schema_command_registry/ (registry.json + products/),
schema_command_registry.schema.json, the three //go:embed directives, and
the registry-only loaders/validators (loadReviewedCommandRegistry,
decodeCommandRegistry, ValidateCommandRegistrySource, shard assemble/merge
helpers, ReviewedCommandRegistryMergedJSON/SourceHash, ReviewedCommandSpecs)
- keep CommandSpec/CommandRegistry/EffectiveCommandRegistry,
newEffectiveCommandRegistry/indexCommandSpecs, and SourceHash; the
collector-built effective hash is byte-identical to the reviewed one, so
catalog surface_hash/source_hash are unchanged
- convert the Phase 2 dual-run gate into TestCollectedIdentityIsValidSingleSource:
collected specs non-empty, no missing primaries, effective build succeeds,
SourceHash stable across repeated collection walks
- generators: catalog -surface and agent-metadata -registry/-surface become
fail-closed retired valves; outputguard no longer protects the registry
paths; fetch_mcp_metadata derives interface refs from collected identity
instead of the merged registry JSON
- retire scripts/policy/check-schema-command-registry.sh and its Makefile
invocation; generate-schema/check-generated-drift now fail closed if
schema_command_registry/ reappears
- update AGENTS.md, docs/reference.md, and in-code reviewed-input notes
Flipping BuildEffectiveCommandRegistry to the identity collector before
removing the reviewed registry is not a clean incremental step: the collector
only finds leaves present in the tree, so the 'reviewed entry without a Cobra
leaf' bind-failure path disappears and synthetic-root tests that exercise it
break. The switchover therefore ships together with the registry removal
(Phase 3) as one atomic change. The standing dual-run gate
(TestCollectedIdentityMatchesReviewedRegistry) keeps collected identity
byte-equivalent with the registry until then.
Unify natural target resolution and message contracts, add deterministic IM event listening, streamline cold-start skills, and cover the flows with schema gates and end-to-end tests.
Synchronize schemaSourceRootFn via atomic.Value, fail closed on malformed
Int/Bool FlagSpec Default, and stop projecting a sole visible flag as
required when a hidden sibling still satisfies ValidateConstraints.
Co-authored-by: Cursor <cursoragent@cursor.com>
Phase 2 of identity-deregistry:
- Promote the opt-in probe to a standing regression gate
(TestCollectedIdentityMatchesReviewedRegistry, no env var): collected
Contract.Identity must stay byte-equivalent to the reviewed registry.
This is the insurance that lets Phase 3 retire the registry; its
MISSING_PRIMARY/DIAG/DIFF logs pinpoint any drifted command.
- CollectIdentitySpecs now self-validates (fail closed): duplicate canonical
paths, duplicate primary CLI paths, and alias collisions with a primary
path or another alias all error at collection time.
Phase 1 of identity-deregistry: demonstrate command identity can be collected
from live Cobra leaves carrying ContractFinal.Identity, byte-equivalent to the
reviewed schema_command_registry.
- schema_identity_collect.go: CollectIdentitySpecs walks ALL runnable leaves
(hidden included, mirroring bindCommandRegistryPath reachability) and builds
CommandSpec from ContractFinal.Identity; CompareCommandSpecEquivalence and
DiagnoseMissingPrimaries produce a deterministic diff/diagnostic report.
- opt-in probe test (DWS_IDENTITY_PROBE=1): collected SourceHash equals
reviewed SourceHash (846 commands), zero missing primaries, zero field diffs.
Skips without the env var so normal test runs are unaffected.
- registry: add minutes.shortcut_minutes_search (a declared read-only smart
shortcut with full Identity, consistent with 215 registered sibling smart
shortcuts); homology reviewed-tool count 845 -> 846.
Registry SourceHash advances 60eee8e2 -> 2214177084; no pinned baseline
references the old value.
Extract production resetSchemaDeliveryState for RegisterSchemaSourceRoot,
gate production *ForTest call sites, and record the H0 constraint "provided"
semantics in CHANGELOG.
Co-authored-by: Cursor <cursoragent@cursor.com>
Fill the remaining ~12 changed-code stmts blocking Coverage at 99.85%,
and point RFC reviewed-input wording at the Go mapping ledger.
Co-authored-by: Cursor <cursoragent@cursor.com>
Move mapping_exclusions/removals into a reviewed Go ledger so ParamDecl.Property
stays the sole property authority without a committed empty bindings{} Phase 2 gate.
Co-authored-by: Cursor <cursoragent@cursor.com>
race:remaining was SIGTERM'd (exit 143) mid test/smoke after mock_mcp with
no FAIL/DATA RACE; NewRootCommand public-tree smoke under -race is too heavy
to share that shard. Mirror the cli split and give smoke a 15m budget.
Co-authored-by: Cursor <cursoragent@cursor.com>
Cover remaining cli/agentmetadata/pat edge paths so aggregate coverage
stays at or above the merge-base overall percentage.
Co-authored-by: Cursor <cursoragent@cursor.com>
AllowingLegacy bypasses assembleRuntimeToolSpec, so the coverage injection
stubs never ran and CI failed on a false provenance error before the gate.
Co-authored-by: Cursor <cursoragent@cursor.com>
Rename policy loader assertions to loadPinnedMCPMetadata and add
minimal CrossPlatformCoverage tests for the remaining changed-code
statements that kept macOS/Windows gates below 100%.
Co-authored-by: Cursor <cursoragent@cursor.com>
Pin admission race shards to timeout_budget (12m/cli 15m), cover
runtimeannotate and schema_source_root success paths for platform/main
gates, and make Windows absolute catalog path checks platform-safe.
Co-authored-by: Cursor <cursoragent@cursor.com>
Finish Catalog/Agent-metadata naming debt so delivery and fixture symbols no
longer imply a retired go:embed Catalog or Hint overlay path.
Co-authored-by: Cursor <cursoragent@cursor.com>
Drop misleading Catalog-embed and HintFile naming now that assembly is
declare→delivery and selection comes from ContractFinal.
Co-authored-by: Cursor <cursoragent@cursor.com>
Drop the compiled-literal feasibility probe and its generator; runtime
Catalog delivery is already single-track ResolveSchemaBuild only.
Co-authored-by: Cursor <cursoragent@cursor.com>
Runtime assemble was stamping Snapshot.SourceHash with the registry
surface hash, so schema --all catalog_hash diverged from the CI dump
content source_hash and failed Policy. Also remap ContractFinal
Interface.Ref onto pinned MCP metadata so interface_type stays aligned.
Co-authored-by: Cursor <cursoragent@cursor.com>
Require param_aliases generate plus assembly determinism instead of a
committed cmd_schema_catalog go:generate path; gofmt and temp cleanup.
Co-authored-by: Cursor <cursoragent@cursor.com>
Drop residual go:embed catalog / committed-fixture wording so architecture
and the dynamic-endpoint design match declare→runtime assembly + Meta cache.
Co-authored-by: Cursor <cursoragent@cursor.com>
Keep declare→ResolveSchemaBuild as the ToolSpec authority, but materialize
map[cli_path]CommandMeta during deliverySchemaCatalog sync.Once so leaf
--help / ResolveMeta are O(1) after the first Schema touch. Defer wire
Catalog/Tools maps, stamp Source as runtime-assembled, drop committed
catalog/gob fixtures, and cover steady-state reuse with app/cli tests.
Co-authored-by: Cursor <cursoragent@cursor.com>
Move schema authority to declare-time ParamDecl/ContractFinal and
ResolveSchemaBuild so CI/runtime assemble instead of shipping JSON
exclusions/meta-index as delivery sources.
Co-authored-by: Cursor <cursoragent@cursor.com>
Break the remaining corecmd→cli reverse dependency by owning
runtimeannotate and contractfinal on the framework side, with cli
keeping thin re-exports. Document the three authoring tiers and that
Shortcut may use DeclareLeafMetadata.
Co-authored-by: Cursor <cursoragent@cursor.com>
Round-6 Medium docs only: drop retired agent-metadata JSON authority, document
seam packages, and pin Title/Description delivery rules. Also clarify
AttachContract godoc that description compares Long only.
Co-authored-by: Cursor <cursoragent@cursor.com>
Align ContractFinal godoc and AttachContract comments with the
contractfinal/runtimeannotate seams, clarify CHANGELOG that corecmd
still may import cli subpackages, and gofmt shortcut_test imports.
Co-authored-by: Cursor <cursoragent@cursor.com>
Move AnnotateRuntime* into cli/runtimeannotate and the Cobra-keyed
ContractFinal store into cli/contractfinal so corecmd depends on thin
subpackages instead of the cli delivery root. Keep contract as DTO-only,
document Description declare-vs-delivery, and house homology gates under
cli/homology.
Co-authored-by: Cursor <cursoragent@cursor.com>
Homology docs still said Catalog was the sole embed artifact; align with
meta-index ResolveMeta/help Safety, and add an assemble-path regression
so Short-only leaves keep declared description as contract_final.
Co-authored-by: Cursor <cursoragent@cursor.com>
Unblock CI Lint/gofmt on contract_decl_test, refresh design/CHANGELOG for
ContractDecl + schema_meta_index ResolveMeta delivery, and correct SafetyForCLIPath comments.
Co-authored-by: Cursor <cursoragent@cursor.com>
Publish a compact schema_meta_index.json beside the catalog so help/selection
lookups avoid decoding the full ToolSpec wire on the hot path.
Co-authored-by: Cursor <cursoragent@cursor.com>
SchemaDecl confused authoring with Catalog/ToolSpec delivery. Authors now
declare ContractDecl (nested contract.* types) on Spec/LeafSpec/Shortcut;
AttachContract registers only through cli.RegisterRuntimeContractFinal, and
description provenance stamps cobra_help when Long wins.
Co-authored-by: Cursor <cursoragent@cursor.com>
Drop the dual-entry thin alias layer so helpers/shortcut/framework author
contract.* types directly; keep only AnnotateRuntime* delivery helpers in cli
and document the corecmd→cli seam.
Co-authored-by: Cursor <cursoragent@cursor.com>
Remove schema_hints as a generation input so Catalog delivery depends solely on leaf ContractFinal and ProductDecl; migrate policy and contract tests to embedded catalog introspection and fix publicShortcutCount for chat-list.
Co-authored-by: Cursor <cursoragent@cursor.com>
Move the last hint parameter overlays into in-code ParamDecls (helpers +
shortcuts), regenerate catalog, and harden the migrate script for factory
and Use/RPC matching so all 74 overlay tools are declaration-backed.
Co-authored-by: Cursor <cursoragent@cursor.com>
Move 121/210 parameter-level Schema field overlays from
schema_hints/metadata JSON into in-code ParamDecl declarations on
DeclareLeafMetadata commands. The declared values are emitted as
dws.schema.* annotations at assembly time via ApplyParamDecls,
outranking tool_schema_hint (rank 620 > 500) so the hint overlay
becomes redundant once the declaration is in place.
Key mechanism changes:
- Add SchemaDecl.Parameters []ParamDecl with property/required/
interface_type/description/required_when/enum fields
- Add cli.ParamDecl type carried inside ContractFinalPayload
- ApplyParamDecls emits dws.schema.* annotations from the payload
at assembly time (no sync.Map, no tree-rebuild key issue)
- Add cli.AnnotateRuntimeFlagInterfaceType (41 overlays needed it)
- Add cli.AnnotateRuntimeFlagRequiredValue for explicit true/false
- Compatibility alias check tolerates dws.schema.* annotation
differences between primary and alias commands
- Remove runtime source_hash recomputation (87ms/997k allocs saved;
enforced by check-generated-drift.sh at build time instead)
- Add shortcut.Flag.RequiredWhen and wire through FromShortcut
- Add boolFlag OR semantics to fix confirmationBypass disagreement
- Add bindKey default kebab-to-camel for forgotten Bind
- Add schema consumption benchmarks (catalog decode 1.5s/817MB,
shortcut load 1.9ms/3MB — three orders of magnitude apart)
- Add catalog codegen feasibility probe (34 tools: 0.06s compile,
31ns access, 87KB linked — extrapolates to 2.1MB for 845 tools)
Migrated products (29 tools, 121 fields):
aisearch(1), chat(12), contact(4), doc(3), drive(1),
hrbrain(10), mail(2), report(1), sheet(3), todo(6)
Remaining 89 fields across 11 products blocked by:
- RPCName not found as string literal (19 tools, shortcut/variable)
- No matching DeclareLeafMetadata near callMCPTool (11 tools)
- No single RPCName for multi-step commands (drive.upload etc.)
All tests green: corecmd, cli, helpers. Generation and drift clean.
The fixture codified the migration's risk downgrade (high→medium) and the
publish re-classification (write→destructive); both were reverted to keep
the published Schema byte-stable, so the expectations follow the shipped
values (write tools stay high, publish stays write/high).
The ContractFinal assembly path dropped every non-declared parameter fact,
breaking the published Schema against the reviewed merge-base contract:
- merge pinned MCP parameter metadata and the reviewed in-code runtime hints
back into contract_final parameter resolution (318 interface_type losses,
calendar recurrence required/required_when regressions)
- restore devapp write risk to high and publish back to write/high; the
migration silently downgraded 14 dev write tools and re-classified publish
as destructive
- keep dev at-least-one checks as Validate hooks with the shipped wording
instead of publishing new typed constraints; constraint publication is a
contract change that belongs to its own reviewed PR (aitable annotations
reverted for the same reason)
- align RunE escape hatch, BoolFlag shadowing, guard-first ConfirmFirst
declaration, and Sheet target preflight with behavioral tests; drop the
retired gen_schema_decls.py helper and fix corecmd naming in docs/CHANGELOG
check-authoritative-schema-compatibility vs origin/main: ok.
PreRunE Validate was skipped by direct RunE / proxy calls. Run both hooks
in one wrapper (Validate first), add pat chmod Validate, let Sheet outer
guards call ContractValidate first, and assert declare user_required
leaves expose Validate, required flags, or CallTool-defer confirm.
Co-authored-by: Cursor <cursoragent@cursor.com>
Without Validate, DeclareLeafMetadata no longer confirms before RunE-local
required checks. Wrap deps.Caller so the first MCP CallTool runs
ConfirmSafety; Validate-backed leaves keep confirm-after-PreRunE.
Co-authored-by: Cursor <cursoragent@cursor.com>
DeclareLeafMetadata user_required wraps were confirming before RunE-local
checks, so illegal calls got confirmation_required instead of real errors.
Allow Validate on PreRunE, migrate event stop and drive publish checks, and
lock the Sheet dual-gate transitional state.
Co-authored-by: Cursor <cursoragent@cursor.com>
Compile reviewed Agent Schema into bind-time Go declarations so catalog
tools stamp contract_final without changing execution bodies.
Co-authored-by: Cursor <cursoragent@cursor.com>
- manualAgentExampleDryRunEvidence now recognizes executor invocation
envelopes ("kind": "*_invocation" / connect_preview with dry_run) as
invocation previews before the generic request branch, so the 32 devapp
declared tools match their declared preview_kind; pinned with a unit test
covering all invocation kinds plus request/plan precedence.
- TestDevAppWriteGuardRequiresFinalSchemaConfirmation updates devapp wants
to the declared risk grading (reversible writes medium; create/version
create/robot submit high-write; delete/publish destructive) and accepts
contract_final provenance for declared tools while hints-fed tools keep
reviewed_explicit.
Co-authored-by: Cursor <cursoragent@cursor.com>
Align the write-confirmation UX with lark-cli: ConfirmRisk (and the
shortcut confirmRisk) now print the yes/no prompt only when stdin is a
real terminal (ioctl-level check via go-isatty; a char-device stat would
misclassify `< /dev/null`). Non-interactive callers get a clean
structured confirmation_required error on stderr. Piped answers are
still honored for humans/scripts; --yes/--dry-run remain the sanctioned
non-interactive paths.
skills/mono: add the recognition + retry protocol for agents —
identify confirmation_required via error.reason, show action and params,
retry the original command with --yes only after explicit user consent,
never silently append --yes or treat it as a transient error.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Alignment-only changes in runtime_schema.go, schema_contract_model.go,
and devapp_safety_homology_test.go. Remaining make lint findings are in
upstream-owned keychain/transport files untouched by this PR.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
- schemaSafetyFromDecl: drop the now-unreachable nil return; the tier
fill always produces a complete block for a declared Schema
- validateDispatchDecl: panic when ConfirmFirst is set without Risk —
it orders a confirmation that does not exist, and for declared-Schema
writes an empty Risk would silently publish the read safety tier
- RFC: document the boundary that write commands must declare Risk
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Upstream added five reviewed tools (845 total); hashes and counts
refresh. Content of existing tools is unchanged.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Risk (runtime confirmation) and Safety (schema metadata) are two
independent enums composed at embed time: explicit SafetyDecl fields >
CommandSpec.Safety tier > Risk.SafetyDefault(). The tier fill now also
covers idempotency, so an enum-only declaration is self-sufficient and
validateSchemaDecl no longer needs safety completeness checks.
devapp reclassifies its write leaves by reversibility: reversible
mutations declare LeafSafetyWrite (risk high->medium), create/robot
submit/version create declare LeafSafetyHighWrite, and delete/version
publish declare LeafSafetyDestructive (publish effect
write->destructive). Shared hand-written safety constants are deleted.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
- validateSchemaDecl now also requires Safety (effect/risk/confirmation
or the Risk shorthand; Idempotency is declaration-only) and Interface
(mode/availability, plus reason for composite/unavailable), so every
unconditional catalog required key is guaranteed at construction time
- declared dry_run capabilities are indexed by BindEffectiveCommandRegistry
instead of Schema assembly: every process resolving the command tree
gets the reviewed set, removing the hidden "must assemble in-process
first" precondition of the delivery gate
- agent-metadata contract merge now errors when a declared tool has no
canonical CLI projection instead of silently dropping the declaration
Artifacts are byte-identical; full cli/cmdcore/helpers/generator suites pass.
Co-authored-by: Cursor <cursoragent@cursor.com>
- dry_run capabilities declared via cmdcore.SchemaDecl are reviewed by
construction: the Schema pass-through indexes them into the reviewed
capability set, so declared tools no longer need manual entries in
reviewedDryRunCapabilityGroups (31 devapp paths deleted). A conflicting
manual entry for the same canonical is a hard error.
- NewCommand now enforces authoring-time homology for declared commands:
a non-empty Schema without Description/AgentSummary/UseWhen/AvoidWhen/
Examples panics at construction instead of failing later in generated
artifacts or silently drifting from cobra prose.
- --help Example inherits Schema.Selection.Examples when not authored
separately, keeping one authored source for examples.
Catalog and agent metadata artifacts are byte-identical.
Co-authored-by: Cursor <cursoragent@cursor.com>
The agent-metadata generator now merges each registered Contract final
overlay (cmdcore.SchemaDecl) as the top-precedence contract_final
candidate, so declared tools no longer need hint-file rows for
agent_summary/use_when/avoid_when/examples/safety/interface. Selection
eval fixtures and example execution plans synthesize the same assertions
from the declaration, keeping semantic-eval and example coverage intact.
- devapp hint rows deleted from schema_hints/{metadata,selection}/dev.json
(connect_status/connect_stop/search_open_platform_docs_rag kept);
artifact content for all 31 declared leaves is byte-identical, only
provenance now reads contract_final / cmdcore.SchemaDecl
- exact-coverage gates exempt declared tools (hints remain required for
every non-declared command); reviewed-delivery gate accepts
contract_final as the stronger reviewed source
- cmdcore derives effect_source=cmdcore.contract for SchemaDecl-only
safety (read leaves), matching the Risk-shorthand path
Co-authored-by: Cursor <cursoragent@cursor.com>
Every devapp leaf now declares its full final Schema in LeafSpec
(description/safety/interface/selection/dry_run plus Risk, Required
flags and at-least-one Constraints); declaration is the sole final
source, hints no longer shape the published catalog for these tools.
- Hand-written delete/robot submit/robot config migrate to
LeafSpec+RunE with manual cmdcore.ConfirmRisk; robot result becomes
a plain declared leaf. Legacy write guard, runtime_gate annotation
and now-dead helpers are removed; the homology gate is strengthened
to declare-only for the devapp tree.
- New CommandSpec/LeafSpec ConfirmFirst knob reproduces the devapp
guard-first semantics (confirmation_required before parameter
validation) without changing shortcut ordering.
- dry_run is published for all 31 leaves via the reviewed capability
registry (invocation preview, no remote reads).
- Catalog regenerated: dry_run blocks added, unified-app-id/
version-id/member-type/user-ids correctly marked required,
require_one_of constraints published for get/webapp config/security
config, and robot config name corrected to optional (CLI upsert
runtime truth; the remote schema's required was not CLI-accurate).
Co-authored-by: Cursor <cursoragent@cursor.com>
- SchemaDecl on CommandSpec/LeafSpec declares the final ToolSpec payload;
framework converts in-process (no JSON bridge) and Schema assembly
pass-throughs it.
- Assembly fails closed on declared identity mismatched with the bound
entry and on reviewed fields in the declaration payload.
- RFC/homology/AGENTS docs pin declare=final-source, safety precedence
Final > Risk > gate, and light runtime write semantics.
Co-authored-by: Cursor <cursoragent@cursor.com>
Treat EOF/closed stdin as confirmation_required instead of an
interactive decline so agent/CI no longer get exit 0 for writes that
never ran. Align cmdcore.ConfirmRisk the same way.
Co-authored-by: Cursor <cursoragent@cursor.com>
Lift business flags/const params out of Call/PostMount, add typed
flag defaults and policy gates, and realign the RFC acceptance bar to
"no Execute/Call body exists only to assemble params".
Co-authored-by: Cursor <cursoragent@cursor.com>
The single Dispatch hook assumed every command is one MCP call, so the
Shortcut projection could only ever describe a command, never run it. Split
dispatch into Invoke (assembled toolArgs) and Orchestrate (multi-step), keep
RunE as the escape hatch, and reject specs that declare anything other than
exactly one at construction time. Ctx gives both hooks the same typed flag
accessors so orchestration no longer reaches for framework-specific plumbing.
Catalog output stays byte-identical.
Adversarial review confirmed the Phase 1 extraction is a verbatim move (no BLOCKERs) and that cmdcore.BoolFlag is equivalent to both original readers. All fixes below are in the Phase 2 additions.
Correctness: a CommandSpec declaring neither RunE nor Dispatch no longer runs the whole pipeline (write-confirmation prompt included) and silently exits 0 — it now fails with a typed internal error, which also defuses the FromShortcut trap. FromShortcut no longer double-renders the 参数约束 section (shortcutLongHelp already appends it and NewCommand appends ConstraintHelp again): it now maps intent prose only. Flag usage keeps mount()'s flagHelp decoration (必填/可选值) so projected help matches the live shortcut, and the constraint Flags slice is copied instead of aliasing the shortcut registry.
Honesty: the FromShortcut doc block now lists every dropped semantic (Required Changed-vs-effective-value divergence, typed bool/int/slice defaults, Enum, Hidden, Tips to Example, custom constraint, required/enum runtime-schema annotations). cmdcore's package doc no longer claims catalog drift proves runtime behavior — drift covers the build-time projection, unit tests cover the runtime pipeline. leaf.go's stale Phase 1 header updated. Panic messages say command not leaf; doc comments lead with the exported names.
Tests: new mount-equivalence test compares the projected command's flag set/types/usage and rendered Long against live mount(s) — the test that would have caught both bugs above; new root-to-child test exercises the inherited/root-persistent --yes/--dry-run lookup that the leaf-local helper never reached; the nil-dispatch test is inverted to assert the error. docs/architecture.md documents internal/cmdcore and how it differs from internal/cobracmd.
Verified: drift ok (840 tools, identical hashes), make policy pass, skill-command-integrity ok (1033 paths), cmdcore self-coverage 100%, CI-style changed-code coverage 100% (278 statements), full helpers/cmdcore/shortcut suites green.
CI coverage jobs run go test -coverprofile WITHOUT -coverpkg, so each package is measured only by its own tests. cmdcore's logic was exercised only indirectly from internal/helpers, leaving cmdcore self-coverage at 31.5% — failing the CI coverage gate (changed-code 40.5%, overall regression 90.79% to 90.55%) even though the cross-package platform gate reported 100%.
Add direct tests for every cmdcore primitive: flag registration for all four kinds plus hidden aliases/MarkRequired, the explicit-alias-env-default fallback chain incl. Trim/empty skips, integer and slice resolution, required validation, toolArgs assembly incl. Bind/ArgDefault/OmitEmpty/Transform (value, nil-skip, error), constraint declaration panics, constraintProvided (default-not-counted, alias, env, bool, slice main+alias), all three constraint kinds with exact error wording, Risk confirmation (read/--yes/--dry-run/accept/decline), BoolFlag (nil/missing/local/root), schema projection, constraint help, and NewCommand orchestration (order, RunE escape, per-stage abort, decline-cancels, nil dispatch).
cmdcore self-coverage 31.5% to 100%; CI-style changed-code coverage 100%.
Introduce cmdcore.CommandSpec as the single typed leaf definition and
cmdcore.NewCommand as the one orchestration path (flags → constraint decl
checks → Runtime Schema projection → constraint help → PostMount → RunE
escape / generated RunE{required → constraints → Validate → BuildArgs →
ConfirmRisk → Dispatch}). Dispatch becomes a spec property, not a
separate framework.
helpers.NewLeafCommand now delegates to cmdcore.NewCommand(FromLeafSpec),
so every LeafSpec command — including all 27 devapp leaves — flows through
the unified spec. The MCP dispatch (Call / callMCPToolOnServer /
callMCPTool) is captured in the FromLeafSpec closure.
internal/shortcut/adapter.go adds FromShortcut, the typed seam mapping a
Shortcut's shared base (flags of every kind, known constraints, risk,
help identity) into a CommandSpec. It is intentionally NOT wired into the
live mount() path: Shortcut's multi-step Execute, decline-returns-nil
semantics, and Flag.Enum/Hidden/custom-constraint extras are not modeled
by cmdcore yet, so the 376 shipped shortcuts stay byte-identical. Live
wiring is deferred to Phase 3, gated by shortcut-list + schema equivalence.
Commands are provably unaffected: check-generated-drift ok (840 tools,
identical hashes), `dws schema --all` and `shortcut list` unchanged, full
helpers/cmdcore/shortcut suites green, changed-code coverage 100%.
Phase 1 of converging the command frameworks onto one typed base. Extract
LeafSpec's flag registration, alias/env/default effective-value fallback,
required validation, cross-flag constraint declaration checks + runtime
enforcement, Risk-driven write confirmation (--dry-run/--yes/global-flag
aware via a 3-level bool lookup), toolArgs assembly, and Agent Runtime
Schema projection into a new dispatch-agnostic internal/cmdcore package.
internal/helpers/leaf.go now keeps only the LeafSpec shell (with MCP
dispatch fields) and NewLeafCommand orchestration; LeafFlag/LeafFlagKind/
LeafConstraint/LeafConstraintKind/LeafRisk and their constants become
aliases to cmdcore types, so all 27 devapp call sites compile unchanged.
Dispatch (callMCPTool/OnServer/Call) stays in helpers.
Pure extraction, zero behavior change: catalog is byte-identical
(check-generated-drift ok), the leaf unit + risk/constraint tests pass,
and changed-code coverage is 100% (224 statements across both packages).
Only the leaf framework code is touched; Shortcut delegation is deferred
to Phase 2/3.
Close the last capability gap versus the shortcut framework: LeafSpec now
carries a Risk field (read / write / high-risk-write) and enforces the
same pre-dispatch write confirmation as shortcut's confirmRisk. Read (and
empty) risk never prompts; write/high-risk-write prompt unless --yes or
--dry-run, cancelling without dispatch on decline. Prompt wording matches
the shortcut runner verbatim (command path substitutes Service+Command)
so atomic commands and smart shortcuts confirm identically. --yes is read
robustly across local/inherited/root-persistent flags.
Converge the atomic LeafSpec framework toward the shortcut framework's
constraint system so both share one flag-registration + validation base,
differing only in dispatch path (single-step MCP vs multi-step
orchestration).
- Add LeafBool / LeafStringSlice flag kinds (registration, effective-value
detection, required semantics, toolArgs assembly: bool delivers on
Changed incl. explicit false; slice trims and drops empty elements).
- Add LeafConstraint (at_least_one / exactly_one / mutually_exclusive) on
LeafSpec. The framework validates them between required checks and the
Validate hook, with error wording identical to the shortcut runner's
RuntimeContext validators; "provided" reuses LeafSpec's alias/env
fallback chain (registration defaults do not count), which the
shortcut framework's bare Changed check lacks.
- Project constraints to the Agent Runtime Schema (exactly_one =
require_one_of + mutually_exclusive) and render a 参数约束 help section,
matching shortcut leaf help. Declaration errors panic at build time.
Replace the CI classifier change with a real PR-level regression
contract: materialize the embedded multi skill source and assert
dws-shared/SKILL.md keeps the 禁止选择第一项、最近登录或最近使用账号 rule
that the MultiSkill e2e release gate requires. The new test file also
makes the revision full-suite so all quality gates run on this PR.
Skill markdown files are agent documentation embedded at build time;
they carry no Go code changes. Without this classification a one-line
SKILL.md edit triggers the full -race test suite on internal/app and
reverse dependencies, which exceeds the 8m job timeout and fails CI
deterministically.
Commit dc20ddec dropped the 禁止选择第一项、最近登录或最近使用账号 rule
from dws-shared/SKILL.md during the multi-skill refactor while the
MultiSkill e2e contract still asserts it there, blocking the
v1.0.55-beta.6 release run. Restore the rule as a mandatory-contract
bullet pointing at dingtalk-profile/SKILL.md for the full selection and
cross-org rules.
Must-fix: drive permission apply now gates on confirmDangerousAction and
declares confirmation=user_required, matching its help-text promise.
Wukong parity restored: formula-verify --exit-on-error (payload-parsing
exit path) and --targets conflict error, sheet info --include, chat
location/profile message types, search-advanced wukong flag aliases,
and a dedicated drive download-version leaf replacing the removed
polymorphic download --version.
Consistency fixes: transfer-owner --node/--workspace XOR and JSON-aware
dry-run after --yes validation; drive list --versions rejects
--depth/--pattern instead of misleading depth errors; depth BFS resumes
rate-limited folders from the failed page cursor to avoid duplicates;
doc style cover upload honors cmd.Context() and a 20 MiB size cap; chat
user-settings set validates per-item openConversationId and is
risk=medium.
Hardened the skill static audit to scan fenced code blocks and reject
unknown subcommands on group commands, fixing the stale aitable/drive
doc examples it exposed. Added CHANGELOG entry and coverage tests for
all changed statements plus previously untested ported commands.
The 26 newly registered commands raised the registry count to 839, but
runtime-surface-completeness.json still declared source_tools=813, so
check-schema-catalog.sh failed the Policy job ("runtime-surface
completeness source must remain unreviewed and interface-free"). The 26
tools are all reviewed in metadata/selection sources, so the unreviewed
71-tool list is unchanged; regenerate dependent schema artifacts.
The CI platform coverage gate enforces 100% coverage of changed
statements via tests named TestCrossPlatformCoverage*/TestAllShortcuts.
Add unit tests for drive list --depth BFS (pagination, rate-limit retry,
dedup, truncation, SIGINT, anomalies), drive list --versions/transfer-
owner/cover/revert paths, doc style cover upload flow, sheet
formula-verify target parsing, and chat group user-settings validation.
Also drop an unreachable resourceID guard in uploadDocStyleImage.
- Port drive list --depth N BFS recursive listing (pan + workspace routes,
rate-limit requeue, SIGINT partial emit, --pattern/--quiet)
- Port doc style cover set/clear, background set/clear, get with local
image validation and attachment-upload subflow
- Register all 26 newly ported commands in schema_command_registry with
reviewed metadata/selection hints instead of exclusions (813->839 tools)
- Review fixes: drive list --node usage text no longer implies required
in agent schema; remove broken formula-verify --exit-on-error; error on
--range without --sheet-id; portable stdin read; drop local --yes
shadowing root -y on drive revert/transfer-owner; use
confirmDangerousAction for non-delete confirms; explicit
recursiveChange=false now transmitted; sheet version revert and
comment delete moved into sheet confirmationGuards registry
Remove --version flag from drive download (polymorphic tool dispatch
incompatible with schema validation). Regenerate schema catalog and
add new commands to schema_command_exclusions.json.
Restore calendar helper behavior to main, finalize reviewed alias/guard decisions, cover payload and dry-run paths, and record the local migration freeze checkpoint.
Now that alias spellings are normalized to canonical flags in the PreParse
pipeline, drop the redundant flagOrFallback tails in the event-list handler and
read --start/--end/--calendar-id/--cursor/--limit directly (keeping --count as a
deliberately separate flag). Behaviour is unchanged; the pilot test guards it.
Add the ⑥ regression gate that replays every reviewed validation_fixture bad case
through the real embedded PreParse pipeline and asserts the canonical outcome
(accepting either semantic rewrite or native real-flag acceptance, failing only
on a genuine unknown-flag hallucination). Add check-param-concepts.sh (dictionary
schema/loader invariants) and check-param-alias-cooccurrence.sh (full-tree
co-occurrence scan), and wire all three into make policy.
Unify runtime morphology on pkg/cmdutil.Morph (same function the generator uses),
add a SemanticAliasHandler that looks up the embedded generated table after
morphological normalization and rewrites synonyms to the command's canonical flag
(leaving blocked/ambiguous synonyms untouched for the did-you-mean path), and
thread the command CLIPath through the pipeline Context. Pilot the mechanism on
'calendar event list' by removing its hand-written hidden spelling variants; a
behaviour-preservation test locks the outcome.
Add internal/generator/cmd_param_aliases: reads the reviewed dictionary plus the
live Cobra tree, reduces each concept against a command's real flags (>=2 visible
real flags without a reviewed ambiguous entry fails generation), and emits the
committed internal/cli/param_aliases_generated.go table with lookup helpers.
Extend generate-schema and check-generated-drift.sh to treat the dictionary as a
reviewed input and byte-guard the generated table.
Introduce internal/cli/param_concepts.json as the single reviewed source of
parameter-normalization concepts and per-command overrides, guarded by a closed
JSON schema and a go:embed loader with contract tests. Add the design spec.
No production LeafSpec uses LeafInt64; devapp only needs LeafInt
(non-zero-only putInt semantics). The default MCP dispatch and Server
routing stay — they are the framework's documented main path for
future MCP-direct products.
Post-review cleanup round:
- leaf.go: required validation now matches leafArgs inclusion rules
(LeafInt explicit 0 / LeafInt64 <= 0 count as missing) via
leafHasEffectiveValue; fallback-chain candidates are judged after
TrimSpace when Trim is set so pure-whitespace values fall through.
- command_meta.go: drop catalogStringVal/catalogStringSliceVal in favor
of existing schemaString/schemaStringSlice.
- fetch_mcp_metadata: cross-owned canonicals skip name-coincidence
direct merges; the reviewed cross-server identity is the sole source.
Live matching only recognized srv.ID+"."+name == registry canonical, so
the 101 canonicals whose reviewed interface_ref routes to a differently
named server/tool were silently skipped and stayed frozen at the
previous snapshot (or degraded to stubs). Build a reverse index from the
previous snapshot's reviewed interface_refs (live key → canonicals) and
fan the live descriptor out to every owning canonical, preserving the
reviewed ref through the existing merge semantics.
addDevAppVersionLocatorFlags and registerDevAppMemberMutationFlags lost
their last callers when the dev app command surface was reworked; the
uncovered dead code regressed overall coverage below the merge base.
alias-vs-alias collisions in the command meta lookup now resolve to the
owner with the lexicographically smallest primary path instead of map
iteration order. Move catalogStringVal next to its sibling helpers in
command_meta.go and drop the redundant captureBaseHelpFunc alias in the
calendar help wrapper. Unify the Safety help annotation to English
"(requires --yes)".
matched_tools claimed every surface tool matched even when entries were
registry stubs with no live MCP metadata, and unmatched_tools was
hardcoded to 0. Coverage now excludes stubs from matched_tools, reports
them as unmatched, and a registry JSON parse failure warns instead of
silently producing a stub-only snapshot. The schema catalog policy
invariant is relaxed to match the honest accounting.
The leaf fallback chain read only string flags, so LeafInt/LeafInt64
flags could never satisfy Required via alias or env, alias values for
integer flags were silently dropped, and a registered Default shadowed
alias/env values. Resolution order is now explicit flag > alias > env >
Default > ArgDefault, aliases register with the primary flag's Kind, and
unparsable integer env values fail loudly.
Restore capabilities now supported on main (doc read --scope/--tags,
drive upload --node overwrite, chat category, dingtalk-markdown routing),
remove commands still absent from the open-source CLI (calendar event
instances, sheet info --include, chat group create --owner), remap
folded services (attendance/ding/oa/report/sheet) to dingtalk-misc in
the shortcut generator, and regenerate shortcut sections and schema
metadata.
Two independent shortcut correctness fixes surfaced by the audit:
- Name→ID resolution (chat +dm / +broadcast / … via the shared resolver) dropped
every search_contact_by_key_word row with an empty userId. External /
cross-org contacts arrive with only an openDingTalkId, so they were silently
discarded — making resolution report a real person as missing, or collapse to
the wrong single match when an in-org namesake existed. Keep any row with at
least one usable identity (userId or openDingTalkId) and fall the display name
back through nick/showName/flowerName/staffName/userName.
- chat +messages-resource-url required --message-id with no alias, so an agent
copying the message list's openMessageId/msgId output field hit "unknown
flag". Accept --msg-id / --open-message-id as aliases (declared via an
at-least-one constraint since a shortcut's Required check only sees the
primary flag name), mirroring the earlier chat message download-media fix.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A changed production Go file with no function bodies (pragma carriers such
as internal/cli/gen.go, doc-only files) can never appear in a coverage
profile, so the missing-profile check failed every PR touching one. Parse
changed files and exempt those without executable statements; unreadable
or unparsable files stay conservative.
Plain Required now validates the effective value (primary flag -> aliases
-> env) instead of only the primary flag, matching the declared fallback
semantics; whitespace-only values under Trim count as missing. Extracted
cmdutil.MissingRequiredFlagsError to keep the unified error format.
ResolveMeta copies Catalog aliases into CommandIdentity and registers each
alias path against the same metadata (primary cli_path wins on collision),
so compat paths like 'report list' resolve instead of returning ok=false.
snapshot_services now counts only services whose tools/list succeeded and
missing_services names the failures, so a partially failed refresh can no
longer write a snapshot that claims full coverage.
Publishes all 210 public built-in shortcuts as reviewed Agent-visible
leaf tools across 16 product groups, with stable canonical identities,
executable +shortcut CLI paths, parameter and cross-parameter
constraints, selection guidance, interface metadata, and runtime-aligned
safety/confirmation semantics. Catalog grows from 603 to 813 tools.
Gitee API returns HTTP 200 with null body when a release tag doesn't
exist, unlike GitHub which returns 404. Treat empty release_id as
"no release exists" instead of erroring out.
Add `sync_release_version` input to mirror-to-gitee.yml for ad-hoc
release asset synchronization that bypasses the release.yml verify
gate when tag metadata cannot be updated.
npm registry behind CDN can take 1-5 minutes for dist-tag to propagate
to edge nodes. Extend max attempts from 12 to 60 and use incremental
backoff: 5s for first 12 attempts, then 10s.
- minutes: drop the minutesId/minutes_id candidate from the taskUuid mapping.
minutesId is the minutes document id, a different identifier from the
recording taskUuid that +record-pause/resume/stop consume via --id, so
substituting it would feed record control a wrong id. The backend list
already returns taskUuid; the guard test now asserts taskUuid/task_uuid.
- Rewrite the guard-test failure messages and fixture data in English to match
the repository convention (only the two assertions that match the
production Chinese validation string are kept).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The HR Brain entry was incorrectly placed in the released
[1.0.55-beta.1] section during merge conflict resolution. Move it
back to ## [Unreleased] ### Added since hrbrain has not shipped yet.
Several read shortcuts returned an empty list with exit 0 and no error
envelope even though the underlying MCP tool returned data, so agents misread
"no data" and made wrong decisions.
Root causes:
- Container key mismatch: the resolver probed the wrong key —
processCodeList / values / wikiSpaces / itemList / groupList / recentItems /
emailAccounts / deptUserList / labelUserList / roles / report_list, plus
get_org_labels grouped labels[] needing a descend.
- Item fields nested under a VO wrapper, not unwrapped: shiftVO / entityVO /
userInfo.
- Param exceeded a backend limit: todo +created-todos sent pageSize=50 while
the backend silently returns empty for pageSize>20; now uses the shared pager
(pageSize=20).
Affected: contact/oa/wiki/drive/minutes/calendar/attendance/chat/report/smart
resolvers. Every fix ships a guard test that feeds the real backend response
shape (and, for minutes, both the taskUuid and minutesId item shapes) and
asserts the projection is non-empty with a usable id.
scripts/shortcut_real_result.py now compares the upper (projection) output
against the lower (raw backend) layer, and record_real_shortcut_run.py captures
the lower layer in memory (persisting only derived counts, never raw PII) so an
exit-0 empty projection over a non-empty backend is scored as
projection-data-loss instead of real-ok. The Python self-test runs in CI via
test/scripts.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Normalize message projections across read shortcuts, preserve mixed user JSON, expand forwarded records, mask ciphertext, and accept media-download message ID aliases while retaining the Cobra/Schema required contract.
Drop the unreachable defensive tag-skip branch in transportEnvelopeSchema
(every transport.Event field carries a non-empty JSON tag) and add a unit
test for the validatePersonalEventOutputMode success path so the changed
code coverage gate reaches 100%.
pluginDescriptorConflictsWithDistribution and the identity-owner seeding
both treated conference as distribution-owned, so the whole plugin server
was skipped before the replaceable-fallback merge in addPluginCommandsSafe
could run. Skip replaceablePluginFallbacks names in both early gates while
keeping reserved-command protection and plugin-vs-plugin ownership intact.
The "AI Behavior" context is reported as a commit status (via
github.rest.repos.createCommitStatus) rather than a check run, but the
Code Admission gates only queried check runs via
github.rest.checks.listForRef. This caused every release to fail with
"missing: AI Behavior" since the context was never found.
Add a commit-status query after the check-run loop in both the preflight
and sealed-commit Code Admission gates. Statuses are merged only for
required contexts not already covered by a check run, preserving the
existing check-run precedence.
Stable releases previously required a byte-identical tree with the
promoted beta (only CHANGELOG.md could differ) and local releases had
to run exactly at the origin/main tip with an atomic main+tag push.
Together these froze main for the whole beta-to-stable window.
Relax both gates while keeping the beta soak mandatory:
- stable still requires an explicit delivered, non-withdrawn beta whose
commit is an ancestor of the sealed release commit; the tree-identity
drift check is removed
- local releases accept any clean sealed commit contained in
origin/main history (any branch or detached HEAD) and push only the
release tag; command-compatibility checks compare the sealed HEAD,
matching CI
These files were real-backend capture artifacts committed by mistake and
contain personal data — employee names/emails, mail subjects, conversation &
message IDs, contact userIds/org, and hardcoded real test-target IDs:
- docs/shortcut-real-read-results.json (raw read responses)
- docs/shortcut-real-write-results.json (raw write responses)
- docs/shortcut-comparison.html (embeds the raw responses)
- scripts/run_shortcut_real_read_matrix.py (hardcoded real target IDs)
They are dev-only capture artifacts, not build/CI inputs — the checked-in
public_catalog_generated.go is committed and no workflow/Makefile references
them, so removal does not affect the build. The generator scripts under
scripts/ that read these JSONs are local dev tools; they should consume a
locally-provided, uncommitted capture instead.
Add .gitignore rules so these (and the untracked shortcut-gsb-eval.* variants)
can never be re-committed.
Note: this only removes them going forward. They remain in git history on
origin/main (commit 8687d68); scrubbing history requires a separate,
owner-approved filter-repo/force-push.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The macOS/Windows coverage gates only execute tests matching
^(TestAllShortcuts|TestCrossPlatformCoverage), so the 401 refresh-retry
tests added for this change were invisible to them, leaving 12 changed
statements uncovered (92.73% < 100%). Rename the 12 existing tests into
the TestCrossPlatformCoverage prefix and add a fetchTicketAttempt edge
test covering transport failures, retryable statuses, and missing
endpoint/ticket payload fields.
The Coverage gate flagged 16 uncovered changed statements (90.6% < 100%):
- drop the unreachable handler error / nil response branches in
runPortalTicketAttempt: makeHandler never fails, matching the pre-port
portal loop on main
- cover portalStageError nil Error/Unwrap, the reconnect min/max clamp,
and the acked backoff reset via an end-to-end reconnect test
- cover personalRetryLogError fallback when a token failure carries no
structured HTTP status
- cover ClassifyRefreshFailure nil/net.Error/redirect branches, the nil
HTTPStatusError message, and oauthExchangeDisplayError fallback
- cover the personal stream source ForceRefreshToken wiring end to end
Local gate now reports changed code coverage 100.0% (165 statements).
Ported from 342d44efe (backup/event-token-lazy-resolution-pre-rewrite) and
adapted to the current in-place single 401 refresh+retry design:
- portal source: classify ticket/dial/read/ack failures via portalStageError
and reconnect with backoff on retryable stages only (DisableReconnect for
tests and one-shot callers); stage errors never leak response bodies
- personal/portal: transient token provider or refresh failures (network,
408/429/5xx) go through the reconnect loop instead of killing the source;
terminal failures (400/401/403) remain fatal
- personalRetryLogError: token resolution/refresh errors log only the
structured HTTP status, never provider error details
Unlike the original commit, a rejected token is still retried once in place
after a successful refresh, and a second 401 stays fatal (single-refresh
guard agreed in review).
Restored from the pre-rewrite branch head 342d44efe (backed up as
backup/event-token-lazy-resolution-pre-rewrite); the auth-layer changes
apply verbatim on the rebased branch.
- Add ClassifyRefreshFailure with structured HTTPStatusError so refresh
failures split into transient (network, timeout, 408/429/5xx) and
terminal (400/401/403) classes; unknown errors stay fatal.
- GetTokenSnapshot no longer marks a profile expired on transient
refresh failures, so long-running sources can retry after backoff.
- postJSON returns HTTPStatusError keeping the response body out of the
error string; the OAuth callback page HTML-escapes the sanitized
exchange error instead of echoing raw server output.
- isInvalidGrantError also matches the preserved response body.
A 401 whose error body failed mid-read (e.g. unexpected EOF) was wrapped
as retryable by the body-read path, letting the outer reconnect loop
re-enter fetchTicket and refresh again on every iteration, bypassing the
single refresh-retry guard.
Classify non-2xx responses by status first; the body is only drained
best-effort since it is never used for error reporting here. 401 stays
fatal regardless of body state, while 2xx body-read failures remain
retryable transport errors.
Portal and personal ticket requests now perform a single controlled
refresh + retry inside the production chain when the server rejects the
resolved access token with HTTP 401:
- Add optional ForceRefreshToken callback to PortalTicketConfig and
PersonalConfig. It receives the exact rejected token so the app-level
compare-and-refresh (ForceRefreshRejectedToken) can dedupe concurrent
rotations, and returns the fresh token.
- requestPortalTicket / fetchTicket retry the ticket request once with
the rotated token directly instead of surfacing an error and hoping an
outer loop retries; a second 401 stays fatal to prevent refresh loops.
- Refresh failures keep both the original 401 and the refresh error via
errors.Join; empty rotated tokens fail fast before hitting the server.
- Wire forceRefreshRejectedAccessToken into event consume (portal) and
personal stream sources; resolveSourceAccessToken strict semantics are
unchanged (provider errors still propagate, no static-token fallback).
- Tests: full DingtalkSource.Start -> startPortalTicket chain
(401 -> refresh -> ticket ok -> WebSocket event), rotated-token reuse,
refresh failure, nil-callback compatibility, second-401 fatality, and
app-level wiring.
ossutil 2.x signs requests with V4 and refuses to run without an
explicit region, so the OSS mirror sync would fail in CI even with
valid credentials. Derive OSS_REGION from the endpoint host
(including -internal variants) and fail fast when it cannot be
derived.
The push-triggered mirror-gitee-release job consumes the same run's
finalized-release-dist artifact, so it cannot mirror a tag that was
already published — including one delivered by a recovery dispatch such
as v1.0.53-beta.3. Add a workflow_dispatch repair-gitee job (input
mirror_gitee_version) that re-derives the asset set from the immutable
GitHub Release, verifies it byte-for-byte via checksums, and runs
sync-to-gitee.sh. Guarded to the official repo + default branch and
gated by the existing Gitee secrets.
Wait for the accept loop to stop before waiting for connection handlers, and track accepted connections before publishing handlers. This removes the WaitGroup Add/Wait race caught by PR #667 CI and follows up the event bus introduced in #589.
Remove the internal design and execution plans from docs/plans and docs/superpowers so the public pull request contains only implementation and maintained user-facing documentation.
The four files were introduced only on this branch. No runtime code, generated output, README, CHANGELOG, or Skill documentation references them.
Verification:
- Confirmed origin/main does not contain the files.
- Confirmed no remaining repository references.
- Ran git diff --check before committing.
Preserve manual-token defaults across explicit profile refreshes and selective logout while keeping legacy marker behavior compatible.
Make profile login, refresh, switch, and logout writes rollback-safe; reject unsupported future profile versions before remote side effects; and prevent cross-profile token fallback.
Forward token overrides through usage recording, use the newly authenticated identity for post-login authorization, distinguish unavailable profile state, and propagate Windows registry deletion failures.
Describe exact and friendly profile selector forms, deterministic organization-current behavior, profile listing semantics, and single-account or organization logout examples.
Update both mono and multi skills so agents avoid implicit account selection and request corpId:userId when an organization is ambiguous.
Record the compatibility design and implementation plan, including profiles v2 migration, legacy command support, storage mirrors, risk controls, and end-to-end acceptance criteria.
Accept corpId:userId and friendly organization/account selectors across global --profile, profile switch/use, event child processes, and multi-profile command execution.
List every local account in storage order with live identity-token status, preserve exact current and previous identities, and require explicit selection when an organization has no deterministic current account.
Extend auth logout to remove one exact account, every account in one organization, or all local accounts while revoking each token with its persisted credentials.
Use in-memory login tokens for identity enrichment before persistence, refresh generated Schema artifacts, and cover the complete CLI flow with isolated beta.3 end-to-end tests.
Store DingTalk credentials in corpId:userId identity slots while retaining organization and legacy mirrors for forward compatibility.
Resolve organization, account, friendly-name, current, previous, and deletion selectors without silently choosing among ambiguous accounts.
Make identity tokens the source of truth, serialize migration and refresh reads, reject unsafe mirror recovery, and sweep orphan token entries during reset.
Persist token source and client ID for exact remote revocation, require user identity before first-login persistence, and add cross-platform regression coverage for migration, deletion, refresh, and keychain failures.
Rename the get locator tests to TestCrossPlatformCoverage* so macOS/Windows changed-code coverage actually executes them.
Co-authored-by: Cursor <cursoragent@cursor.com>
Keep embedded catalog/bindings in sync with the new cobra flag so schema help-flag and policy checks pass.
Co-authored-by: Cursor <cursoragent@cursor.com>
- **Whiteboard shortcuts** (#1082) — adds strict query and confirmed update workflows with stable-target receipts and exact readback verification.
- **Sheet shortcut hardening** (#1082) — makes worksheet listing and cell-range reads fail closed on malformed, ambiguous, or truncated responses, publishes a closed reviewed output shape, and preserves non-executing `--dry-run` previews for range reads.
- **AiSearch and Contact shortcuts** (#1083) — adds strict people search and reviewed unified results; people results must use the live-reviewed `person` source, and exact mobile lookups normalize accepted formatting before calling the dedicated mobile interface. Agent/public discovery keeps `contact +list-roles`, `contact +list-roster-fields`, `contact +get-roster`, and incomplete Live routes unavailable rather than publishing ambiguous results, while the historical Contact CLI commands retain legacy MCP execution and real error propagation. The legacy role-list projection preserves the service's reviewed null placeholder without exposing that ambiguous row through Agent Result contracts.
- **AITable datasource shortcuts** — adds 7 shortcuts for datasource sync management (`+datasource-create`, `+datasource-update`, `+datasource-sync`, `+datasource-sync-status`, `+datasource-get-config`, `+datasource-list-sources`, `+datasource-get-fields`) and updates the `dingtalk-aitable` skill with routing rules and a new `aitable-datasource.md` reference guide.
- **OA approval attachment upload** — `dws oa approval attachment upload --file <path>` uploads a local file as an approval attachment in one command: it initializes the upload credential (MCP `oa/init_attachment_upload_info`), HTTP PUTs the file to OSS, then commits it (MCP `oa/commit_attachment_upload_info`). `--file-name` defaults to the file's base name and `--md5` is auto-computed when omitted.
- **Chat automatic pagination controls** (#970) — adds bounded `--max-items` and cancellable `--page-delay` support to the core IM list shortcuts, with safe continuation metadata and truncation reporting.
- **Doc/drive/wiki routing descriptions** — clarifies the document-space container-vs-content boundary across the doc, drive, and wiki skill descriptions for more predictable first-round Agent selection, without changing CLI behavior.
- **Doc and Drive parameter aliases** — normalizes reviewed identifier, pagination, path, version, and role synonyms while blocking ambiguous values before dispatch.
- **International DingTalk region support** — adds `.io` login and MCP routing, pre-release endpoint overrides, and profile-aware gateway selection while preserving the existing `.com` flow.
- **Chat IM ID flags** (#954) — standardizes chat command entry points on `--conversation-id` for conversation IDs and `--message-id` for message IDs, so help, Schema, and Agent recommendations use the same canonical flags.
- **Legacy chat flag compatibility** (#954) — keeps older chat IM ID flags such as `--group`, `--id`, `--chat`, `--open-conversation-id`, `--msg-id`, and `--open-message-id` working as compatibility aliases where applicable, while hiding migrated aliases from recommended help and Schema surfaces.
- **Chat group bots target flag** (#954) — keeps `dws chat group bots` on the visible `--group` flag; this command does not register `--group-name`, and `--group` accepts either an openConversationId or a uniquely resolved group name.
- **Chat card update evidence** — distinguishes an accepted update request from an independently verified visible update, preserving the real `bizId` and warning callers not to repeat an unverified write.
- **Chat command guidance** — splits message and group references by task and explains that `--from` is ambiguous between sender and time-range intent.
- **Robot group reference replies** (#928) — `chat message send-by-bot` supports paired `--reply` and `--ref-sender` flags for Markdown replies that quote an existing group message.
- **Document write verification** (#960) — avoids false partial-success results when normalized Markdown, paginated blocks, inline images, or version reverts are confirmed by server readback. Document reverts and media inserts now require explicit readback evidence and report partial success when the server cannot prove the requested result.
- **Doc/drive description scope** — restates the `dingtalk-doc` description as document-entity-and-content operations with an explicit exclusion list, and narrows `dingtalk-drive` to file-level management of DingTalk documents, so first-round Agent selection separates content work from file management without changing CLI behavior.
- **Sheet SourceRange dropdowns** — supports range-backed dropdowns across direct, cell, and batch write paths, with structured readback for valid and invalid references. Batch `set-dropdown` now rejects unsupported top-level `colors` / `source-colors`; Inline colors belong in `options[].color`, while SourceRange color writes remain unsupported.
- **Sheet read completion metadata** — documents and preserves returned ranges, truncation reasons, and partial-read status for large range and CSV reads.
- **Windows event bus lifecycle** — start event consumers without unsupported inherited file descriptors, stop buses through local IPC with a termination fallback, and preserve subscription cleanup when startup fails.
- **Chat group roles** (#1058) — exposes the single-value `--role-id` flag for assigning one custom group role while preserving hidden `--role-ids` compatibility.
- **Chat user mentions** — preserves literal `<@openDingTalkId>` tokens in current-user Markdown messages and rejects mismatches between message-body mentions and mention flags before sending.
- **Chat direct media** — uses the IM upload target field for current-user direct file, audio, and video uploads, then uses the Chat receiver field for final message delivery.
- **CLI compatibility governance** — adds a reviewed two-stage path for hiding retained legacy commands or optional `NoOpt=true` boolean flags from Help and Schema when their activated capability moves to a dedicated command, with legacy-leaf, complete parameter/constant mapping, durable runtime constant evidence, protected framework bridges, dry-run preservation, parameter-collision, and fail-closed required-parameter checks.
- **OA admin approval query** — `oa approval list-by-admin` queries approval instances of a template with admin scope, with simple flags and an advanced `--request` mode; `startTime`/`endTime` use `yyyy-MM-dd HH:mm:ss` strings per the 2026-08 MCP contract update (ISO-8601 flag inputs auto-convert), and pageSize/time format are validated client-side with localized errors.
- **Chat personal emotions** — adds `chat emotion list`, `chat emotion send`, and `chat emotion favorite` for current-user personal favorite emotion listing, sending, and favoriting.
- **Minutes, DingTalk tasks, and Wiki parameter aliases** — adds reviewed parameter-name normalization, ambiguity guards, and end-to-end payload coverage for the three products.
- **Calendar empty windows** (#1074) — returns a legitimate empty result when the service emits its exact exhausted empty-event sentinel.
- **Task update verification** (#1074) — compares due-time readback as exact milliseconds so committed updates are no longer reported as failures.
- **Comment reaction validation** (#1074) — narrows accepted reaction input to reviewed DingTalk emoji names and rejects Unicode emoji and unsupported names such as `like` and `heart` before the RPC.
- **Stable release sealing** — directly preparing a stable release now renders and archives release fragments merged after its beta baseline, avoiding a forced extra beta solely to consume pending notes.
- **Sheet revision changesets** — adds read-only commands for querying the current workbook revision and reviewing Agent-readable changes between revisions, with guidance for distinguishing revisions from saved history versions and safely selecting rollback targets.
`Could not inspect existing reviews for PR #${pullNumber}; skipping reviewer routing to avoid a duplicate request (${error.status || 'unknown status'}).`,
echo "- Success means every configured channel was verified and the permanent withdrawn/${VERSION} tombstone remains as the version-reuse barrier."
echo "- Failure may occur before or after the tombstone/channel mutations; inspect the failed step and rerun the exact same inputs after fixing the cause."
echo "- The problem GitHub Release and original tag are removed after npm and every tag-enabled/configured mirror are rolled back, so GitHub installers stop resolving the bad version while the Homebrew rollback PR is reviewed."
echo "- npm is deprecated rather than unpublished; already-installed clients cannot be remotely downgraded."
echo "- If a Homebrew rollback PR was opened, this run remains failed until that PR is independently reviewed, merged, and the workflow is rerun."
@@ -5,20 +5,75 @@ unrelated work, and use `gofmt` for every modified Go file.
## Build and test
- Build: `go build ./cmd`
- Build: `make build` (wraps `scripts/dev/build.sh` → `go build -o dws ./cmd`; bare `go build ./cmd` fails because output name `cmd` collides with the directory)
- Full test suite: `DWS_PACKAGE_VERSION=0.0.0-test go test ./...`
- **Declare = final Schema source**: `Flags` / `Constraints` / `Safety` / `ConstParams` / `Contract` (`corecmd.ContractDecl`; nested fields are `contract.*`)
- Naming: `ContractDecl` is the authoring leaf declaration. "Schema" means Catalog / `ToolSpec` delivery — do not reintroduce `SchemaDecl`.
-`Safety` uses `contract.SafetySpec` (`internal/corecmd/contract` only — no `cli.*` type alias). Its `confirmation` drives the runtime gate; `effect` / `risk` / `idempotency` are published unchanged. When `Contract` is set, convert once via `contractfinal.RegisterRuntimeContractFinal` (all callers — `corecmd.New` registers internally); assembly **pass-throughs** Final.
- ContractFinal cobra store + Register → `internal/corecmd/contractfinal` (framework-owned)
- homology gates → `internal/cli/homology`
- Catalog assembly / `ResolveMeta` (`RegisterSchemaSourceRoot` → `ResolveSchemaBuild`); go:embed only for reviewed inputs → `internal/cli` root (package-local aliases for annotate/store APIs live in `runtime_schema_seam.go`; the former `cli/runtimeannotate` / `cli/contractfinal` shim packages are removed — import `corecmd/*` directly)
- **Hard rule**: `internal/corecmd` (and its subpackages) must **not** import any `internal/cli` package
- **Tier2** — `DeclareLeafMetadata` (helpers migration; **Shortcut may also use this path — acceptable**)
- **Tier3** — bare Cobra (should shrink over time; reviewed exclusions where needed)
- Long-term outlook only: broader mcpbind / fewer hand-written `Execute` bodies. **Not** a current hard requirement to delete `Shortcut.Execute` or force mcpbind.
- Description declare vs delivery: construction requires `ContractDecl.Description` (evidence). Catalog delivery prefers Cobra Long → provenance `cobra_help`; without Long, declared text → `contract_final`. Title: declared first, then Short, then MCP. Do **not** read this as "declare = wire final" or dual authority.
- **Execute** = hooks (`Validate` / `Call` / `RunE` / `PostMount`) — not a second surface authority
- Declaration path has **no reviewed parallel fields**; migration-only `runtime_gate` annotate until `Safety` is declared
- **Do not add** new production `AnnotateRuntimeRisk` / `AnnotateRuntimeGate`
(`runtime_gate`) call sites; migrate leaves to declared `Safety` /
`ContractDecl` instead. Existing annotate sites may remain until migrated.
## flag / help / schema homology
- Decision (path A — Contract/LeafSpec is CLI-surface authority **and must embed into Schema**): `docs/flag-help-schema-homology.md`
- Hard rule: every help/Schema fact is **declared****or****annotated**; never inference-only (§1.1–§1.3; framework §5.0).
- MCP metadata must not create CLI flags; optional 1:1 passthrough is a gated subset only.
- Gate IDs: `HOM-P*`, `HOM-S*`, `HOM-I1`, `HOM-D1` (see that doc §3–§4). `HOM-P1`/`HOM-D1`/`HOM-S1`/`HOM-S2` are on the `check-schema-catalog.sh` policy whitelist; remaining IDs land incrementally.
## Agent Schema contract
@@ -27,72 +82,133 @@ The Schema data flow is one way:
└─ ResolveMeta projects Identity/Safety/Selection from the same registry
└─ CI may dump Catalog via cmd_schema_catalog for jq gates / determinism
```
Parameter overlays from metadata are merged into `EffectiveCommandRegistry`
*before* Cobra binding; after that point there is no second identity source and
no identity precedence winner. The binder must reject a missing/non-runnable
Cobra path, an alias collision, and any native identity annotation that
disagrees with the effective registry. A missing native identity annotation is
allowed because annotations are implementation-side assertions, not identity
fallbacks.
**Reviewed inputs / 评审输入** (organizational family under `internal/cli`;
parallel peers, not one merged authority). These are assembly inputs only —
never Catalog declaration authority, never leaf `Contract` / `ProductDecl`
substitutes. Keep them side-by-side; do **not** fold one into another:
| Input | Path | Owns |
|---|---|---|
| Command identity | collected from `ContractFinal.Identity` on live Cobra leaves (`schema_identity_collect.go`; not a file input) | stable identity, primary CLI path, aliases, navigation |
-Edit only its `metadata/<product>.json` and `selection/<product>.json`.
- Read Skill, Cobra/`--help`, Runtime confirmation sites, and live
`dws schema <leaf> --compact` for its tools. Mapping/interface/provenance
audits may query the full leaf only through a narrow `--jq` / `--fields`
projection; do not load an entire full leaf into Agent context.
-Hand-write selection prose and leaf Contract / ProductDecl declarations;
forbid wholesale JSON merges from review dumps.
- Edit only its product’s leaf declarations (and `ProductDecl` when needed).
- **Never** `git checkout` unrelated product files to “clean scope”.
### Regenerate and gates
@@ -218,23 +337,24 @@ make generate-schema
go test ./internal/app -run '^TestSheetFinalSchemaConfirmationMatchesRuntimeGuards$' -count=1
```
`check-runtime-confirmation-truth.sh` compares live ContractFinal.Safety with the assembled ToolSpec `confirmation=user_required` and probes the runtime gate.
`schema_hints/` must stay absent.
Example rules (fail generation otherwise):
- At most two examples per tool; no `--yes` in stored examples.
- Examples must match live Cobra argv (path, flags, required groups).
- No shell comments in examples.
After generation, spot-check Catalog: selection provenance is
`reviewed_explicit` from `selection/`, and `user_required` count equals
metadata `runtime_gate != none`.
After generation, spot-check Catalog: selection and safety/interface
provenance are `contract_final` from ProductDecl / leaf declarations
(`user_required` must match Runtime confirmation gates).
`make generate-schema`is a full deterministic snapshot rebuild, not an
incremental patch over the previous Catalog. It rereads every reviewed input,
removes stale generated product metadata, and rewrites the exact metadata and
Catalog projections. Incremental work happens only when an Agent or human
edits selected `metadata/` or `selection/` entries; the next publication still
recomputes all outputs. Generated files must never be read back as merge input,
and byte guards fail generation if it changes the hint inputs or CommandRegistry.
`make generate-schema`refreshes `param_aliases_generated.go` and runs
assembly determinism (`check-schema-assembly.sh`). It does not rewrite a
committed Catalog as delivery authority — runtime reassembles from
declarations. Byte guards fail if generation mutates parameter-concept
inputs; policy fails if the retired `schema_command_registry/` reappears.
Selection prose may choose a more or less restrictive recommendation. It cannot
create a Cobra command or flag, change parameter facts, invent an
| `data_schema` | yes | One recursive JSON Schema **object** describing only the runtime envelope's `data` value. Every named `properties` child must have a non-empty `description`. It must not duplicate `ok`, `outcome`, `error`, or `meta`. |
| `sensitive_paths` | no | Unique safe dot paths relative to `data`; renderers/redaction consumers must not treat them as shell/JQ expressions. |
Optional members are omitted, never emitted as `null`. A leaf without a
reviewed Result omits the entire `result` key. Compact must preserve the same
normalized Result value as the full leaf; it must not summarize, infer, rename,
or independently rebuild any Result field. Product/group summaries do not
aggregate child Result objects.
`pagination` is a sibling of `result`, not a child. It declares the canonical
CLI cursor parameter and the fixed framework paths under `meta.pagination`.
Product response fields used to derive that metadata remain mapper internals;
they are not part of `result.data_schema`. Do not execute a second request to
derive pagination metadata.
Invalid result declarations fail closed during normalization: unknown or
duplicate outcomes, a non-object/multiple `data_schema`, unsafe or duplicate
sensitive paths, unsupported pagination kinds, attempts to override framework
meta paths, and an invalid cursor parameter must be rejected rather than
silently removed.
Full-leaf wire round trips must
preserve the normalized Result exactly. Do not commit generated Schema JSON as
evidence; tests construct contracts in Go and runtime/CI assemble the Catalog
from declarations.
### Performance model and rules
- Catalog construction is declaration-driven and cached through the existing
lazy `sync.Once` delivery path. Do not reassemble or reopen annotations per
command invocation, per leaf lookup, or per renderer.
- Normalizing one Result declaration is linear in the size of that declaration.
Full `schema --all` is linear in tools + parameters + Result schema bytes and
is an audit/compatibility export, not the normal Agent discovery path.
Overview → compact product/group → compact leaf remains the normal route;
only the final leaf carries its Result declaration.
- Constructing a `CommandResult` defensively clones result data and validates
invariants; rendering is buffer-first and then writes once. Both CPU cost and
transient memory are O(payload size), with roughly one additional in-memory
rendered copy. This buys immutability and prevents partial JSON leakage, but
it is not free.
- Large list/search commands must use bounded pages and publish continuation
facts. The current emitter buffers one command result/page before publishing;
pagination is the memory bound. Continuous event streams are a separate,
command-specific protocol and are not described by `ResultSpec`.
- A `dual_validate` command must execute the business request exactly once,
validate a shadow unified result, and preserve legacy bytes. Never obtain
validation by issuing a second network or write request.
- Filters and alternate formats are render-time work over the same in-memory
result. They must not rerun the business operation or rebuild Schema.
- Performance changes must preserve the one-result, buffer-first, fail-closed,
and atomic `--output` guarantees. Do not trade correctness for a microbenchmark
improvement. For a material hot-path change, benchmark representative small
and page-sized payloads and report allocations/bytes as well as latency.
## Current Schema boundaries
-`schema list` remains a progressive overview. `schema --all` is the stable
@@ -345,13 +600,16 @@ path; a generator unit test or JSON count alone is insufficient.
`parameters` object for commands without flags. Keep it suitable for the #602
compatibility baseline and fail rather than silently emitting a partial
export.
-`schema --all` is not normal command discovery. Use overview -> product/group
-> leaf for routine Agent work. `--compact` is supported for context-saving
projections, but a compact full export is not a complete compatibility
baseline.
-`schema --all` is not normal command discovery. Use overview -> compact
product/group -> compact leaf for routine Agent work. `--compact` is the
reviewed positive-field allowlist for Agent context: new full/audit fields
must not appear there until explicitly reviewed. A compact full export is not
a complete compatibility baseline.
-`dws <path> --help` defines whether Cobra exposes a path and which flags the
executable accepts. A leaf Schema defines Agent selection, parameter mapping
and constraints, and safety/confirmation semantics. A conflict is contract
drift, not permission to guess.
executable accepts. A compact leaf defines Agent selection, CLI parameters,
constraints, safety/confirmation semantics, and any reviewed `result`
contract. Full leaf fields such as `property`, `interface_ref`, and
provenance are audit facts. A conflict is contract drift, not permission to
guess.
- Schema and Help describe commands; neither returns DingTalk business data.
After discovery, execute the real read/search/list command to obtain data.
| Documentation-only | Prose and documentation assets with no executable, generated, workflow, packaging, or interface change | Links/content/rendering plus repository asset checks | Lightweight documentation validation; all nine named contexts still report |
| Standard | Ordinary implementation work with a stable package graph | Focused unit/integration tests and observable behavior for the changed path | Race tests for changed packages and their reverse dependencies, scope-matched HEAD/base coverage, and representative Darwin/Windows compilation |
| High-risk | Workflow/policy, package graph, generated Schema/registry, platform, auth/keychain, installer, packaging, release, transport, recovery, or an unprovable infrastructure change | Relevant full or domain suite plus focused behavior evidence | Complete race suite, native platform tests, and all affected domain gates; protected `main` uses this tier |
Classification fails closed: an incomplete diff, package add/remove/rename, or
uncertain dependency graph selects the high-risk suite. Native changed-code
coverage is additionally selected for platform-sensitive code.
## Pull Request Checklist
1. Keep implementation and tests in sync.
2. Run `./scripts/dev/ci-local.sh`.
3. Run `./scripts/policy/check-command-surface.sh --strict` when command paths/flags change.
4. Run `./scripts/policy/check-generated-drift.sh` when generated artifacts may change.
5. Run `./scripts/release/verify-package-managers.sh` when packaging or installer surfaces change (run `make package` first).
6. Update docs and `CHANGELOG.md` for behavior/interface changes.
7. Include verification evidence in your PR description.
2. Select the documentation-only, standard, or high-risk tier and run the
smallest checks that prove the change. Use `./scripts/dev/ci-local.sh` when
a complete local pass is warranted; it is not required for every ordinary
PR.
3. Include both the commands/results and user-visible or contract-level
behavior evidence in the PR description.
4. Run `./scripts/policy/check-command-surface.sh --strict` when command
paths/flags change. CI resolves the exact merge-base, latest reachable
non-withdrawn stable GA tag, and committed candidate SHA, then enters the single compatibility
| **mono**(legacy) | One `dws` skill coveringall products | Cross-product workflows; single entry point |
> 🧪 **`multi` is currently EXPERIMENTAL / preview.** All product-scoped skills pass the dispatch verifier, but interface, naming and cross-skill references may change in future releases. For production / shared environments, prefer `mono`. File issues if you hit problems.
> Installs and upgrades default to `multi`. `mono` remains available via `DWS_SKILL_MODE=mono` or `dws skill setup --mode mono`. File issues if you hit problems.
- **TTY install** (download then run): `curl -O .../install.sh && bash install.sh` — prompts `1) multi 2) mono` (default 1).
- **Override via env**: `DWS_SKILL_MODE=mono curl -fsSL ... | sh`.
- **Switch later**: `dws skill setup --mode mono` (or `--mode multi`) — review the listed paths and confirm interactively.
</details>
@@ -210,7 +210,7 @@ The verifier uses isolated directories and does not replace the `dws` on the cur
The upgrade process follows a two-phase atomic flow to ensure consistency:
1. **Prepare** — downloads the platform-specific binary and skill packages to a temporary directory, verifies SHA256 checksums, and extracts/validates all files. If any step fails, the upgrade aborts without modifying the existing installation.
2. **Apply** — only after all preparations succeed, the binary is replaced and skill packages are installed to all detected agent directories (`~/.agents/skills/dws`, `~/.claude/skills/dws`, `~/.cursor/skills/dws`, etc.).
2. **Apply** — only after all preparations succeed, the binary is replaced and skills are flattened into the canonical `~/.agents/skills` root. Agents classified by the pinned compatibility registry as supporting the universal root read it directly; other detected Agents receive links to the canonical copy, with a direct-copy fallback when links are unavailable. Older DWS-managed agent-specific copies are backed up and retired so the same Skill is not discovered twice.
A backup of the current version is automatically created before each upgrade. Use `dws upgrade --rollback` to restore the previous version if needed.
@@ -283,16 +283,22 @@ Credentials are securely persisted after first login (Keychain). Subsequent runs
`dws` can stay logged in to several DingTalk organizations at once. Each organization is one **profile**; the current profile decides which org a command runs against (credentials are stored per organization).
`dws` can stay logged in to several DingTalk accounts at once, including multiple accounts in the same organization. A profile is uniquely identified by `corpId:userId`; the current profile decides which identity a command runs as.
```bash
dws auth login # log in to another org → adds a profile (first login becomes the primary)
dws profile list # list logged-in orgs (primary / current marker, status)
dws profile switch <name|corpId> # switch the default org (use - to toggle back to the previous one)
dws --profile <name|corpId> contact user search --query "..." # run one command against a specific org, without changing the default
dws auth login # add or refresh one account
dws profile list # list every logged-in account
dws profile switch <corpId:userId> # persistently switch; use - to toggle back
dws profile switch "<corpName>:<userName>" # friendly input; names must be unique
dws --profile <corpId> contact user search --query "..." # use that org's explicitly recorded current account
dws --profile <corpId:userId> contact user search --query "..." # use one exact account without changing the default
```
Cross-org reads are orchestrated by the agent rather than a built-in `--all-orgs`: list the profiles, run the query per org with `--profile`, then merge. Writes default to the current org only — confirm the target org before writing across orgs.
Selectors support `corpId:userId`, `corpId:userName`, `corpName:userId`, and `corpName:userName`. Friendly names are input aliases only; use the stable `profile` value returned by `profile list` for automation. Duplicate organization or account names fail with explicit `corpId:userId` candidates. If an organization has multiple accounts but no recorded current account, `--profile <corpId>` fails instead of choosing the first or most recently used account.
`currentProfile`, `previousProfile`, and per-organization defaults are stored as exact identities. `primaryProfile` remains in JSON only for compatibility and is not used for selection. `profile list` reads status and expiry from each real identity Token without refreshing it. `auth logout --profile <corpId>` removes all local accounts in that organization; an exact selector or local profile name removes one account.
Cross-org reads are orchestrated by the agent rather than a built-in `--all-orgs`: list profiles, group by `corpId`, and use the unique `isOrgCurrent=true` account for each organization. If a multi-account organization has no default, ask the user to choose an account first. Writes default to the current account — confirm both organization and account before cross-org writes.
On macOS, an unreadable registered token slot blocks a new OAuth login rather than risking a mixed Keychain/file-DEK state. If normal terminal commands can still read the login while a sandbox using `DWS_DISABLE_KEYCHAIN=1` cannot, migrate the legacy and profile auth entries without exposing tokens:
DWS_DISABLE_KEYCHAIN=1 dws auth status --format json
```
The migration validates every selected auth ciphertext before writing, ignores unrelated application secrets, and can be rerun after an interrupted commit. If validation identifies genuinely damaged ciphertext, remove only the affected profile with `dws auth logout --profile <name|corpId>`, then log in again. Use `dws auth reset` only when you intend to discard every local profile.
The migration validates every selected auth ciphertext before writing, ignores unrelated application secrets, and can be rerun after an interrupted commit. If validation identifies genuinely damaged ciphertext, remove only the affected account with `dws auth logout --profile <corpId:userId>`, or all accounts in one organization with `--profile <corpId>`, then log in again. Use `dws auth reset` only when you intend to discard every local profile.
Use Cobra help and Schema for different parts of the command contract:
- `dws <path> --help` is the source of truth for whether a command exists and which flags the binary accepts.
- `dws schema "<path>"` is the Agent contract for command selection, parameter mappings and constraints, risk, and confirmation semantics.
- `dws schema "<path>" --compact` is the normative Agent view for command selection, CLI parameters and constraints, risk, and confirmation; use a full leaf with a narrow `--jq` projection for mapping or provenance audits.
- If Help and Schema disagree, treat it as contract drift: pass only flags accepted by Cobra and use the more conservative safety semantics.
- Schema describes commands; it does not read or search DingTalk business data. Execute the real product command after discovery.
@@ -371,32 +380,32 @@ Use Cobra help and Schema for different parts of the command contract:
dws aitable record query --help
# Discover within a product, then inspect the selected leaf contract
dws schema aitable
dws schema "aitable record query"
dws schema aitable --compact
dws schema "aitable record query" --compact
# Execute the real business query
dws aitable record query --base-id BASE_ID --table-id TABLE_ID --limit 10
```
`dws schema --all` exports the complete contract for tooling, CI, audits, and compatibility baselines. Agents should prefer product/group discovery followed by a leaf query to avoid loading the full Catalog into context.
`dws schema --all` exports the complete contract for tooling, CI, audits, and compatibility baselines. Agents should query progressively with `--compact`; its positive field allowlist prevents new full/audit fields from silently expanding Agent context.
### Agent Skills
The repo ships a complete Agent Skill system under `skills/`, organized into two layouts:
- `skills/multi/` — per-product skills (`dingtalk-aitable/`, `dingtalk-calendar/`, `dingtalk-chat/`, ...), each with its own `SKILL.md`. 🧪 **EXPERIMENTAL / preview — see banner in each multi `SKILL.md` for caveats.**
- `skills/multi/` — per-product skills (`dingtalk-aitable/`, `dingtalk-calendar/`, `dingtalk-chat/`, ...), each with its own `SKILL.md`. Default layout.
Shared reviewed inputs for Schema generation live separately under `internal/cli/schema_hints/`. They are not Agent Skills and are excluded from binaries and release skill bundles.
Leaf safety/parameters/selection prose for Schema generation come from ProductDecl / ContractFinal declarations in Go. The former `internal/cli/schema_hints/` HintFile tree is fully retired and must not reappear.
After installing, AI tools like Claude Code / Cursor can operate DingTalk directly through natural language:
```bash
# Install skills into current project (defaults to mono)
# Install skills into current project (defaults to multi; DWS_SKILL_MODE=mono switches back)
curl -fsSL https://raw.githubusercontent.com/DingTalk-Real-AI/dingtalk-workspace-cli/main/scripts/install-skills.sh | sh
> Installers use`$HOME/.agents/skills/` as the canonical global store, following the universal `.agents/skills` convention. Agents classified by the pinned compatibility registry as universal read that root directly; detected non-universal Agents receive links to it (or copies when links are unavailable). Multi layout is per-product siblings, while mono uses the `dws/` subdirectory.
>
> China users: prefix `DWS_GITEE_REPO` to use the Gitee mirror — see [China mirror](#china-mirror).
| `--yes` | — | Scripting-only: skip the confirmation prompt. Removals are still backed up to `~/.dws/skill-backups/` first |
> The setup command can remove the opposite-mode layout (`dws/` for multi, DWS-managed multi Skills for mono) and stale managed Skills not in the bundle. DWS records ownership, installer version, source, and content digest centrally in `~/.dws/skills-state.json` (or `$DWS_CONFIG_DIR/skills-state.json`). Exact official names shipped before the centralized state remain a frozen migration list. A `dingtalk-*` prefix alone never authorizes cleanup, so other same-prefix market/user Skills are preserved. Every removal is previewed before confirmation and preserved under `~/.dws/skill-backups/<timestamp>/`; a directory that cannot be backed up is never removed. In a non-interactive shell, first run `--dry-run` and inspect its output; only then may the caller explicitly choose the scripting-only confirmation bypass.
After a multi setup or upgrade, DWS stores the official bundle snapshot and centralized ownership metadata in `~/.dws/skills-state.json` (or `$DWS_CONFIG_DIR/skills-state.json`). Every upgrade installs and overwrites the complete bundled Skill set from that release. Deleting or excluding a bundled Skill is not sticky: the next upgrade restores it. `dws upgrade --force` additionally allows reinstalling the current CLI version when no newer version is available.
Env vars: `DWS_SKILL_MODE=mono|multi` (also honored by `install.sh` / `install.ps1`), `DWS_SKILL_SOURCE=<path>`.
<summary><strong>Personal Event Subscription</strong> — real-time DingTalk messages for event-driven agents</summary>
`dws event consume` subscribes as the currently logged-in user over a managed Stream WebSocket and emits each event as one NDJSON line on stdout. The public catalog currently covers messages that mention the current user, one-to-one messages with a specified user, and messages in a specified group.
`dws event consume` subscribes as the currently logged-in user over a managed Stream WebSocket and emits each event as one NDJSON line on stdout. The public catalog covers scoped and all one-to-one/group messages, specified senders, read/recall/reaction events, group lifecycle events, and seven OA approval task/instance events.
The default `ndjson`, `json`, and `pretty` output preserves the transport envelope (`type`, `event_type`, string `data`, and `headers`) for existing scripts; `compact` retains its existing processor. Add `--flatten` to emit the stable top-level business fields used by Agent workflows. `--format` controls JSON serialization; `--flatten` controls the data structure and cannot be combined with `-f raw` or `--debug-raw-events`.
> **Prerequisite**: run `dws auth login`. Personal identity is resolved from the OAuth token and cannot be supplied through command-line identity flags.
@@ -473,31 +492,67 @@ For an event-focused installation, use the official convenience installer:
```bash
curl -fsSL https://raw.githubusercontent.com/DingTalk-Real-AI/dingtalk-workspace-cli/main/scripts/install-event.sh | sh
# Or install the standalone multi skill from an existing dws installation
dws skill setup --mode multi -s event
```
```bash
# Inspect the public personal event catalog and schema
# Listen for all seven public OA approval events in one process
dws event consume \
user_oa_approval_task_created \
user_oa_approval_task_finished \
user_oa_approval_task_redirected \
user_oa_approval_instance_started \
user_oa_approval_instance_cc \
user_oa_approval_instance_terminated \
user_oa_approval_instance_finished \
--flatten -f ndjson
# Inspect local consumers and cancel a subscription
dws event status
dws event stop <subscribe_id>
```
For one-to-one and specified-sender events, use exactly one target identity: `--user` for an internal `userId`, or `--open-dingtalk-id` for an `openDingtalkId`. The CLI does not infer or convert between these identity types.
| Feature | Details |
|---------|---------|
| Managed lifecycle | `consume` creates or reuses the personal subscription; `stop` cancels it and cleans local state |
| Shared connection | Consumers for the same user share one local bus and cloud connection |
| Multi-event process | One consume process can listen for compatible events for the same target while retaining one subscription per event |
| Subscription isolation | Normal consumers match both event type and `subscribe_id` |
| Agent-friendly output | Stream events are written to stdout as NDJSON; status and diagnostics use stderr |
| Observability | `status` shows remote subscriptions, the personal bus, and local consumers |
| Todo | `todo` | Create / list / update / complete tasks and comments |
@@ -683,7 +738,7 @@ See [`docs/robot-quickstart.md`](./docs/robot-quickstart.md) for the full 4-step
<summary>Coming soon</summary>
- `conference` (video meetings)
- Multi-skill mode (experimental) — per-product skills under `skills/multi/`; opt in via `dws skill setup --mode multi`
- Multi-skill mode (default) — per-product skills under `skills/multi/`; installs and upgrades default to it, `dws skill setup --mode mono` switches back after interactive confirmation
</details>
@@ -732,6 +787,7 @@ See [`docs/robot-quickstart.md`](./docs/robot-quickstart.md) for the full 4-step
## Reference & Docs
- [International DingTalk (`.io`) guide](./docs/international-region-guide.md) — international login, domestic/international profile switching, isolated testing, and troubleshooting
- [Command Index](./docs/command-index.md) — every runtime command with description and when-to-use guidance
`dws` is a Go CLI with a versioned, static command surface for DingTalk MCP capabilities. Cobra help serves humans; the embedded Command Catalog serves AI agents.
`dws` is a Go CLI with a versioned, static command surface for DingTalk MCP capabilities. Cobra help serves humans; runtime-assembled Schema (`ResolveSchemaBuild`) serves AI agents.
## High-Level Flow
@@ -9,8 +9,8 @@
3. `internal/helpers` contains the main command handlers for all product surfaces (`dev`, `chat`, `calendar`, `contact`, `aitable`, etc.).
5. `internal/auth` manages login state, PAT tokens, and agent-code detection.
6. Schema generation starts from the reviewed `CommandRegistry`, binds each identity to the exact current Cobra leaf, and then resolves typed constraints, sanitized MCP snapshots, Agent hints, and Skills into one `SchemaRegistry`. Startup and Schema queries do not call MCP `tools/list`.
7. The embedded Catalog is a downstream release artifact and never backfills identity or participates in regeneration. Stable flag-to-interface property bindings come from the reviewed, content-addressed v3 manifest in `schema_parameter_bindings.json`; its exact active tuples, corrections, removals, and mappingexclusions are validated against the final bound`SchemaRegistry`. CLI `required` and constraints come from the resolved typed contract, while MCP `required` remains interface-only metadata.
6. Schema assembly (`ResolveSchemaBuild`) starts from the reviewed `CommandRegistry`, binds each identity to the exact current Cobra leaf, and then resolves typed constraints, sanitized MCP snapshots, and leaf ContractFinal / ProductDecl into one `SchemaRegistry`. Startup and Schema queries do not call MCP `tools/list`. There is no generate-written Catalog delivery step.
7. Production Catalog / `ResolveMeta` consume the lazily assembled registry via `RegisterSchemaSourceRoot` → `ResolveSchemaBuild` / `deliverySchemaCatalog` (声明即 Catalog; lazy `sync.Once`). `ResolveMeta` projects Identity/Safety/Selection from that assembly into an in-process map cache — not a committed `schema_catalog/` or `schema_meta_index.*` fixture. Flag-to-interface property delivery is owned by leaf `ParamDecl.Property` (native annotations). `schema_parameter_mapping_ledger.go` holds reviewed `mapping_exclusions` / `removals` (the empty`schema_parameter_bindings.json` audit table is retired). CLI `required` and constraints come from the resolved typed contract, while MCP `required` remains interface-only metadata.
8. Agent selection results are fixed in versioned review inputs. Every public tool has explicit use/avoid/example and interface disposition metadata; Skill references that are not current leaves require an explicit alias/group/stale/out-of-surface review instead of fuzzy runtime matching.
`Lint` resolves the complete base/head diff before any helper is skipped.
Unknown or truncated input fails closed into the high-risk tier.
| Tier | Selection | Admission work |
|---|---|---|
| Documentation-only | Only prose/documentation assets; no executable, generated, workflow, packaging, or interface surface | Documentation and repository-asset validation; expensive code helpers skip while every required context still succeeds |
| Standard | Ordinary code change with a stable package graph | Race tests for changed Go packages and their reverse dependencies; candidate and merge-base coverage over the same impacted scope and `coverpkg`; representative Darwin/Windows compilation |
| High-risk / protected `main` | Workflow/policy, package add/remove/rename, generated Schema/registry, platform, auth/keychain, installer, packaging, release, transport, recovery, or an unprovable infrastructure classification | Complete race suite and full native macOS/Windows tests, plus every affected domain gate |
Domain helpers (`Edition`, `Interface Integrity`, `CLI Smoke`, and `Mock MCP`,
for example) execute their substantive suites when the diff can affect that
contract or when the high-risk tier is selected. Otherwise their stable named
contexts still report a successful, explicit unaffected result. Release-script
tests follow the same impact rule. This preserves the ruleset contract without
charging every developer for unrelated work.
Platform-sensitive changes additionally run native changed-code coverage.
Protected `main` always runs native tests; generic portable changes are held to
the Linux changed-code gate rather than being forced to manufacture
platform-only coverage.
Complete `Multi-profile E2E` is not a PR admission context. It belongs to the
`Main Integration — 主干集成` workflow and runs only after a push to `main` (or
an explicit manual dispatch). A failing downstream run remains a real
regression and must be repaired, but it must not be represented by a synthetic
successful PR check.
```mermaid
flowchart TB
PR["Pull request"] --> ADMISSION["CI"]
ADMISSION --> L["Lint"]
ADMISSION --> T["Test"]
ADMISSION --> C["Coverage"]
ADMISSION --> P["Policy"]
ADMISSION --> E["Edition"]
ADMISSION --> I["Interface Integrity"]
ADMISSION --> A["AI Behavior"]
ADMISSION --> S["CLI Smoke"]
ADMISSION --> M["Mock MCP"]
ADMISSION --> MAIN["Protected main"]
MAIN --> NATIVE["Full native platform matrix"]
MAIN --> E2E["Multi-profile E2E"]
MAIN --> RELEASE["Release delivery"]
```
## Review ownership and auto-merge
A base-owned `pull_request_target` workflow routes newly opened, updated,
reopened, or newly ready PRs targeting `main` to one eligible peer reviewer. It
does not check out or execute PR code, excludes both the author and the known
latest pusher, and balances the open requested-review load across the reviewed
maintainer pool. A current-head approval or change request is preserved; after
a new push, stale activity does not suppress a fresh request, and an
outstanding change requester is preferred for continuity.
The branch ruleset keeps one human approval and all nine strict required
contexts, and requires someone other than the latest pusher to approve after
the most recent head update. Repository auto-merge is enabled for ready PRs,
so a PR merges after that approval and the current revision's nine checks are
green. If `main` advances, strict checks rerun before merge. The reviewer
router is orchestration, not a quality context, and must not be added to the
ruleset.
## Running focused gates locally
Run the contracts relevant to the change. Ordinary contributors are not
expected to repeat every CI job locally:
```sh
make build
make interface-integrity
make authoritative-interface-integrity BASE_REF=<merge-base>
make schema-compatibility BASE_REF=<merge-base>
make policy
make interface-integrity BASE_REF=<merge-base> STABLE_REF=<stable-tag> CANDIDATE_REF=<candidate-sha>
make schema-compatibility BASE_REF=<merge-base> STABLE_REF=<stable-tag> CANDIDATE_REF=<candidate-sha>
make skill-command-integrity
make cli-smoke
# Run on the corresponding native runner with its generated profile:
make coverage-gate-platform BASE_REF=<merge-base> PROFILE=<coverage-profile>
make mock-mcp-smoke
go test -v -count=1 ./pkg/editiontest/...
```
`make coverage-gate` is the enforcement step, not a profile generator. It
expects the candidate, policy, and merge-base profiles (`coverage.txt`,
`coverage-policy.txt`, and `coverage-base.txt`) produced by the preceding CI
steps. A clean local checkout can reproduce the Linux/overall CI gate with:
CI 先解析并核对精确的 merge-base、最近可达且未撤回的 stable GA tag 和已提交的 candidate
1. `flag_rename`:保留旧 flag 的可执行兼容性,但把它从 Help 与 Agent Schema 中隐藏,并将新的规范 flag 设为唯一可见入口;rename 必须保持原 flag 的 requiredness,optional 只能迁到 optional,required 只能迁到 required。
2. `requiredness_change`:同一个公开 flag 从 optional 精确提升为 required;flag 的名称、类型、作用域、可见性、shorthand、`no_opt` 与 alias 关系必须保持不变。
Some files were not shown because too many files have changed in this diff
Show More
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.