merge: bring canonical main into direct profile invocation branch
This commit is contained in:
@@ -21,14 +21,15 @@ primary sources. In the default mode, poll at a 60-second cadence for at most
|
||||
review feedback, or a terminal clean state. Keep polling directly in this mode:
|
||||
`scripts/pr-await` waits for every check to finish, which cannot return early on
|
||||
a failure or conflict while other checks are still pending. If the caller says "wait N
|
||||
minutes" or "then fix up", use strict-deadline mode instead: the 20-minute cap
|
||||
does not apply there — honor the caller's own N-minute deadline. Those
|
||||
semantics are exactly what `scripts/pr-await <PR> --deadline-min N` implements,
|
||||
so prefer it there and report its single output. Without that script, still poll
|
||||
at a 60-second cadence but for the caller's full N minutes: calculate and
|
||||
include the absolute deadline in the polling prompt, accumulate findings, and
|
||||
do not return early for findings, pending checks, or a clean snapshot. Stop
|
||||
early only if the PR is merged/closed or access is revoked. At the deadline,
|
||||
minutes" or "then fix up", pass the caller's value to
|
||||
`scripts/pr-await <PR> --deadline-min N` as a maximum deadline. The helper can
|
||||
return early when checks are terminal; do not describe that as a full-duration
|
||||
wait. If the caller explicitly requires the full N minutes, the helper has no
|
||||
minimum-wait mode: without that script, poll at a 60-second cadence for the
|
||||
caller's full N minutes, calculate and include the absolute deadline in the
|
||||
polling prompt, accumulate findings, and do not return early for findings,
|
||||
pending checks, or a clean snapshot. Stop early only if the PR is merged/closed
|
||||
or access is revoked. At the deadline,
|
||||
return the latest named pending checks and named actionable review findings,
|
||||
not only aggregate CI and review states.
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ change and a small diff because this limits risk and maintainer burden.
|
||||
## Available skills
|
||||
|
||||
- **`/tdd`** — Recommend when flagging untested logic. The author can use this to add tests.
|
||||
- **`/mobile-parity`** — Required when a review touches frontend or user-facing UI, including scrolling, visibility, or activation behavior, even when the change is not described as responsive.
|
||||
|
||||
## Steps
|
||||
|
||||
@@ -42,6 +43,14 @@ head if needed, and use that immutable SHA in the diff. If the current PR head
|
||||
cannot be fetched, say so rather than reporting a stale checkout as a review of
|
||||
the current PR.
|
||||
|
||||
A Kandev task worktree may hold none of the work: a clean checkout can be an old
|
||||
base while the reviewable code lives on the PR's branch, and prior-session claims
|
||||
that local changes exist may be stale. Before reporting “no changes”, confirm
|
||||
that `git log "$(git merge-base <base-remote>/<base-ref> HEAD)"..HEAD` is
|
||||
non-empty. If it is empty, resolve the PR branch with
|
||||
`gh pr view <N> --json headRefName,headRefOid`, fetch that ref, and review its
|
||||
immutable head, stating why it is not the local branch.
|
||||
|
||||
Record the base and head SHA for each review round. A new contributor push starts
|
||||
a new round: reassess prior findings and the verdict against the new head, and
|
||||
verify checks or workflow results for that head rather than relying on a PR
|
||||
@@ -49,9 +58,15 @@ number or author summary. From `scripts/pr-state --summary`, also record
|
||||
`pr.base_ref_name`, `pr.base_head_oid`, `pr.merge_base_oid`, and
|
||||
`pr.base_advanced_since_head` when available.
|
||||
|
||||
For a follow-up round, inspect `<previous-reviewed-head>..<current-head>` first
|
||||
to isolate the author's response, then re-evaluate `<base>...<current-head>` for
|
||||
complete PR coverage. Record both immutable heads.
|
||||
For a follow-up round, inspect `<previous-reviewed-head>..<current-head>` first,
|
||||
then re-evaluate `<base>...<current-head>` for complete PR coverage. Record both
|
||||
immutable heads. The first range does not isolate the author's response when the
|
||||
branch merged or rebased onto an advanced base: it also carries every upstream
|
||||
commit that arrived with it. Use `rtk proxy git log --oneline --first-parent
|
||||
<previous-reviewed-head>..<current-head>` to list the branch's own commits, then
|
||||
`rtk proxy git show --stat <sha>` per commit to size the response. When history
|
||||
shape decides review scope, use `rtk proxy git log` because the normal RTK
|
||||
wrapper can omit merge commits and truncate subjects.
|
||||
|
||||
When `pr.base_advanced_since_head` is `true`, validate the actual merge result
|
||||
before declaring the PR ready. Record the latest base and immutable head SHAs,
|
||||
@@ -74,11 +89,22 @@ reports hidden unresolved threads, use `pr-resolve list` to inspect them rather
|
||||
than assuming the filtered list is complete.
|
||||
|
||||
Compare the PR description, checklist, and claimed manual validation with that
|
||||
exact head, especially after a major refactor. Report stale claims separately;
|
||||
they are not verification evidence for the current diff.
|
||||
exact head, especially after a major refactor. Treat unchecked static template
|
||||
boxes preserved by `/pr` as intentional; flag only prose or checklist claims
|
||||
factually contradicted by the exact-head diff or evidence. Report stale claims
|
||||
separately; they are not verification evidence for the current diff.
|
||||
|
||||
Read each changed file in full — understand surrounding code, not just the diff. Navigate callers, interfaces, and tests to understand changes end-to-end.
|
||||
|
||||
To read files at an immutable head that is not checked out, create a detached
|
||||
worktree so file and line anchors remain accurate:
|
||||
|
||||
```bash
|
||||
git worktree add --detach /tmp/review-<pr> <head-sha>
|
||||
# inspect the files in /tmp/review-<pr>
|
||||
git worktree remove --force /tmp/review-<pr>
|
||||
```
|
||||
|
||||
For each file, identify which requirement or intent it serves. Flag any changes that don't map to the task — scope creep is a blocker.
|
||||
|
||||
### 2. Review tests and verification first
|
||||
@@ -88,6 +114,8 @@ Before reviewing implementation details:
|
||||
- Check whether tests assert behavior, not implementation details.
|
||||
- Check whether the selected test level is appropriate: unit for pure logic, integration for boundaries, E2E for critical browser flows.
|
||||
- Identify missing coverage for happy path, key error paths, edge cases, auth/workspace boundaries, and concurrency/order-sensitive behavior.
|
||||
- When a contract spans dispatchers, explicit service/API launches, approval/UI flows, or background handlers, enumerate every user-reachable entry point, trace each to the operation, and require path-specific regression coverage before declaring review clean.
|
||||
- When a PR changes the semantics of a field, flag, enum, event, or API contract, grep all producers and consumers for comments, logs, names, and tests that describe the old meaning. Those unchanged descriptions are in scope because the PR makes them false; anchor the finding to the changed contract use and list affected downstream sites.
|
||||
- For concurrent or event-driven changes, require a deterministic schedule that checks ownership or generation identity, stale-event handling, cancellation, and lock scope. Channel/barrier coordination is preferable to timing sleeps.
|
||||
- For stale-event races, cover both event-before-successor and delayed-old-event-after-successor orderings. Prefer integration coverage for cross-package event or callback paths when practical.
|
||||
- When an HTTP mutation returns a full entity while WebSocket/event updates can
|
||||
@@ -102,7 +130,10 @@ Before reviewing implementation details:
|
||||
coverage.
|
||||
- For terminal event streams, block an earlier publication, enqueue a terminal event (for example delete or cancellation), then enqueue a stale update. Assert no later mutation reaches an upserting consumer; queues must tombstone the entity or discard pending work at the terminal boundary.
|
||||
- When completion events lack a stable workload identity, test N outstanding registrations with N completion signals and duplicate delivery. A single-registration test cannot prove that uncorrelated completions retire work correctly. Compare this behavior with the accepted spec or ADR; a passing test that contradicts the contract is still a blocker.
|
||||
- For durable one-shot metadata, inventory every producer, claim, restore, retry/redelivery, and startup-sweep path. If a token carries a structured descriptor, restore that exact claimed value rather than replacing it with a boolean marker; test a real producer and a claim-to-restore-to-reclaim round trip.
|
||||
- Components can remain mounted while hidden or zero-sized. For visibility/activation behavior, ensure deadlines begin or reset at visible activation; mount the hidden state, advance fake timers or deliver content/layout changes, activate it, and assert final ownership with controlled observers or animation frames.
|
||||
- Treat missing tests for new or changed non-UI logic as a blocker unless the change is explicitly untestable and says why.
|
||||
- For forms that catch typed validation or provider errors, review feedback and cleanup as separate paths. Require focused coverage for every typed error family (or a table-driven equivalent) that asserts localized feedback is shown and the dialog/input remains open, plus a success assertion that it still closes; a `finally` block must not close a recoverable form unconditionally.
|
||||
|
||||
### 3. Review for issues
|
||||
|
||||
@@ -141,18 +172,24 @@ Check every changed file for the following layers. Skip layers that don't apply
|
||||
- There is a single clear source of truth; state or business rules are not duplicated across layers
|
||||
- Nullability, optional fields, defaults, and invalid combinations are modelled deliberately
|
||||
- Persistence schemas or transport types are not leaking implementation details into domain/application contracts
|
||||
- Persistence conformance tests call real production stores and assert a non-zero domain write/read-back; synthetic tables and no-op SQL are not semantic coverage. Startup tests trace errors through bootstrap and auth/middleware order, not readiness alone.
|
||||
- Concurrency, retries, partial failures, and duplicate requests cannot corrupt state or apply transitions more than once
|
||||
- Backward compatibility, migrations, and mixed-version behaviour are considered when contracts or persisted data change
|
||||
|
||||
**Logic & correctness:**
|
||||
- Edge cases handled (empty input, nil/null, zero, max values)
|
||||
- Error paths covered and not silently swallowed
|
||||
- For marker-delimited full-document mutations, define missing, orphaned,
|
||||
duplicate, and malformed-marker behavior before writing. Cleanup must no-op or
|
||||
fail closed when the body is not owned, and tests must cover each ownership
|
||||
boundary without retrying an unowned document.
|
||||
- Race conditions or concurrency issues in concurrent code
|
||||
- Async events carry an immutable identity when they can outlive the operation that created them; stale events cannot mutate a replacement operation
|
||||
- Locks protect only the atomic ownership boundary and are not held across unbounded I/O or a full asynchronous operation
|
||||
- Synchronous callbacks cannot re-enter a lock they already need; moving publication asynchronous also requires an immutable value snapshot, clear shutdown ownership, and protection against a delayed event changing successor state
|
||||
- When a generation, token, or lease authorizes a side effect, validate and mutate within one critical section. Check every terminal path separately: success, raw error, cancellation, timeout, and disconnect.
|
||||
- Detached goroutines have immutable snapshots and a real happens-before relationship before reading state that can otherwise transition underneath them
|
||||
- When a system design requires telemetry for an external or provider operation, audit every early return and require exactly one completion outcome per invocation. Outcomes should use allowlisted stable IDs, duration, response shape, and typed failure category, while excluding URLs, request/response bodies, credentials, and raw upstream errors; add observer/logger assertions for success and representative timeout and invalid-descriptor failures.
|
||||
|
||||
**Performance:**
|
||||
- No N+1 queries (loop with individual DB calls)
|
||||
@@ -196,6 +233,7 @@ Check every changed file for the following layers. Skip layers that don't apply
|
||||
**Testing (blocker if missing):**
|
||||
- Backend (Go): new or changed functions/methods must have corresponding `*_test.go` tests
|
||||
- Frontend: new utilities, hooks, API clients, and store slices must have focused tests. Pure React markup may skip a unit test, but behavior-bearing components (conditional status, accessibility, store-derived state, or responsive/mobile variants) need focused `*.test.tsx` coverage and/or E2E. Route responsive user-facing changes through `/mobile-parity`.
|
||||
- For conditional UI driven by store-derived data, trace branch predicates through real callers and use production-shaped props. Include required identity props and seed the empty store/data state; do not simulate "no data" by omitting props that callers always pass.
|
||||
- Async UI lifecycle: loading/busy state must clear through `finally` or equivalent terminal cleanup for success, error, cancellation, and early/no-op returns; require focused tests for those terminal paths.
|
||||
- Exceptions: config files, generated code, and pure React component markup
|
||||
- Missing tests for new or changed logic is a **blocker** — suggest what tests to add and recommend `/tdd`
|
||||
@@ -216,12 +254,14 @@ review; preserve all pre-existing user changes.
|
||||
Report findings with a concrete suggested fix. Do not edit the checkout during
|
||||
a review-only request; otherwise remediate in the same primary conversation.
|
||||
|
||||
Before drafting or posting a copyable PR comment, map each proposed point against
|
||||
Before drafting or sending an author-facing review finding through any channel,
|
||||
including a PR comment or `message_task_kandev`, map each point against
|
||||
exact-current-head review bodies, top-level discussion comments, and all
|
||||
unresolved or hidden threads. If a point is already raised, omit it from the
|
||||
author-facing comment but retain it in the private review summary; repeat it
|
||||
author-facing delivery but retain it in the private review summary; repeat it
|
||||
only when the user explicitly asks for reinforcement. Re-fetch immediately
|
||||
before posting and start a new review round if `headRefOid` changed.
|
||||
before delivery and start a new review round if `headRefOid` changed. Name the
|
||||
exact reviewed SHA in the outgoing finding.
|
||||
|
||||
### 5. Output
|
||||
|
||||
|
||||
@@ -115,6 +115,9 @@ explicitly requests task tracking.
|
||||
qualifies as a successful hook receipt.
|
||||
|
||||
4. **Stage files:** Stage relevant files (prefer specific files over `git add -A`).
|
||||
Before reviewing or committing, inspect every `??` path from `git status
|
||||
--short`; `git diff` omits untracked files. Stage each relevant path or read
|
||||
it explicitly so new tests and helpers are not missed.
|
||||
- **Splitting commits with new files:** When introducing a brand-new file alongside the file that uses it, stage them together. The Go lint pre-commit hook stashes *unstaged* changes before linting but keeps *untracked* files in the working tree — so a new helper committed alone, while its (still-unstaged) caller sits in the working tree, lints as `unused` and rejects the commit.
|
||||
|
||||
5. **Commit:** Write a commit message following the format above. If changes span multiple concerns, consider separate commits.
|
||||
@@ -133,23 +136,41 @@ qualifies as a successful hook receipt.
|
||||
changed copy and fail. Localize it and add matching `en`/`pseudo` catalog
|
||||
entries before retrying; verify with `cd apps/web && pnpm run i18n:check` and
|
||||
the normal hook receipt.
|
||||
Capture the normal hook stream in a temporary log while committing and use
|
||||
that log to record each hook ID and result. Do not infer hook results from a
|
||||
condensed launcher summary. For example:
|
||||
If the commit command returns an `exec_command` `session_id`, preserve the
|
||||
full result and poll that same session until it is terminal; never start a
|
||||
second commit attempt while the first is live. Retain the exact temporary
|
||||
log path from that single attempt for the hook receipt. Capture the normal
|
||||
hook stream in a temporary log while committing and use that log to record
|
||||
each hook ID and result. RTK can condense `rtk git commit`
|
||||
output, so use `rtk proxy git commit` when preserving raw hook output. Do not
|
||||
infer hook results from a condensed launcher summary. For example:
|
||||
```bash
|
||||
COMMIT_LOG="$(mktemp "${TMPDIR:-/tmp}/kandev-commit.XXXXXX.log")"
|
||||
set -o pipefail
|
||||
git commit -m "type(scope): description" 2>&1 | tee "$COMMIT_LOG" >/dev/null
|
||||
rtk proxy git commit -m "type(scope): description" 2>&1 | rtk proxy tee "$COMMIT_LOG" >/dev/null
|
||||
```
|
||||
Read the log to extract the hook receipt, rather than printing the full
|
||||
stream again. Remove the exact temporary file after copying the receipt into
|
||||
Both the commit and `tee` stages must use raw-output mode when the log is
|
||||
parsed; normal `rtk git commit` output may contain only `ok <sha>` and is not
|
||||
hook evidence. Read the log to extract every hook ID/result and confirm the receipt still
|
||||
says `bypass: false`, rather than printing the full stream again. Remove the
|
||||
exact temporary file after copying the receipt into
|
||||
the handoff:
|
||||
```bash
|
||||
unlink "$COMMIT_LOG"
|
||||
```
|
||||
If a timed-out hook leaves a commit or linter process running, inspect
|
||||
`git status`, `git log`, and only that attempt's processes before retrying.
|
||||
Wait for or stop the owned process tree first; do not mistake a transient
|
||||
`parallel golangci-lint is running` message for a second commit failure.
|
||||
If the commit exits nonzero, preserve and print the full or bounded log
|
||||
before cleanup so hook diagnostics are not lost. Remove the temporary log
|
||||
only after copying a successful hook receipt or recording the failed output.
|
||||
When extracting receipt lines from a nested shell, use shell-safe single-
|
||||
quoted `awk` or `sed` expressions; a double-quoted `sed` range containing
|
||||
`$` can be expanded by the outer shell and hide the receipt. Record active
|
||||
hooks that report `Skipped` or `no files to check` as `skipped`; do not infer
|
||||
their result from the launcher's summary, and retain the log until both the
|
||||
pre-commit and commit-msg results are recorded.
|
||||
|
||||
If a hook fails only because another worktree is already running
|
||||
golangci-lint (for example, `parallel golangci-lint is running`), wait for
|
||||
|
||||
@@ -27,13 +27,18 @@ Before changing code:
|
||||
- Read the scoped `AGENTS.md` for the subtree you will touch, e.g. `apps/backend/AGENTS.md`, `apps/web/AGENTS.md`, or integration-specific guidance.
|
||||
- Use `rg` to find existing patterns before inventing one.
|
||||
- Read the file you will edit and nearby tests.
|
||||
- For product features, read `docs/specs/README.md`, the owning system index,
|
||||
adjacent indexes with similar capability names, and only the relevant
|
||||
- For product features, read `docs/specs/README.md`, the owning system `README.md`,
|
||||
adjacent system README files with similar capability names, and only the relevant
|
||||
requirement and system-design files. Choose the owner from the durable
|
||||
contract, not the affected code layer. During migration, use
|
||||
`docs/specs/INDEX.md` to find a legacy source.
|
||||
contract, not the affected code layer. Use `python3 scripts/list-docs.py
|
||||
specs --format paths` to find documents. During migration, add
|
||||
`--kind legacy` to find a legacy source.
|
||||
- When implementing from a plan, read `plan.md` for orientation and only the
|
||||
current work order. Follow its `REQ-*`, `AC-*`, and system-design references.
|
||||
- Dependency, sibling, and stacked PR references are snapshots. Resolve the
|
||||
current dependency and base heads before implementation and again before
|
||||
final fixup; if a dependency moved or landed, re-read the affected
|
||||
requirements, designs, contracts, and traceability links.
|
||||
- For frontend/UI, include `/mobile-parity` and `/e2e` guidance when applicable.
|
||||
- For OpenAI/API docs or other fast-moving dependencies, use official docs or primary sources.
|
||||
|
||||
|
||||
@@ -118,6 +118,12 @@ Read these sources before designing the plugin:
|
||||
4. The current `kdlbs/kandev-plugin-template` repository, including its
|
||||
`README.md`, `Makefile`, tests, and release workflow.
|
||||
|
||||
**Marketplace attribution:** The manifest `author` is independent from GitHub
|
||||
repository ownership or organization. Never infer author `kandev` from a
|
||||
`kdlbs/kandev-plugin-*` repository. Read and preserve the manifest author,
|
||||
verify `repo_url` separately, and require an explicit contributor identity when
|
||||
an externally maintained plugin is released.
|
||||
|
||||
Prefer the public authoring docs and current template over old examples. The
|
||||
frontend contract pair is `docs/plans/plugins/PLUGIN-API.md` plus
|
||||
`apps/web/lib/plugins/types.ts`; concrete UI exports are in
|
||||
|
||||
@@ -53,6 +53,36 @@ Start with the cheapest faithful reproduction:
|
||||
with a fresh all-source bundle.
|
||||
5. Unknown: trace from the symptom backward through code and add temporary instrumentation only where it will split the search space.
|
||||
|
||||
When logs show repeated calls to the same endpoint or action, classify each
|
||||
request by transport, method/action, query or body cursor, caller, and cadence
|
||||
before diagnosing a loop. A periodic newest-window refresh and cursor
|
||||
pagination can share a route while serving different purposes. Verify
|
||||
pagination by capturing the directional cursor request and its response. If no
|
||||
such request exists, investigate the UI trigger or lifecycle; repeated
|
||||
uncursored refreshes do not prove that the server failed to advance a cursor.
|
||||
|
||||
Before any restart or mutating reproduction, capture the baseline diagnostic
|
||||
bundle and record the exact database and log pointers. Treat the live database
|
||||
as latest state, not historical evidence; preserve the baseline and reconstruct
|
||||
the lifecycle from timestamped logs, events, and transition tables before
|
||||
comparing later state.
|
||||
|
||||
For a GitHub merge-queue ejection, a timeline removal event is not the cause.
|
||||
Inspect the ruleset's `check_response_timeout_minutes`, the current
|
||||
`mergeQueueEntry`, and `merge_group` runs. Record the synthetic run/job IDs,
|
||||
`head_sha`, start/end timestamps, configured workflow/job timeout, and logs
|
||||
before classifying the failure as CI capacity or changing product code. Use the
|
||||
PR-fixup merge-queue and CI-troubleshooting references for the query details.
|
||||
|
||||
For workflow-routing failures, inspect the workflow's `on` activity types,
|
||||
job-level `if` gates, permissions, exact PR and head SHA, and the actor that
|
||||
added a label. Verify token-trigger behavior against GitHub's
|
||||
[GITHUB_TOKEN documentation](https://docs.github.com/en/actions/concepts/security/github_token):
|
||||
events created with `GITHUB_TOKEN` generally do not create another workflow run,
|
||||
and a label-only trigger does not cover a later `synchronize` update. Confirm
|
||||
the observed run and event rather than inferring that a missing run means the
|
||||
workflow logic was skipped.
|
||||
|
||||
### File-first log triage
|
||||
|
||||
Start with the retained backend files before asking for a broad export. Each
|
||||
|
||||
@@ -52,10 +52,15 @@ needed when authentication is disabled.
|
||||
|
||||
Use the smallest source set that can answer the question. `backend` covers the
|
||||
retained daily files; `frontend` asks connected tabs for their bounded local
|
||||
console history; `runtime` adds only allow-listed session status/executor
|
||||
metadata; `acp` is an explicit debug-only selection of one to ten authorized
|
||||
sessions and can include full protocol content. Standard evidence does not
|
||||
include stored chat/session/agent messages, while ACP evidence deliberately can.
|
||||
console history. The task-session bundle currently accepts only the source
|
||||
values exposed by its active schema (`backend`, `frontend`, or `all`); do not
|
||||
assume that `runtime` or `acp` is available. Standard evidence does not include
|
||||
stored chat/session/agent messages. For a delivery-versus-visibility question,
|
||||
identify the exact sender and receiver sessions first, then use authorized task
|
||||
conversation or ACP evidence when that source is available. Inspect
|
||||
`manifest.json` warnings before drawing conclusions; if the active diagnostic
|
||||
tool does not expose the requested chat/ACP source, report protocol evidence as
|
||||
unavailable instead of inferring delivery from ordinary logs.
|
||||
|
||||
Extract into a newly created temporary directory, never a repository, home
|
||||
directory, workspace root, or reused path. Reject ZIP entries that are
|
||||
@@ -87,6 +92,27 @@ A zero-match task-ID search is inconclusive: install-wide backend events and
|
||||
browser pages outside recognized task routes may not carry `task_id`. Next
|
||||
search a precise route/error string or a bounded timestamp window.
|
||||
|
||||
## Stalled launch triage
|
||||
|
||||
When a session stays `RUNNING` but produces no agent output, do not treat a
|
||||
responsive browser or WebSocket as proof that the prompt reached the agent.
|
||||
Correlate three independent facts for the exact session and time window:
|
||||
|
||||
1. Retained runtime state: current execution, turn, prompt generation, and
|
||||
session status.
|
||||
2. Backend logs: launch admission, attachment claim/materialization, prompt
|
||||
dispatch, and any asynchronous error or terminal event.
|
||||
3. Raw and normalized ACP frames for that authorized session, when protocol
|
||||
evidence is necessary.
|
||||
|
||||
No prompt frame means the failure happened before ACP admission. A prompt frame
|
||||
without a terminal frame points to transport or provider settlement. A terminal
|
||||
frame with durable state still `RUNNING` points to lifecycle reconciliation.
|
||||
For staged attachments, trace upload, claim, materialization, and prompt
|
||||
dispatch separately; an uploaded descriptor is not proof that launch claimed
|
||||
it. Finally, check whether an asynchronous error only reached a log line or
|
||||
also entered the correlated terminal execution path.
|
||||
|
||||
Backend files also live under `<HOME_DIR>/logs/backend-logs.log` with the two
|
||||
previous UTC daily archives. Prefer a fresh bundle because its manifest records
|
||||
completeness and includes requested browser evidence.
|
||||
|
||||
@@ -10,6 +10,9 @@ Use this skill to decide whether public docs need updates and to make those upda
|
||||
## Docs Boundaries
|
||||
|
||||
- Public website docs source lives under `docs/public/**`.
|
||||
- Repository-facing public docs also include the root `README.md` and
|
||||
`docs/screenshots.md`; search them for feature, integration, and screenshot
|
||||
changes even when the website source is unchanged.
|
||||
- Product context, requirements, and system designs stay under `docs/specs/**`.
|
||||
- Implementation plans and work orders stay under `docs/plans/**`.
|
||||
- Architecture decisions stay under `docs/decisions/**`.
|
||||
@@ -37,26 +40,34 @@ Skip public docs when the change is:
|
||||
## Workflow
|
||||
|
||||
1. Identify docs impact from the diff and changed behavior.
|
||||
2. Search `docs/public/**` first for affected terms and commands.
|
||||
2. Search `docs/public/**`, the root `README.md`, and `docs/screenshots.md`
|
||||
first for affected terms, features, integrations, and screenshots.
|
||||
For behavior changes, also search the same contract and old precedence/reset
|
||||
wording across `docs/public`, `docs/specs`, and `docs/decisions`; ensure the
|
||||
introductory summary agrees with later upgrade and recovery sections.
|
||||
3. If public docs exist, update them with the same PR as the behavior change.
|
||||
4. If no public docs exist but the behavior is user-facing, add or propose the smallest useful public page/section.
|
||||
When adding a page, include `title` and `description` frontmatter and list its page slug or path without the `.md` extension in `docs/public/meta.json` exactly once, for example `cli`. See `docs/public/README.md`.
|
||||
5. If the change only updates implementation intent or architectural history, update specs/plans/ADRs instead.
|
||||
6. Classify each public page by its primary Diátaxis content type:
|
||||
6. For operational recovery guidance, distinguish permission-denied, active-lock,
|
||||
and stale-lock failures before recommending a fallback. Verify that the
|
||||
fallback uses a different execution or resource path; if it shares the same
|
||||
worktree, state that it cannot bypass the contention.
|
||||
7. Classify each public page by its primary Diátaxis content type:
|
||||
- **Tutorial:** teach a beginner by leading them through one successful outcome.
|
||||
- **How-to guide:** help a reader complete a known task, with focused steps, choices, and recovery paths.
|
||||
- **Reference:** provide accurate, complete lookup information such as fields, commands, defaults, limits, or protocol contracts.
|
||||
- **Explanation:** build understanding of a concept, boundary, rationale, or trade-off.
|
||||
Keep one dominant type per page. Link to another page when a long section changes from learning to procedure, lookup, or explanation; do not force every page into a generic tutorial-shaped opening.
|
||||
7. Keep public docs task-oriented and scan-friendly:
|
||||
8. Keep public docs task-oriented and scan-friendly:
|
||||
- Tutorials should lead with prerequisites and a linear first success; how-to guides should lead with the task, expected result, and only the prerequisites it needs.
|
||||
- Reference pages should lead with scope and the contract readers need to look up; explanation pages should lead with the question or concept and why it matters.
|
||||
- Use short paragraphs (one idea, normally three sentences or fewer) and bullets for choices, limits, and consequences.
|
||||
- Prefer a link to the page that owns a detailed contract over repeating it.
|
||||
- Use native `<details>` / `<summary>` disclosures for non-essential edge cases, exhaustive option lists, and advanced configuration. Keep required steps, security warnings, destructive effects, and eligibility limits visible.
|
||||
- Use tables only for genuine comparisons, not narrative text.
|
||||
8. Preserve internal links inside `docs/public/**` where possible. Link to source-only raw docs only when the raw note is intentionally not published.
|
||||
9. Note docs impact and the page's primary content type in the PR body.
|
||||
9. Preserve internal links inside `docs/public/**` where possible. Link to source-only raw docs only when the raw note is intentionally not published.
|
||||
10. Note docs impact and the page's primary content type in the PR body.
|
||||
|
||||
## Diagrams for Public Docs
|
||||
|
||||
@@ -100,6 +111,9 @@ node --test scripts/validate-public-docs.test.mjs
|
||||
node scripts/validate-public-docs.mjs
|
||||
```
|
||||
|
||||
Run both public-doc validators when the root README or screenshot catalog is
|
||||
updated, not only when `docs/public/**` changes.
|
||||
|
||||
For website docs publishing changes, also run from the landing repo:
|
||||
|
||||
```bash
|
||||
|
||||
+29
-254
@@ -79,7 +79,7 @@ pnpm e2e:run # auto: docker if daemon + CI ima
|
||||
pnpm e2e:run tests/task/my-test.spec.ts # single file (extra args pass through to Playwright)
|
||||
pnpm e2e:run tests/path/spec.ts -- --grep "exact test name" # exact CI failure with a fresh build
|
||||
pnpm e2e:run --shards 3 # 3 shards concurrently on this machine (isolated)
|
||||
pnpm e2e:run --no-build -- --grep "task creation" # skip rebuild; forward flags after --
|
||||
pnpm e2e:run --no-build -- --grep "task creation" # runner options before --; Playwright options after
|
||||
pnpm e2e:run --no-build --project mobile-chrome tests/layout/mobile-spa-resilience.spec.ts
|
||||
pnpm e2e:docker # force the docker CI image (full isolation from a host dev instance)
|
||||
pnpm e2e:clean # remove build/test artifacts, incl. root-owned ones from prior docker runs
|
||||
@@ -104,71 +104,24 @@ See [resource-safety.md](references/resource-safety.md) before any full local te
|
||||
|
||||
The runner solves the sharp edges hand-rolling would hit: in docker it builds the CGO backend on the **host** and runs it in the runtime image (forward-compatible when the host glibc ≤ the image's — the usual case; it smoke-tests this and only falls back to the build image if the host is newer), builds the Vite web assets on the host, runs them through the Go-served SPA, and keeps Playwright output container-local. See `apps/web/e2e/README.md` → "the managed runner".
|
||||
|
||||
`--no-build` reuses every production E2E artifact, not only Vite assets and the
|
||||
backend executable. On a fresh worktree or after rebasing, confirm packaged
|
||||
fixtures also exist; global setup currently requires
|
||||
`apps/backend/.build/kandev-plugin-e2e-1.0.0.tar.gz`. If it is absent, run
|
||||
without `--no-build` or first run `make -C apps/backend e2e-plugin-package`.
|
||||
Prefer a normal managed build after source or base-branch changes and reserve
|
||||
`--no-build` for repeated runs against unchanged artifacts.
|
||||
`--no-build` reuses Vite, backend, and packaged fixtures; prefer normal managed builds
|
||||
after source/base changes. If intentional, rebuild both with `make -C apps/backend build`
|
||||
and `make -C apps/backend e2e-plugin-package`; global setup requires the plugin tarball.
|
||||
|
||||
For a raw Docker/SSH/container run, `make build-backend` alone does not build
|
||||
the Linux mock-agent fixture. Prefer the managed runner; otherwise run
|
||||
`make build-backend build-backend-remote-helpers build-web`. If the fixture
|
||||
reports a missing `KANDEV_MOCK_AGENT_LINUX_BINARY`, run
|
||||
`make -C apps/backend build-mock-agent-linux` before diagnosing product code.
|
||||
For raw Docker/SSH/container runs, `make build-backend` does not build the Linux
|
||||
mock-agent fixture. Prefer managed; otherwise run `make build-backend
|
||||
build-backend-remote-helpers build-web`. If `KANDEV_MOCK_AGENT_LINUX_BINARY` is
|
||||
missing, run `make -C apps/backend build-mock-agent-linux` before diagnosing code.
|
||||
|
||||
### Raw commands (when you need fine control)
|
||||
|
||||
```bash
|
||||
make test-e2e # all tests, headless (host)
|
||||
cd apps && pnpm --filter @kandev/web e2e:raw -- tests/task/my-test.spec.ts # single file
|
||||
cd apps && pnpm --filter @kandev/web e2e:raw -- --grep "task creation" # by name
|
||||
cd apps && pnpm --filter @kandev/web e2e:raw -- --grep "task creation" # workspace-filter syntax; from apps/web use `pnpm e2e:raw --project=mobile-chrome e2e/tests/<area>/<spec>.spec.ts` without a standalone `--`; run `--list` first if forwarding syntax is uncertain
|
||||
```
|
||||
|
||||
### Flake reproduction
|
||||
|
||||
Start by matching CI as closely as possible, then add pressure deliberately:
|
||||
|
||||
1. Run the exact failed shard in the CI runtime image with CI env enabled:
|
||||
```bash
|
||||
docker run --rm --ipc=host -v "$PWD":/work -w /work/apps/web \
|
||||
-e CI=true -e GITHUB_ACTIONS=true -e GITHUB_WORKSPACE=/work \
|
||||
-e NODE_OPTIONS=--dns-result-order=ipv4first \
|
||||
-e PLAYWRIGHT_BROWSERS_PATH=/ms-playwright \
|
||||
ghcr.io/kdlbs/kandev-ci:runtime-latest \
|
||||
bash -lc 'git config --global --add safe.directory /work 2>/dev/null; bash e2e/scripts/run-raw-e2e.sh --project=chromium --project=mobile-chrome --shard=<failed-shard>/14 -- --reporter=list --retries=0'
|
||||
```
|
||||
2. If the exact shard passes, constrain container resources and repeat the
|
||||
failing spec/test. GitHub-hosted runners can expose timing bugs that a roomy
|
||||
local machine hides:
|
||||
```bash
|
||||
docker run --rm --ipc=host --cpus=2 --memory=4g --memory-swap=4g \
|
||||
-v "$PWD":/work -w /work/apps/web \
|
||||
-e CI=true -e GITHUB_ACTIONS=true -e GITHUB_WORKSPACE=/work \
|
||||
-e NODE_OPTIONS=--dns-result-order=ipv4first \
|
||||
-e PLAYWRIGHT_BROWSERS_PATH=/ms-playwright \
|
||||
ghcr.io/kdlbs/kandev-ci:runtime-latest \
|
||||
bash -lc 'git config --global --add safe.directory /work 2>/dev/null; bash e2e/scripts/run-raw-e2e.sh --project=mobile-chrome e2e/tests/terminal/mobile-terminal-keybar.spec.ts --grep "user presses an OS-keyboard letter while no modifier is active" --repeat-each=30 --reporter=list --retries=0'
|
||||
```
|
||||
3. Preserve nearby test ordering when a single-test repeat stays green; run the full spec or shard with the same resource limits before declaring a flake non-reproducible.
|
||||
|
||||
The config uses `failOnFlakyTests: !CI`: local runs fail on a flaky retry, while
|
||||
CI temporarily tolerates one. Either result is a failure signal for agents:
|
||||
reproduce it with `--retries=0` only after disabling any `test.describe.configure({ retries: 1 })` override; never
|
||||
rerun until it happens to pass. If isolated repeats stay green but the shard
|
||||
fails, binary-search preceding specs in one worker. The fix is complete only
|
||||
when the smallest reproducing sequence passes without retries.
|
||||
|
||||
Record the exact command, resource limits, repeat number, and failure artifact path. For a failed shard, inspect every `error-context.md` in its downloaded
|
||||
`test-results-<shard>` artifact and compare shared page-object waits with `main`
|
||||
before changing product code; the context can expose duplicate active terminals
|
||||
or a terminal stuck on "Starting terminal...".
|
||||
|
||||
When a PR E2E shard fails, investigate every spec, including those outside the
|
||||
diff; never dismiss, rerun, or merely record a failure as unrelated. Reproduce
|
||||
the exact test, then preserve shard ordering and CI pressure. Fix every valid
|
||||
defect with retries disabled, or evidence a concrete external blocker.
|
||||
For flake reproduction, load [failure-triage.md](references/failure-triage.md).
|
||||
|
||||
**CRITICAL: E2E tests run against the production Vite build served by the Go backend**, not dev mode. After any frontend code change, you **must** rebuild before running tests (`pnpm e2e:run` does this for you):
|
||||
|
||||
@@ -183,6 +136,8 @@ Without this, tests can exercise stale code: after backend changes run `make -C
|
||||
1. Read `helpers/api-client.ts` and `pages/` to discover available seed methods and page objects; use `data-testid` attributes for selectors — add them to components as needed
|
||||
2. Import fixtures from `../../fixtures/test-base` — provides `testPage`, `apiClient`, and `seedData` (pre-created workspace with default workflow). Pull `backend` from the fixture too when you need the backend URL — it's worker-scoped, dynamic, and `process.env.KANDEV_API_BASE_URL` is **not** set in the Playwright runner. Use `backend.baseUrl`.
|
||||
3. Use page objects for common interactions; create new ones for new pages. For GitHub features, use `apiClient.mockGitHub*()` methods to seed mock data
|
||||
4. Workspace-scoped mock events must carry the real `workspace_id`. `mockGitHubAssociateTaskPR` defaults an omitted ID to the active workspace; pass it explicitly for foreign-workspace cases, and keep production handlers fail-closed for missing or mismatched IDs. Direct-store bridges that inject Git status or inspect Changes must resolve `environmentIdBySessionId[sessionId]` and use environment-keyed state; sessions can share one environment.
|
||||
5. For new status, hydration, or persistence assertions, inventory existing action/mutation scenarios and add coverage beside them, never replace them; preserve response/error, idempotency/duplicate-suppression, and desktop/mobile pointer/target checks, then run affected specs together.
|
||||
|
||||
### Input-modality behavior
|
||||
|
||||
@@ -216,7 +171,8 @@ expect(metricsBox!.height).toBeCloseTo(actionsBox!.height, 1);
|
||||
|
||||
Run the assertion in the relevant desktop and mobile projects when responsive
|
||||
layout can change the result. Do not rely on fixed pixels when the product
|
||||
contract is equality or alignment.
|
||||
contract is equality or alignment. For control-size regressions, read the mobile-parity [sizing contract](../mobile-parity/references/control-sizing.md).
|
||||
For computed colors, parse alpha/opacity semantically or assert a deliberate class/data contract; do not compare serialized `getComputedStyle` strings because browsers may return `rgba()`, `oklab()`, or `color()`.
|
||||
|
||||
**Animation-aware geometry:** Before reading dialog or panel geometry, wait only for currently running Web Animations with finite `effect.getComputedTiming().iterations`; await `animation.finished.catch(() => undefined)` because Radix overlays can cancel animations during close or replacement. Never blanket-await infinite animations or use a fixed sleep; then read bounding boxes and assert the relationship.
|
||||
|
||||
@@ -251,103 +207,9 @@ test.describe("my feature", () => {
|
||||
|
||||
## Dev-first workflow
|
||||
|
||||
Before writing an E2E test, validate the feature works interactively using
|
||||
`pnpm --dir apps exec playwright-cli` against a dev server. This gives a fast
|
||||
feedback loop — code changes are picked up by hot reload in ~1-2 seconds, no
|
||||
production rebuild needed. Once confirmed working, translate the interactions
|
||||
into a proper E2E test.
|
||||
|
||||
### Start the dev environment
|
||||
|
||||
Multiple agents may run in parallel, so use random ports for dev servers. Managed runner shards are isolated, but separate raw Playwright processes are not automatically safe: fixture ports are backend `18080 + E2E_PORT_OFFSET + workerIndex` and agentctl `30001 + E2E_PORT_OFFSET*1000 + workerIndex*200`; `--repeat-each` advances `workerIndex`, so nearby fixed offsets can overlap later.
|
||||
|
||||
```bash
|
||||
OFFSET=$((RANDOM % 100))
|
||||
BACKEND_PORT=$((19000 + OFFSET))
|
||||
FRONTEND_PORT=$((14000 + OFFSET))
|
||||
```
|
||||
|
||||
Start the backend:
|
||||
```bash
|
||||
E2E_TMP=$(mktemp -d) && mkdir -p "$E2E_TMP/.kandev" && \
|
||||
printf '[user]\n name = E2E Test\n email = e2e@test.local\n[commit]\n gpgsign = false\n' > "$E2E_TMP/.gitconfig" && \
|
||||
HOME="$E2E_TMP" KANDEV_HOME_DIR="$E2E_TMP/.kandev" KANDEV_SERVER_PORT=$BACKEND_PORT \
|
||||
KANDEV_DATABASE_PATH="$E2E_TMP/kandev.db" KANDEV_MOCK_AGENT=only \
|
||||
KANDEV_MOCK_GITHUB=true KANDEV_DOCKER_ENABLED=false KANDEV_WORKTREE_ENABLED=false \
|
||||
KANDEV_LOG_LEVEL=warn apps/backend/bin/kandev &
|
||||
```
|
||||
|
||||
Start the dev frontend:
|
||||
```bash
|
||||
KANDEV_API_BASE_URL=http://localhost:$BACKEND_PORT VITE_KANDEV_API_PORT=$BACKEND_PORT \
|
||||
pnpm --filter @kandev/web dev --port $FRONTEND_PORT &
|
||||
```
|
||||
|
||||
### Validate with playwright-cli
|
||||
|
||||
```bash
|
||||
pnpm --dir apps exec playwright-cli open http://localhost:$FRONTEND_PORT
|
||||
pnpm --dir apps exec playwright-cli snapshot # see page structure and element refs
|
||||
pnpm --dir apps exec playwright-cli click e5 # interact using refs from snapshot
|
||||
pnpm --dir apps exec playwright-cli fill e3 "test input"
|
||||
pnpm --dir apps exec playwright-cli snapshot # verify result
|
||||
```
|
||||
|
||||
### Fast iteration cycle
|
||||
|
||||
1. Make a code change in `apps/web/`
|
||||
2. HMR picks it up in ~1-2 seconds
|
||||
3. `pnpm --dir apps exec playwright-cli snapshot` or `pnpm --dir apps exec playwright-cli screenshot` to verify
|
||||
4. Repeat until the flow works correctly
|
||||
|
||||
### Translate to E2E test
|
||||
|
||||
Once validated, write the Playwright test using project fixtures and page objects. The `playwright-cli` interactions map directly to Playwright API calls:
|
||||
|
||||
| playwright-cli | Playwright API |
|
||||
|---|---|
|
||||
| `playwright-cli click e5` | `page.getByTestId('...').click()` |
|
||||
| `playwright-cli fill e3 "text"` | `page.getByTestId('...').fill('text')` |
|
||||
| `playwright-cli snapshot` (verify element visible) | `expect(page.getByTestId('...')).toBeVisible()` |
|
||||
|
||||
Use `data-testid` selectors in the test (not snapshot refs), and wrap common flows in page objects.
|
||||
|
||||
### Capture PR evidence
|
||||
|
||||
After confirming the feature works, capture screenshots or a video as proof for the PR:
|
||||
|
||||
```bash
|
||||
# Screenshots of key states
|
||||
pnpm --dir apps exec playwright-cli screenshot --filename=apps/web/.pr-assets/feature-before.png
|
||||
# ... interact to show the feature ...
|
||||
pnpm --dir apps exec playwright-cli screenshot --filename=apps/web/.pr-assets/feature-after.png
|
||||
|
||||
# Or record a video walkthrough
|
||||
pnpm --dir apps exec playwright-cli video-start apps/web/.pr-assets/feature-demo.webm
|
||||
# ... perform the user flow ...
|
||||
pnpm --dir apps exec playwright-cli video-stop
|
||||
```
|
||||
|
||||
Create `apps/web/.pr-assets/manifest.json` so the `/pr` skill picks them up:
|
||||
```json
|
||||
{
|
||||
"assets": [
|
||||
{"name": "feature-demo", "file": "feature-demo.webm", "format": "gif", "caption": "Feature demo"},
|
||||
{"name": "feature-after", "file": "feature-after.png", "format": "png", "caption": "Result"}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Final verification
|
||||
|
||||
Always verify against the production build before finishing — dev mode can hide boot-payload, asset-serving, or hydration issues:
|
||||
|
||||
```bash
|
||||
pnpm --dir apps exec playwright-cli close
|
||||
# Kill dev server and backend
|
||||
make build-web
|
||||
cd apps && pnpm --filter @kandev/web e2e:raw -- tests/path/to/test.spec.ts
|
||||
```
|
||||
For interactive development and PR captures, load
|
||||
[dev-workflow.md](references/dev-workflow.md).
|
||||
Production-build verification remains required after development.
|
||||
|
||||
## Test organization
|
||||
|
||||
@@ -365,12 +227,12 @@ Tests are grouped by feature area in subdirectories under `tests/`. When creatin
|
||||
- **Verify persistence with page reload.** After changing a setting or creating data, reload the page (`testPage.reload()`) and assert the state is still correct. This catches hydration bugs and Go boot-payload/client-store mismatches.
|
||||
- **Restore patched persisted settings.** When a test PATCHes user settings, capture the baseline and restore it in `test.afterEach`. The backend is worker-scoped, and `e2eReset` does not reset every persisted setting, including `system_metrics_display`; leaking one can affect later tests in the same worker. Fixtures are lazy: acquire `testPage` before setting a non-default persisted value in `beforeEach`, otherwise page initialization can reapply the default and silently undo setup. Verify with the focused test that depends on that setting.
|
||||
- **Restore patched shared persisted state.** The worker-scoped backend and
|
||||
`e2eReset` do not reset every seeded record. A test that PATCHes a canonical
|
||||
`seedData` profile, repository, executor, or setting must capture its
|
||||
baseline and restore it in `test.afterEach` (so cleanup also runs after
|
||||
failure); prefer a disposable record when the UI can select it. Verify by
|
||||
running the mutating spec followed by its affected neighbour with
|
||||
`--workers=1 --retries=0`.
|
||||
`e2eReset` do not reset every seeded record. A test that creates or PATCHes a
|
||||
canonical `seedData` profile, repository, executor, integration/workspace
|
||||
preset, or other non-user setting must use unique names for shared/global
|
||||
rows, capture every changed baseline, and restore every mutation in
|
||||
`test.afterEach` or `finally` (not only delete rows created by the test).
|
||||
Prefer a disposable record when the UI can select it. Verify by running the mutating spec followed by its affected neighbour with `--workers=1 --retries=0`. Remove temporary or untracked files written into worker-scoped checkouts in `finally`/`afterEach`, including assertion-failure paths.
|
||||
- **Pass browser-evaluation values explicitly.** `locator.evaluate` and
|
||||
`page.evaluate` callbacks execute in the browser, so they cannot close over
|
||||
Node/test variables. Pass expected values as the argument instead, for
|
||||
@@ -393,108 +255,21 @@ Tests are grouped by feature area in subdirectories under `tests/`. When creatin
|
||||
|
||||
## Debugging failures
|
||||
|
||||
### Triage
|
||||
|
||||
When a test fails:
|
||||
|
||||
1. **Read the error output** — the Playwright error message, expected vs. actual, and which locator timed out
|
||||
2. **Read `error-context.md`** from `test-results/<test-name>/` — contains a YAML DOM snapshot showing exactly what was rendered. Search for expected elements, check if the page is in the right state (e.g., simple mode vs advanced mode). **These files persist across runs** — always confirm timestamps (portable: `ls -la e2e/test-results/.../error-context.md`; or `stat -c %y` on Linux / `stat -f %Sm` on macOS) or rebuild + rerun the spec fresh before trusting the snapshot. A stale context from a previous failure mode will send you debugging the wrong bug.
|
||||
3. **Read the failure screenshot** from `e2e/test-results/` — see what the page actually rendered
|
||||
4. **Attach to the failure** for deeper debugging using `playwright-cli`:
|
||||
```bash
|
||||
cd apps && PLAYWRIGHT_HTML_OPEN=never pnpm --filter @kandev/web e2e:raw -- tests/path.spec.ts --debug=cli &
|
||||
# Wait for "Debugging Instructions" with session name
|
||||
pnpm --dir apps exec playwright-cli attach tw-<session>
|
||||
pnpm --dir apps exec playwright-cli snapshot # inspect page state at failure point
|
||||
pnpm --dir apps exec playwright-cli console # check for JS errors
|
||||
pnpm --dir apps exec playwright-cli network # check API responses
|
||||
```
|
||||
|
||||
### Classify and fix
|
||||
|
||||
| Category | Signals | Fast loop |
|
||||
|---|---|---|
|
||||
| **Test logic** | Wrong selector, wrong expected text, missing page object method | Fix test files, re-run immediately (no rebuild -- Playwright transpiles TS at runtime) |
|
||||
| **Frontend-only** | Screenshot shows wrong UI, missing element, client error. API calls succeed. | Start dev server, fix with hot reload, verify with `playwright-cli`, then `make build-web` + re-run test |
|
||||
| **Backend** | 500 errors, wrong API response, "Backend did not become healthy" | Fix Go code, `make build-backend`, re-run test |
|
||||
|
||||
### Common issues
|
||||
|
||||
- **"Backend did not become healthy"** — run `make build-backend build-web`, check with `E2E_DEBUG=1`
|
||||
- **"Cannot find module"** — run `cd apps && pnpm install --frozen-lockfile`
|
||||
- **Port conflicts** — run raw repeat/stress invocations sequentially, or prove both computed port ranges are disjoint and check listeners. A totally white screenshot plus `ERR_CONNECTION_REFUSED` is a port/lifecycle signature to rule out before changing waits; never fix it with longer locator timeouts.
|
||||
- **Responsive layout stays stale after `page.setViewportSize()`** — record
|
||||
`window.innerWidth`, the affected element and parent `clientWidth`, and any
|
||||
layout-library width before changing waits. Headless Chromium reliably
|
||||
resizes the DOM/container and fires `ResizeObserver`, while an
|
||||
application-only `window.resize` listener may not be observed in the test.
|
||||
Prefer synchronizing with the layout container/observer and assert the
|
||||
intended result after both viewport and container-only changes.
|
||||
- **Auto-started session never goes idle** — for sessions started by the same call that creates them, the mock agent can finish before the client WS subscription registers, so a raw `idleInput()` visibility wait hangs. Use `SessionPage.waitForChatIdle()` before opening transient dialogs/drawers/popovers; it may reload and re-derive state from the Go boot payload. If it must run later, reopen the transient UI first. For WS/session hydration races, retain a bounded reload-and-retry fallback in the page object: keep the fast path immediate and the final check failing when genuinely stuck; remove it only with an equivalent deterministic readiness guarantee and focused regression.
|
||||
- **Flaky timeouts** — **never increase locator timeouts to fix flaky tests.** If a locator times out, the root cause is almost always something else: a setup failure, missing navigation, race condition, or the element genuinely not rendering. Investigate why the element never appears instead of giving it more time. Note: infrastructure health timeouts (30s in `fixtures/backend.ts`) and overall test timeouts (60s in `playwright.config.ts`) are separate and should not be modified either.
|
||||
- Screenshots on failure, video on first retry (CI)
|
||||
|
||||
### Debugging CI shard failures
|
||||
|
||||
CI splits host tests across 14 shards (plus 6 container shards); reproduce a specific shard locally:
|
||||
|
||||
```bash
|
||||
# List which tests are in a shard
|
||||
pnpm e2e:raw -- --shard=2/14 --list
|
||||
|
||||
# Run that shard locally (requires production build)
|
||||
make build-backend build-web
|
||||
cd apps/web && pnpm e2e:raw -- --shard=2/14
|
||||
```
|
||||
|
||||
```bash
|
||||
# Unzip a shard's blob report from CI artifacts
|
||||
unzip report-*.zip -d report-shard && cat report-shard/*.jsonl
|
||||
```
|
||||
|
||||
When a CI shard fails, distinguish its useful artifacts: download/unzip
|
||||
`report-*.zip` to map test IDs and timings; `test-results-<shard>` may be ready
|
||||
before the workflow completes even when logs refuse, so try it for the exact assertion, `error-context.md`, screenshot, and trace:
|
||||
|
||||
```bash
|
||||
gh run download <run-id> --name test-results-<shard> --dir <temp-dir>
|
||||
```
|
||||
|
||||
The blob report surfaces slow-but-passing specs that are latent flake risks;
|
||||
the test-results artifact identifies the concrete failure to reproduce. Specs
|
||||
whose duration approaches the 60s per-test timeout (defined in
|
||||
`playwright.config.ts`) are candidates to harden, typically by converting raw
|
||||
chat-flow assertions to the `waitForChatIdle()` / `expectChatResponseVisible()`
|
||||
recovery helpers documented earlier in this file.
|
||||
|
||||
### Flake triage: intrinsic race vs. contention
|
||||
|
||||
A test that flakes under parallel/sharded load is one of two things — decide which **before** touching it:
|
||||
|
||||
1. **Re-run it in a fresh, isolated container** (or at minimum a single fresh worker), `--retries=0`, a few reps:
|
||||
```bash
|
||||
pnpm e2e:docker --no-build -- --repeat-each=4 --workers=1 --retries=0 tests/path.spec.ts:LINE
|
||||
# or raw: pnpm e2e:raw -- --project=chromium --repeat-each=4 --retries=0 tests/path.spec.ts:LINE
|
||||
```
|
||||
(On Apple Silicon, `pnpm e2e:docker` needs Colima + Rosetta — `colima start --vz-rosetta`; default QEMU segfaults the amd64 Go build. See `apps/web/e2e/README.md`.)
|
||||
- **Flakes alone (fails some reps, fast):** intrinsic race — fix it (condition-correct wait, fix the actual race; not a timeout bump). E.g. a `waitForRequest` that times out the full window means the request *never fired* (a click swallowed during hydration) — retry the action with `await expect(async () => { ... }).toPass()`, don't extend the timeout.
|
||||
- **Passes clean AND fast alone (well under timeout):** contention, not a defect. The wait is correct; the test just starved for CPU/IO under load. No code/test fix applies.
|
||||
2. **Signature of contention, not a code path:** two identical-config full runs giving *different* hard-fail counts (e.g. 0 vs 3). Same code + same config + different outcome ⇒ host oversubscription, not a bug. CI's isolated runners don't reproduce it; reduce local concurrency (2–3 shards, not 5+) for a clean signal.
|
||||
3. **Caveat — don't flake-hunt with `--repeat-each` across many heavy specs in one long-lived worker.** It exhausts per-worker resources (agentctl port range, memory) over a long run and manufactures *false* failures unrelated to the test. Use **one fresh container per spec** instead.
|
||||
For failed specs, shard artifacts, or suspected contention, load
|
||||
[failure-triage.md](references/failure-triage.md).
|
||||
|
||||
## Selector guidelines
|
||||
|
||||
- **Prefer `data-testid` selectors** over text-based locators. Text content can change when UI is updated (e.g., hiding a badge), breaking tests that match by text. Use `getByTestId()` or `locator("[data-testid='...']")` for stable targeting. When translated labels intentionally identify multiple routes, scope by stable `href` or a dedicated test ID rather than role/name alone.
|
||||
- **Scope Radix and responsive locators to the active instance.** Tooltips may use `instant-open`, `delayed-open`, or `open`; use `[data-slot="tooltip-content"]:not([data-state="closed"])`, then scope to the visible portal/popover/container and active ancestor. Hidden mounts can make global locators match the wrong instance; do not use `.first()` to hide duplicates.
|
||||
- **Use page object methods** like `clickSessionChatTab()` (stable `data-testid`) instead of `sessionTabByText("1")` (fragile text match) for session tabs.
|
||||
- **Scope Radix and responsive locators to the active instance.** Tooltips may use `instant-open`, `delayed-open`, or `open`; use `[data-slot="tooltip-content"]:not([data-state="closed"])`, then scope to the visible portal/popover/container and active ancestor. For routes with multiple surfaces, scope controls to the active container first; portal overlays must be selected by visible overlay or the trigger's `aria-controls`, not assumed descendants. For portaled pickers, locate the active visible `role=listbox` or picker container first, then scope `getByRole('option')` within it. Hidden mounts can make global locators match the wrong instance; do not use `.first()` to hide duplicates.
|
||||
- **Use exact dynamic labels and page objects:** `filter({ hasText })` is substring matching; for counts or sibling content, use a stable `data-*` attribute or `getByText(label, { exact: true })` and assert uniqueness. Prefer page object methods like `clickSessionChatTab()` (stable `data-testid`) over fragile text matches such as `sessionTabByText("1")`.
|
||||
- **Dropdown menus can detach** from the DOM when React re-renders the parent (e.g., WS events updating the sidebar). The `openSidebarMenuAndClick()` helper in `session-page.ts` retries the full open-click sequence on detachment — use this pattern for similar interactions.
|
||||
|
||||
## TDD workflow
|
||||
|
||||
Follow `/tdd` when writing E2E tests:
|
||||
|
||||
1. **RED** — Write the spec, run it, watch it fail (missing `data-testid`, feature not implemented, etc.)
|
||||
1. **RED** — Write the spec and run a defect-specific expected-vs-actual assertion that fails before production changes; a missing selector/testid is scaffold failure, not behavioral RED. Add stable selectors as setup, or label selector-only RED separately. For a pure state/data UI regression, if a component/unit RED test already proves the bug and the E2E fixture cannot deterministically reproduce the pre-fix state without production wiring, add the E2E assertion after the minimal fix, run it against a fresh managed production build, and record why E2E was not the RED gate.
|
||||
2. **GREEN** — Implement the feature/fix, add `data-testid` attributes, run the test until green
|
||||
3. **REFACTOR** — Extract page objects, clean up selectors, keep tests green
|
||||
4. Run the targeted E2E spec when done and report that final change-aware
|
||||
verification is required as a separate planner assignment
|
||||
4. Run the targeted E2E spec when done and report that final change-aware verification is required as a separate planner assignment
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
## Dev-first workflow
|
||||
|
||||
Before writing an E2E test, validate the feature works interactively using
|
||||
`pnpm --dir apps exec playwright-cli` against a dev server. This gives a fast
|
||||
feedback loop — code changes are picked up by hot reload in ~1-2 seconds, no
|
||||
production rebuild needed. Once confirmed working, translate the interactions
|
||||
into a proper E2E test.
|
||||
|
||||
### Start the dev environment
|
||||
|
||||
Multiple agents may run in parallel, so use random ports for dev servers and inspect the calculated backend/agentctl ranges with `ss` or `lsof` before starting; choose an unused `E2E_PORT_OFFSET` and never kill unrelated Kandev processes. Managed runner shards are isolated, but separate raw Playwright processes are not automatically safe: fixture ports are backend `18080 + E2E_PORT_OFFSET + workerIndex` and agentctl `30001 + E2E_PORT_OFFSET*1000 + workerIndex*200`; `--repeat-each` advances `workerIndex`, so nearby fixed offsets can overlap later.
|
||||
|
||||
```bash
|
||||
OFFSET=$((RANDOM % 100))
|
||||
BACKEND_PORT=$((19000 + OFFSET))
|
||||
FRONTEND_PORT=$((14000 + OFFSET))
|
||||
```
|
||||
|
||||
Start the backend:
|
||||
```bash
|
||||
E2E_TMP=$(mktemp -d) && mkdir -p "$E2E_TMP/.kandev" && \
|
||||
printf '[user]\n name = E2E Test\n email = e2e@test.local\n[commit]\n gpgsign = false\n' > "$E2E_TMP/.gitconfig" && \
|
||||
HOME="$E2E_TMP" KANDEV_HOME_DIR="$E2E_TMP/.kandev" KANDEV_SERVER_PORT=$BACKEND_PORT \
|
||||
KANDEV_DATABASE_PATH="$E2E_TMP/kandev.db" KANDEV_MOCK_AGENT=only \
|
||||
KANDEV_MOCK_GITHUB=true KANDEV_DOCKER_ENABLED=false KANDEV_WORKTREE_ENABLED=false \
|
||||
KANDEV_LOG_LEVEL=warn apps/backend/bin/kandev &
|
||||
```
|
||||
|
||||
Start the dev frontend:
|
||||
```bash
|
||||
KANDEV_API_BASE_URL=http://localhost:$BACKEND_PORT VITE_KANDEV_API_PORT=$BACKEND_PORT \
|
||||
pnpm --filter @kandev/web dev --port $FRONTEND_PORT &
|
||||
```
|
||||
|
||||
### Validate with playwright-cli
|
||||
|
||||
```bash
|
||||
pnpm --dir apps exec playwright-cli open http://localhost:$FRONTEND_PORT
|
||||
pnpm --dir apps exec playwright-cli snapshot # see page structure and element refs
|
||||
pnpm --dir apps exec playwright-cli click e5 # interact using refs from snapshot
|
||||
pnpm --dir apps exec playwright-cli fill e3 "test input"
|
||||
pnpm --dir apps exec playwright-cli snapshot # verify result
|
||||
```
|
||||
|
||||
### Fast iteration cycle
|
||||
|
||||
1. Make a code change in `apps/web/`
|
||||
2. HMR picks it up in ~1-2 seconds
|
||||
3. `pnpm --dir apps exec playwright-cli snapshot` or `pnpm --dir apps exec playwright-cli screenshot` to verify
|
||||
4. Repeat until the flow works correctly
|
||||
|
||||
### Translate to E2E test
|
||||
|
||||
Once validated, write the Playwright test using project fixtures and page objects. The `playwright-cli` interactions map directly to Playwright API calls:
|
||||
|
||||
| playwright-cli | Playwright API |
|
||||
|---|---|
|
||||
| `playwright-cli click e5` | `page.getByTestId('...').click()` |
|
||||
| `playwright-cli fill e3 "text"` | `page.getByTestId('...').fill('text')` |
|
||||
| `playwright-cli snapshot` (verify element visible) | `expect(page.getByTestId('...')).toBeVisible()` |
|
||||
|
||||
Use `data-testid` selectors in the test (not snapshot refs), and wrap common flows in page objects.
|
||||
|
||||
### Capture PR evidence
|
||||
|
||||
After confirming the feature works, capture screenshots or a video as proof for the PR:
|
||||
|
||||
```bash
|
||||
# Screenshots of key states
|
||||
pnpm --dir apps exec playwright-cli screenshot --filename=apps/web/.pr-assets/feature-before.png
|
||||
# ... interact to show the feature ...
|
||||
pnpm --dir apps exec playwright-cli screenshot --filename=apps/web/.pr-assets/feature-after.png
|
||||
|
||||
# Or record a video walkthrough
|
||||
pnpm --dir apps exec playwright-cli video-start apps/web/.pr-assets/feature-demo.webm
|
||||
# ... perform the user flow ...
|
||||
pnpm --dir apps exec playwright-cli video-stop
|
||||
```
|
||||
|
||||
Create `apps/web/.pr-assets/manifest.json` so the `/pr` skill picks them up:
|
||||
```json
|
||||
{
|
||||
"assets": [
|
||||
{"name": "feature-demo", "file": "feature-demo.webm", "format": "gif", "caption": "Feature demo"},
|
||||
{"name": "feature-after", "file": "feature-after.png", "format": "png", "caption": "Result"}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Final verification
|
||||
|
||||
Always verify against the production build before finishing — dev mode can hide boot-payload, asset-serving, or hydration issues:
|
||||
|
||||
```bash
|
||||
pnpm --dir apps exec playwright-cli close
|
||||
# Kill dev server and backend
|
||||
make build-web
|
||||
cd apps && pnpm --filter @kandev/web e2e:raw -- tests/path/to/test.spec.ts
|
||||
```
|
||||
@@ -0,0 +1,145 @@
|
||||
# E2E failure triage
|
||||
|
||||
Load this reference for failing tests, CI shard reproduction, or suspected contention.
|
||||
Read [resource-safety.md](resource-safety.md) before full or repeated runs.
|
||||
|
||||
### Flake reproduction
|
||||
|
||||
Start by matching CI as closely as possible. CI uses duration-aware manifests: replay the
|
||||
matching manifest with `E2E_SHARD=<n> bash e2e/scripts/run-planned-shard.sh <manifest.json>`;
|
||||
`--shard=N/14` is only approximate. Regenerate the manifest after source changes and never
|
||||
overlap another managed/raw E2E run. Then add pressure deliberately:
|
||||
|
||||
1. Run the exact failed shard in the CI runtime image with CI env enabled:
|
||||
```bash
|
||||
docker run --rm --ipc=host -v "$PWD":/work -w /work/apps/web \
|
||||
-e CI=true -e GITHUB_ACTIONS=true -e GITHUB_WORKSPACE=/work \
|
||||
-e NODE_OPTIONS=--dns-result-order=ipv4first \
|
||||
-e PLAYWRIGHT_BROWSERS_PATH=/ms-playwright \
|
||||
ghcr.io/kdlbs/kandev-ci:runtime-latest \
|
||||
bash -lc 'git config --global --add safe.directory /work 2>/dev/null; bash e2e/scripts/run-raw-e2e.sh --project=chromium --project=mobile-chrome --shard=<failed-shard>/14 -- --reporter=list --retries=0'
|
||||
```
|
||||
2. If the exact shard passes, constrain container resources and repeat the
|
||||
failing spec/test. GitHub-hosted runners can expose timing bugs that a roomy
|
||||
local machine hides:
|
||||
```bash
|
||||
docker run --rm --ipc=host --cpus=2 --memory=4g --memory-swap=4g \
|
||||
-v "$PWD":/work -w /work/apps/web \
|
||||
-e CI=true -e GITHUB_ACTIONS=true -e GITHUB_WORKSPACE=/work \
|
||||
-e NODE_OPTIONS=--dns-result-order=ipv4first \
|
||||
-e PLAYWRIGHT_BROWSERS_PATH=/ms-playwright \
|
||||
ghcr.io/kdlbs/kandev-ci:runtime-latest \
|
||||
bash -lc 'git config --global --add safe.directory /work 2>/dev/null; bash e2e/scripts/run-raw-e2e.sh --project=mobile-chrome e2e/tests/terminal/mobile-terminal-keybar.spec.ts --grep "user presses an OS-keyboard letter while no modifier is active" --repeat-each=30 --reporter=list --retries=0'
|
||||
```
|
||||
3. Preserve nearby test ordering when a single-test repeat stays green; run the full spec or shard with the same resource limits before declaring a flake non-reproducible.
|
||||
|
||||
The config uses `failOnFlakyTests: !CI`: local runs fail on a flaky retry, while
|
||||
CI temporarily tolerates one. Either result is a failure signal for agents:
|
||||
reproduce it with `--retries=0` only after disabling any `test.describe.configure({ retries: 1 })` override; never
|
||||
rerun until it happens to pass. If isolated repeats stay green but the shard
|
||||
fails, binary-search preceding specs in one worker. The fix is complete only
|
||||
when the smallest reproducing sequence passes without retries.
|
||||
|
||||
Record the exact command, resource limits, repeat number, and failure artifact path. For an explicit no-flakes requirement, download `blob-report-*` with `gh run download <run-id> --pattern 'blob-report-*' --dir <tmp>` and run `python3 scripts/playwright-blob-audit <tmp>`; aggregate green is not flake-free. For a failed shard, inspect every `error-context.md` in its downloaded
|
||||
`test-results-<shard>` artifact and compare shared page-object waits with `main`
|
||||
before changing product code; the context can expose duplicate active terminals
|
||||
or a terminal stuck on "Starting terminal...".
|
||||
|
||||
When a PR E2E shard fails, investigate every spec, including those outside the
|
||||
diff; never dismiss, rerun, or merely record a failure as unrelated. Reproduce
|
||||
the exact test, then preserve shard ordering and CI pressure. Fix every valid
|
||||
defect with retries disabled, or evidence a concrete external blocker.
|
||||
|
||||
## Debugging failures
|
||||
|
||||
### Triage
|
||||
|
||||
When a test fails:
|
||||
|
||||
1. **Read the error output** — the Playwright error message, expected vs. actual, and which locator timed out
|
||||
2. **Read `error-context.md`** from `test-results/<test-name>/` — contains a YAML DOM snapshot showing exactly what was rendered. Search for expected elements, check if the page is in the right state (e.g., simple mode vs advanced mode). **These files persist across runs** — always confirm timestamps (portable: `ls -la e2e/test-results/.../error-context.md`; or `stat -c %y` on Linux / `stat -f %Sm` on macOS) or rebuild + rerun the spec fresh before trusting the snapshot. A stale context from a previous failure mode will send you debugging the wrong bug.
|
||||
3. **Read the failure screenshot** from `e2e/test-results/` — see what the page actually rendered
|
||||
4. **Attach to the failure** for deeper debugging using `playwright-cli`:
|
||||
```bash
|
||||
cd apps && PLAYWRIGHT_HTML_OPEN=never pnpm --filter @kandev/web e2e:raw -- tests/path.spec.ts --debug=cli &
|
||||
# Wait for "Debugging Instructions" with session name
|
||||
pnpm --dir apps exec playwright-cli attach tw-<session>
|
||||
pnpm --dir apps exec playwright-cli snapshot # inspect page state at failure point
|
||||
pnpm --dir apps exec playwright-cli console # check for JS errors
|
||||
pnpm --dir apps exec playwright-cli network # check API responses
|
||||
```
|
||||
|
||||
### Classify and fix
|
||||
|
||||
| Category | Signals | Fast loop |
|
||||
|---|---|---|
|
||||
| **Test logic** | Wrong selector, wrong expected text, missing page object method | Fix test files, re-run immediately (no rebuild -- Playwright transpiles TS at runtime) |
|
||||
| **Frontend-only** | Screenshot shows wrong UI, missing element, client error. API calls succeed. | Start dev server, fix with hot reload, verify with `playwright-cli`, then `make build-web` + re-run test |
|
||||
| **Backend** | 500 errors, wrong API response, "Backend did not become healthy" | Fix Go code, `make build-backend`, re-run test |
|
||||
|
||||
### Common issues
|
||||
|
||||
- **"Backend did not become healthy"** — run `make build-backend build-web`, check with `E2E_DEBUG=1`
|
||||
- **"Cannot find module"** — run `cd apps && pnpm install --frozen-lockfile`
|
||||
- **Port conflicts** — run raw repeat/stress invocations sequentially, or prove both computed port ranges are disjoint and check listeners. A totally white screenshot plus `ERR_CONNECTION_REFUSED` is a port/lifecycle signature to rule out before changing waits; never fix it with longer locator timeouts.
|
||||
- **Responsive layout stays stale after `page.setViewportSize()`** — record
|
||||
`window.innerWidth`, the affected element and parent `clientWidth`, and any
|
||||
layout-library width before changing waits. Headless Chromium reliably
|
||||
resizes the DOM/container and fires `ResizeObserver`, while an
|
||||
application-only `window.resize` listener may not be observed in the test.
|
||||
Prefer synchronizing with the layout container/observer and assert the
|
||||
intended result after both viewport and container-only changes.
|
||||
- **Initial hydration/readiness regressions:** do not use a page-object readiness helper that can reload or re-navigate the page (for example `SessionPage.waitForLoad`) to prove first-render behavior. Wait directly for the invariant locator on the current navigation, using a bounded non-reloading wait when needed. Keep reload-capable helpers for persistence and recovery scenarios.
|
||||
- **Auto-started session never goes idle** — for sessions started by the same call that creates them, the mock agent can finish before the client WS subscription registers, so a raw `idleInput()` visibility wait hangs. Use `SessionPage.waitForChatIdle()` before opening transient dialogs/drawers/popovers; it may reload and re-derive state from the Go boot payload. If it must run later, reopen the transient UI first. For WS/session hydration races, retain a bounded reload-and-retry fallback in the page object: keep the fast path immediate and the final check failing when genuinely stuck; remove it only with an equivalent deterministic readiness guarantee and focused regression.
|
||||
- **Editor readiness is not submit readiness** — a `contenteditable` may be present and writable while a session is still `STARTING`. Before submitting, wait for the scoped submit control to be enabled or for the exact session to reach `WAITING_FOR_INPUT`; use retries disabled when reproducing this boundary.
|
||||
- **Flaky timeouts** — **never increase locator timeouts to fix flaky tests.** If a locator times out, the root cause is almost always something else: a setup failure, missing navigation, race condition, or the element genuinely not rendering. Investigate why the element never appears instead of giving it more time. Note: infrastructure health timeouts (30s in `fixtures/backend.ts`) and overall test timeouts (60s in `playwright.config.ts`) are separate and should not be modified either.
|
||||
- Screenshots on failure, video on first retry (CI). In workflow cache steps, `actions/cache/restore` sets `cache-hit` to `true` for an exact key, `false` for a restore-key/prefix hit, and empty on a miss; gate verification/fallback on empty versus non-empty, smoke-test Chromium before skipping image extraction, and use bounded backoff for transient registry metadata probes such as `docker buildx imagetools inspect`.
|
||||
- **Page-closed errors after timeouts** — `Target page, context or browser has been closed` can be teardown masking an earlier overlay interception. Inspect the preceding action and screenshot, for example with `rtk proxy unzip -p <trace.zip> 0-trace.trace | rtk proxy jq -c 'select(.type == "action" or .type == "error")'`, then close the overlay in the page object, assert it is closed, and rerun with retries disabled before changing timeouts or routes.
|
||||
|
||||
### Debugging CI shard failures
|
||||
|
||||
CI splits host tests across 14 shards (plus 6 container shards); reproduce a specific shard locally. Download `e2e-shard-manifests` when available, verify its selected file list matches the failed job, and prefer the CI runtime image:
|
||||
|
||||
```bash
|
||||
# Run the exact duration-aware manifest-selected shard
|
||||
E2E_SHARD=2 KANDEV_E2E_SKIP_FRESHNESS=1 bash e2e/scripts/run-planned-shard.sh <workspace>/e2e-manifests/normal/2.json
|
||||
# If no manifest exists, inspect the ordinal fallback with `npx playwright test --config e2e/playwright.config.ts --shard=2/14 --list`, then run it with a production build
|
||||
make build-backend build-web
|
||||
cd apps/web && pnpm e2e:raw -- --shard=2/14
|
||||
```
|
||||
|
||||
```bash
|
||||
# Unzip a shard's blob report from CI artifacts
|
||||
unzip report-*.zip -d report-shard && cat report-shard/*.jsonl
|
||||
```
|
||||
|
||||
When a CI shard fails, distinguish its useful artifacts: download/unzip
|
||||
`report-*.zip` to map test IDs and timings; `test-results-<shard>` may be ready
|
||||
before the workflow completes even when logs refuse, so try it for the exact assertion, `error-context.md`, screenshot, and trace:
|
||||
|
||||
```bash
|
||||
gh run download <run-id> --name test-results-<shard> --dir <temp-dir>
|
||||
```
|
||||
|
||||
The blob report surfaces slow-but-passing specs that are latent flake risks;
|
||||
the test-results artifact identifies the concrete failure to reproduce. Specs
|
||||
whose duration approaches the 60s per-test timeout (defined in
|
||||
`playwright.config.ts`) are candidates to harden, typically by converting raw
|
||||
chat-flow assertions to the `waitForChatIdle()` / `expectChatResponseVisible()`
|
||||
recovery helpers documented earlier in this file.
|
||||
|
||||
### Flake triage: intrinsic race vs. contention
|
||||
|
||||
A failure under load can indicate a race, leaked fixture state, or resource contention.
|
||||
Gather evidence before assigning a cause:
|
||||
|
||||
1. **Re-run it in a fresh, isolated container** (or at minimum a single fresh worker), `--retries=0`, a few reps:
|
||||
```bash
|
||||
pnpm e2e:docker --no-build -- --repeat-each=4 --workers=1 --retries=0 tests/path.spec.ts:LINE
|
||||
# or raw: pnpm e2e:raw -- --project=chromium --repeat-each=4 --retries=0 tests/path.spec.ts:LINE
|
||||
```
|
||||
(On Apple Silicon, `pnpm e2e:docker` needs Colima + Rosetta — `colima start --vz-rosetta`; default QEMU segfaults the amd64 Go build. See `apps/web/e2e/README.md`.)
|
||||
- **Flakes alone (fails some reps, fast):** intrinsic race — fix it (condition-correct wait, fix the actual race; not a timeout bump). E.g. a `waitForRequest` that times out the full window means the request *never fired* (a click swallowed during hydration) — retry the action with `await expect(async () => { ... }).toPass()`, don't extend the timeout.
|
||||
- **Passes clean and fast alone:** the cause remains unknown. Preserve shard order and resource limits to distinguish fixture leakage from contention.
|
||||
2. **Different failure counts across identical runs are inconclusive.** Races can also produce different results. Record CPU, memory, and process pressure. Compare resource-bounded runs before attributing the failure to oversubscription.
|
||||
3. **Caveat — don't flake-hunt with `--repeat-each` across many heavy specs in one long-lived worker.** It exhausts per-worker resources (agentctl port range, memory) over a long run and manufactures *false* failures unrelated to the test. Use **one fresh container per spec** instead.
|
||||
@@ -10,6 +10,16 @@ For tests using agent models, modes, commands, or options, treat
|
||||
or test ID and bounded-poll for the semantic capability before interacting.
|
||||
Avoid arbitrary sleeps and `force: true` clicks.
|
||||
|
||||
## Managed-runtime capability probes
|
||||
|
||||
Host-utility capability checks call the managed agent's `IsInstalled()` probe
|
||||
before they invoke the managed `npx` path. If a fixture replaces `npx` to test
|
||||
package recovery, also put a discoverable executable for that agent on the
|
||||
fixture `PATH` (for example, a scoped `opencode` shim). Assert that the initial
|
||||
capability state is `ok` before exercising recovery; otherwise the fixture can
|
||||
stop at `not_installed` and never test the intended path. Verify with the
|
||||
focused containers test and `--retries=0`.
|
||||
|
||||
## Remembered workflow selection
|
||||
|
||||
After seeding tasks in multiple workflows, set
|
||||
|
||||
@@ -10,6 +10,11 @@ most three local shards, and lowers that limit when available memory is below
|
||||
runs inside a container. This prevents host memory reporting from authorizing
|
||||
more shards than the container can hold.
|
||||
|
||||
Use `pnpm e2e:run`, guarded `pnpm e2e:raw`, or `make test-e2e` for ordinary
|
||||
verification. Reserve direct Playwright commands, all-worker overrides, and
|
||||
overlapping full suites for deliberate pressure or guard reproduction. Keep
|
||||
those experiments to one worker or shard by default and record the reason.
|
||||
|
||||
Start with the default single shard; use two or three only when the host has
|
||||
capacity for separate Go backends, SPA processes, Chromium instances, and mock
|
||||
agents. If a command is rejected, reduce its shard or worker count. Do not
|
||||
|
||||
@@ -23,6 +23,23 @@ already-active session may emit no new subscribe frame; consume the original
|
||||
frame or trigger a distinct state transition. Verify ordering in the owning
|
||||
Playwright project with `--retries=0`.
|
||||
|
||||
Arm every WebSocket wait before the action that should produce it, including all
|
||||
waits when one action emits multiple notifications. Establish startup state with
|
||||
REST `expect.poll`, then correlate each post-action event by an active-to-settled
|
||||
transition, revision, or timestamp; `watchWs` does not buffer frames, so never
|
||||
attach a wait after a spinner or visibility assertion.
|
||||
|
||||
## Automatic lifecycle evidence
|
||||
|
||||
When a test proves automatic start, resume, or recovery, seed and assert the
|
||||
exact precondition before the stimulus. For archive recovery, that means an
|
||||
active `WAITING_FOR_INPUT` session whose archive cancellation records the archive
|
||||
reason. Reset transport capture immediately before the stimulus, then require
|
||||
the automatic request or event and passive backend-state polling before any
|
||||
retry or manual recovery action. Do not call `SessionPage.waitForChatIdle()` or
|
||||
another helper that reloads/clicks recovery as the first proof, because it can
|
||||
mask a missing automatic request. Verify the full spec with `--retries=0`.
|
||||
|
||||
## Terminal and Dockview helpers
|
||||
|
||||
- Scope terminal/mobile helpers to the active `data-testid="terminal-panel"`;
|
||||
@@ -32,6 +49,20 @@ Playwright project with `--retries=0`.
|
||||
- If a helper uses `window.__dockviewApi__`, poll until the API is attached;
|
||||
silently skipping cleanup during page initialization leaks panels into later
|
||||
assertions.
|
||||
- Dockview-capable specs must activate a visible public entry point (a tab or
|
||||
topbar/toolbar affordance) instead of assuming the default tab exists after
|
||||
another scenario. Scope assertions to the visible panel and run the full spec
|
||||
to catch order-dependent persisted-layout state.
|
||||
|
||||
For a restored hidden-surface regression, seed the persisted multi-panel state,
|
||||
leave the target inactive, reload, and prove the target is mounted before
|
||||
activating it through the shipped control. If the failure depends on a browser
|
||||
observer missing the hidden-to-visible geometry transition, install a
|
||||
pre-navigation wrapper that suppresses callbacks only for the target entries
|
||||
and leaves unrelated observers intact. Assert both the correctly shaped
|
||||
network request, including its cursor or direction, and the visible user
|
||||
result. This distinguishes lifecycle recovery from an accidentally cooperative
|
||||
observer without exposing a test-only production entry point.
|
||||
|
||||
## Sidebar and context-menu editing
|
||||
|
||||
|
||||
@@ -35,9 +35,18 @@ If the cause remains uncertain, stop and ask the user.
|
||||
|
||||
## Phase 1: Reconcile specifications
|
||||
|
||||
Run the `/interview-me` assumption check for intended behavior and repair scope.
|
||||
Reuse active acceptance criteria for clear regressions. Ask only about material
|
||||
choices that evidence and prior instructions do not settle.
|
||||
|
||||
Read `docs/specs/README.md`, the owning system index, and the relevant
|
||||
requirement and system-design documents. Use the legacy catalog only when the
|
||||
system has not migrated.
|
||||
Read the owning system `README.md` and use this command for discovery:
|
||||
|
||||
python3 scripts/list-docs.py specs --format paths
|
||||
|
||||
Add `--kind legacy` when the system has not migrated.
|
||||
|
||||
Search adjacent systems before you create or move an artifact. A UI symptom does
|
||||
not make the repair UI-owned. Update the system that owns the failed contract,
|
||||
@@ -57,6 +66,12 @@ Classify the bug:
|
||||
Do not create a standalone repair specification. The requirement is the durable
|
||||
behavioral source. The plan and work orders record this repair.
|
||||
|
||||
For predicates over collections or inventory, state the quantifier explicitly:
|
||||
distinguish existential conditions such as “at least one live row” from
|
||||
universal conditions such as “all rows.” When behavior depends on that
|
||||
distinction, require a mixed-state regression containing both live and failed or
|
||||
deleted rows.
|
||||
|
||||
Do not create parallel feature and UI requirements for one repair. A separate UI
|
||||
requirement is valid only when the repair changes an independent reusable UI
|
||||
contract.
|
||||
@@ -77,6 +92,9 @@ The package must:
|
||||
- Name the regression test that fails before the correction.
|
||||
- Name exact files, dependencies, acceptance conditions, and commands.
|
||||
- Use dependency waves only when they clarify implementation order.
|
||||
- For rendered UI changes, include ASCII previews in the plan and relevant UI
|
||||
work orders per `docs/specs/guide/plans-and-work-orders.md#ascii-ui-previews`.
|
||||
Show before/after when it explains the defect and correction.
|
||||
|
||||
Keep work orders sequential by default. A wave does not authorize delegation.
|
||||
|
||||
@@ -89,6 +107,8 @@ Before you change production or permanent test code, report:
|
||||
- Plan and work-order paths.
|
||||
- Dependency order and exact commands.
|
||||
- Risks and exclusions.
|
||||
- For UI changes, a compact inline ASCII preview of the correction, including
|
||||
phone composition when different, and links to the full plan/work-order views.
|
||||
|
||||
Then end the turn. Do not ask the user to approve the package or switch models.
|
||||
Wait for a later explicit implementation request.
|
||||
|
||||
@@ -49,10 +49,18 @@ mirror unless the user explicitly requests a policy reversal.
|
||||
- Put deterministic logic in `scripts/` when agents keep retyping fragile shell/API sequences.
|
||||
- Avoid creating multiple aliases for the same behavior.
|
||||
|
||||
- **Separate independent policy changes:** Keep factual instruction updates
|
||||
and CI changes required by a feature in that feature's PR.
|
||||
Use a separate initiative for independent workflow-policy changes unless
|
||||
the user requests a combined PR. Classify changes by purpose, not path.
|
||||
|
||||
4. **Preserve progressive disclosure**
|
||||
- Keep `SKILL.md` concise.
|
||||
- Move platform tables, long examples, templates, and edge-case notes to `references/`.
|
||||
- Reference each supporting file explicitly from the main skill so future agents know when to load it.
|
||||
- Review word and byte counts as well as line counts. Do not join paragraphs
|
||||
to satisfy a line limit. Move specialized procedures into references with
|
||||
explicit loading conditions. Keep each rule in one authoritative location.
|
||||
|
||||
5. **Edit and validate**
|
||||
- Use `apply_patch` for file edits.
|
||||
|
||||
@@ -76,9 +76,10 @@ Codex subagents inherit the parent sandbox policy and live runtime overrides. A
|
||||
|
||||
Use read-only sandbox for review, exploration, and security agents. Use write access only for implementers or fixers.
|
||||
|
||||
Kandev maintains only the read-only `.codex/agents/pr-poller.toml`, using Codex
|
||||
Spark for an explicitly user-authorized PR wait. Other project-local agents
|
||||
require the user's explicit request and cost approval.
|
||||
Kandev maintains only the read-only `.codex/agents/pr-poller.toml`, using
|
||||
GPT-5.6 Luna at medium reasoning effort for an explicitly user-authorized PR
|
||||
wait. Other project-local agents require the user's explicit request and cost
|
||||
approval.
|
||||
|
||||
## Conversion Notes
|
||||
|
||||
|
||||
@@ -8,9 +8,20 @@ For each learning, extract:
|
||||
|
||||
- **Trigger:** when future agents should apply it.
|
||||
- **Failure mode:** what went wrong or wasted time.
|
||||
- **Evidence:** the observed command, assertion, log, or source that supports
|
||||
the lesson. Keep source task/PR links in the collection record.
|
||||
- **Applicability:** the conditions where the lesson holds and known exceptions.
|
||||
- **Correct action:** exact behavior, command, fallback, or interpretation.
|
||||
- **Verification:** how an agent knows the workaround succeeded.
|
||||
- **Home:** skill, agent, script, AGENTS.md, or command.
|
||||
- **Existing guidance:** the rule this lesson replaces or strengthens.
|
||||
|
||||
Keep unverified explanations in the collection record as hypotheses.
|
||||
Before promotion, verify the explanation against current code or authoritative
|
||||
documentation. One verified incident can justify a narrowly scoped rule.
|
||||
Do not turn its workaround into a universal requirement without supporting evidence.
|
||||
Keep session history in the collection record and reusable instructions in the
|
||||
owning skill or scoped guidance. Encode deterministic checks in scripts and tests.
|
||||
|
||||
Prefer this shape inside skills:
|
||||
|
||||
@@ -31,11 +42,11 @@ only on explicit user request or to remediate a PR/CI finding.
|
||||
|
||||
## Placement Rules
|
||||
|
||||
- PR/CI/review-thread behavior belongs in `.agents/skills/pr-fixup/SKILL.md`.
|
||||
- PR/CI/review-thread behavior belongs in `.agents/skills/pr-fixup/`.
|
||||
- PR creation/body/push behavior belongs in `.agents/skills/pr/SKILL.md` or `.agents/skills/push/SKILL.md`.
|
||||
- Full repo verification belongs in `.agents/skills/verify/SKILL.md`.
|
||||
- TDD/test-level guidance belongs in `.agents/skills/tdd/SKILL.md`.
|
||||
- E2E commands and flake triage belong in `.agents/skills/e2e/SKILL.md`.
|
||||
- E2E commands and flake triage belong in `.agents/skills/e2e/`.
|
||||
- Runtime debugging belongs in `.agents/skills/debug/`.
|
||||
- Backend/frontend conventions belong in the closest scoped `AGENTS.md`.
|
||||
- Repeated fragile commands should become or update a `scripts/` helper, with syntax and mocked behavior checks.
|
||||
|
||||
@@ -9,7 +9,7 @@ python3 .github/scripts/lint-harness-files.py --all
|
||||
python3 scripts/lint-spec-files.test.py
|
||||
python3 scripts/lint-spec-files.py --all
|
||||
git diff --check -- <changed-files>
|
||||
wc -l <changed-harness-files>
|
||||
wc -l -w -c <changed-harness-files>
|
||||
```
|
||||
|
||||
Before committing, also run the targeted pre-commit hook:
|
||||
@@ -21,3 +21,7 @@ pre-commit run harness-lint --files <changed-files>
|
||||
Keep every `AGENTS.md` and `CLAUDE.md` file at 300 lines or fewer. Move detail
|
||||
to the nearest scoped file or a referenced document when a parent reaches the
|
||||
limit.
|
||||
|
||||
Compare word and byte counts with the previous version. If the common workflow
|
||||
grows, remove duplicate rules or move conditional detail into linked references.
|
||||
Line limits do not measure reading cost.
|
||||
|
||||
@@ -1,87 +1,111 @@
|
||||
---
|
||||
name: interview-me
|
||||
description: Clarify what the user wants before requirements, system design, plans, or code. Use when an ask is underspecified, when the user asks for an interview or stress test, or when important product or architecture assumptions are missing.
|
||||
description: Clarify a standalone idea or check assumptions before feature or fix planning. Use focused questions, stress-test intent, and map unresolved decisions for large uncertain initiatives.
|
||||
---
|
||||
|
||||
# Interview Me
|
||||
|
||||
Use this before `/spec-driven-development`, `/spec`, or `/plan` when the requested outcome is not clear enough to implement without guessing.
|
||||
Run the assumption check before requirements, system designs, or implementation
|
||||
plans. Reuse settled answers throughout the design package. An assumption check
|
||||
does not require an interview when the material choices are already clear.
|
||||
|
||||
## When To Use
|
||||
For a standalone interview or stress test, clarify intent without automatically
|
||||
creating a design package. Skip this workflow for mechanical edits and pure
|
||||
information requests.
|
||||
|
||||
Use when the ask is missing one or more of:
|
||||
- Who the user/operator is
|
||||
- Why this matters now
|
||||
- What success looks like
|
||||
- The binding constraint or tradeoff
|
||||
- Explicit out-of-scope boundaries
|
||||
## 1. Check assumptions
|
||||
|
||||
Skip for mechanical edits, obvious bug fixes, pure information requests, or when the user explicitly asks for speed over clarification.
|
||||
Read the request, relevant specifications, source, and tests. Separate:
|
||||
|
||||
## Process
|
||||
- **Confirmed:** the user explicitly requested or previously settled it.
|
||||
- **Verified:** source, tests, or documentation establish a fact. Keep its source
|
||||
reference and distinguish current behavior from intended behavior.
|
||||
- **Unresolved:** a choice or missing fact can change behavior, scope, ownership,
|
||||
permissions, persistence, compatibility, or acceptance criteria.
|
||||
|
||||
### 1. State A Hypothesis
|
||||
State the intended outcome and material unresolved assumptions briefly. Do not
|
||||
invent confidence percentages. Investigate facts that available tools can
|
||||
resolve before asking the user. Evidence of current behavior is not user
|
||||
agreement to preserve it.
|
||||
|
||||
Write one sentence plus a confidence number.
|
||||
Ask about consequential choices that evidence cannot settle. Choose routine,
|
||||
reversible implementation details within the agreed scope. A request for speed
|
||||
reduces questions; it does not make an unanswered material choice confirmed.
|
||||
|
||||
```text
|
||||
HYPOTHESIS: You want workspace switching to preserve task context because agents lose momentum when users navigate between workspaces.
|
||||
CONFIDENCE: 45% - missing: who feels the pain, what "preserve" means, and what counts as done.
|
||||
```
|
||||
For a clear regression, use the active acceptance criterion without asking the
|
||||
user to define the behavior again.
|
||||
|
||||
### 2. Ask Focused Questions
|
||||
## 2. Ask questions in dependency order
|
||||
|
||||
Format every question with a guess.
|
||||
Identify which decisions depend on other answers. Ask only questions whose
|
||||
prerequisites are settled. Use the active harness's user-question tool and obey
|
||||
its limits and waiting rules. Ask one to four independent questions per round,
|
||||
within those limits. Without a question tool, ask one question at a time in chat
|
||||
during a normal interactive session.
|
||||
|
||||
```text
|
||||
Q: Is this primarily for human users switching between workspaces, or for office agents operating across workspaces?
|
||||
GUESS: Human users, because the pain sounds navigation-related rather than automation-related.
|
||||
```
|
||||
If this is an autopilot root or another non-interactive session with no question
|
||||
tool, record each unresolved material choice as an assumption. Continue only
|
||||
when the caller permits autonomous planning for that choice and use the most
|
||||
conservative reversible option. Otherwise return the assumption as a blocker to
|
||||
the caller. Never present an assumption as confirmed, and preserve the normal
|
||||
chat fallback for interactive sessions.
|
||||
|
||||
If the active harness provides a native user-question UI that supports multiple
|
||||
questions in one turn, ask 2-4 focused questions together. Keep each question
|
||||
short, include your guess in the prompt or options, and make the options
|
||||
concrete enough that the user can answer quickly.
|
||||
Give each question concrete options, a recommended answer, and a short reason.
|
||||
Align the question with the recommendation so agreement has one clear meaning.
|
||||
Do not ask the user to locate code or supply facts you can investigate.
|
||||
|
||||
If no multi-question tool is available, ask one question at a time in chat. Do not send a long questionnaire.
|
||||
For example, first settle what "preserve task context" includes. Ask about
|
||||
retention duration only after the user chooses persistent context.
|
||||
|
||||
### 3. Probe Convention-Sounding Answers
|
||||
Wait for answers before resolving dependent choices. After each round, update
|
||||
the remaining questions. If an answer changes an earlier assumption, revisit
|
||||
the affected choices and artifacts. Do not repeat settled questions without new
|
||||
evidence or changed scope.
|
||||
|
||||
If the user says "modern", "scalable", "best practice", "dashboard", "robust", or "clean architecture" without concrete outcomes, ask:
|
||||
If the initiative has too many dependent unknowns to specify reliable work
|
||||
orders, use [decision mapping](references/decision-mapping.md). Ordinary features
|
||||
and clear fixes do not need a map.
|
||||
|
||||
```text
|
||||
If you did not have to justify this as best practice, what would you actually want?
|
||||
```
|
||||
## 3. Stress-test the answers
|
||||
|
||||
### 4. Restate Intent
|
||||
Use concrete scenarios to expose material gaps in success, failure, recovery,
|
||||
and scope. Select scenarios relevant to the request; do not invent adjacent
|
||||
features or an exhaustive questionnaire.
|
||||
|
||||
When confidence is high, restate in this shape:
|
||||
Challenge vague terms such as "robust" with an observable outcome. Compare domain
|
||||
terms with the owning specifications and any existing glossary. If a term or
|
||||
claimed behavior conflicts with those sources, name the conflict and resolve it.
|
||||
|
||||
```text
|
||||
Here's what I think you want:
|
||||
- Outcome:
|
||||
- User:
|
||||
- Why now:
|
||||
- Success:
|
||||
- Constraint:
|
||||
- Out of scope:
|
||||
Distinguish uncertainty that needs evidence from a choice that needs the user.
|
||||
If a design question needs an experiment, define the question and observable
|
||||
result first. Keep prototypes temporary and separate from production changes.
|
||||
Record what the experiment proves and what remains undecided.
|
||||
|
||||
Yes / no / refine?
|
||||
```
|
||||
## 4. Capture decisions and continue
|
||||
|
||||
Do not proceed to `/spec`, `/plan`, or implementation until the user explicitly confirms or corrects the restate.
|
||||
After each answer, preserve the choice and its reason in the current task notes.
|
||||
Use the Kandev task plan when available, preserving user edits. Do not create a
|
||||
separate intent document unless requested.
|
||||
|
||||
## Output
|
||||
During authorized specification or planning work, put settled behavior in the
|
||||
owning requirement and technical contracts in its system design. Use `/record`
|
||||
for significant decisions that meet its ADR criteria. Preserve meaningful
|
||||
alternatives and rationale there. Update an existing glossary when terminology
|
||||
changes; do not introduce a parallel glossary by default.
|
||||
|
||||
The deliverable is a confirmed statement of intent. If the user wants it
|
||||
persisted, save it only after confirmation. Use `/spec` to update the owning
|
||||
system requirement. Do not create a standalone intent document unless the user
|
||||
explicitly requests one.
|
||||
Keep unresolved choices visibly unresolved. A recommendation, silence, or timeout
|
||||
is not confirmation. If the user explicitly delegates a choice, record the
|
||||
selected option as an agent decision made under that delegation.
|
||||
|
||||
## Red Flags
|
||||
When intent is clear and no material choice blocks the next design phase,
|
||||
finish the interview. This includes success criteria, constraints, and scope.
|
||||
Explicit answers and prior
|
||||
instructions count as confirmation; do not require a second approval of their
|
||||
restatement. If broad intent remains ambiguous, ask about the specific remaining
|
||||
gap before continuing.
|
||||
|
||||
- Long open-ended questionnaires with no guesses
|
||||
- A question with no guess attached
|
||||
- Accepting "whatever you think" as confirmation
|
||||
- Starting a spec or plan before the user confirms the restate
|
||||
- No explicit out-of-scope line
|
||||
Summarize the settled intent and exclusions. If another skill called this check,
|
||||
return to its current phase without restarting specification or planning.
|
||||
For a direct planning request, continue through `/spec` and `/plan` to the
|
||||
existing design-package handoff. For a standalone interview, return the clarified
|
||||
intent. Neither path authorizes implementation or delegation.
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
# Decision mapping
|
||||
|
||||
Use this mode for a large initiative whose dependent unknowns prevent reliable
|
||||
specifications or work orders. Start with the desired outcome and scope. Keep
|
||||
ordinary planning in the main interview workflow.
|
||||
|
||||
## Map only what is known
|
||||
|
||||
Use a section in the existing Kandev task plan when available. Otherwise use
|
||||
working notes. For work that must survive across sessions without that tool,
|
||||
use `docs/plans/<initiative>/discovery.md`. Read existing content before updates.
|
||||
|
||||
The map contains:
|
||||
|
||||
- **Destination:** the outcome this investigation must make possible.
|
||||
- **Open decisions:** precise questions, dependencies, evidence needed, and status.
|
||||
- **Settled decisions:** short summaries linked to their authoritative artifacts.
|
||||
- **Not yet specified:** in-scope areas whose questions depend on earlier answers.
|
||||
- **Out of scope:** excluded work and the reason for exclusion.
|
||||
|
||||
For each open decision, use a short name and record:
|
||||
|
||||
| Field | Content |
|
||||
| --- | --- |
|
||||
| Question | One decision or factual uncertainty to resolve |
|
||||
| Depends on | Questions that must resolve first, or none |
|
||||
| Method | Source investigation, external research, prototype, or user question |
|
||||
| Status | Open, blocked, or resolved |
|
||||
| Resolution | Answer, supporting evidence, and affected artifact links |
|
||||
|
||||
An unanswered but precise question belongs in open decisions, even when blocked.
|
||||
Use "Not yet specified" only when the question itself is still unclear.
|
||||
Do not turn vague areas into speculative implementation tasks.
|
||||
|
||||
## Resolve dependencies
|
||||
|
||||
Work on questions with settled prerequisites. Investigate facts in the primary
|
||||
session. Ask the user about material choices through the interview workflow.
|
||||
For a prototype, state the question, smallest experiment, and evidence needed
|
||||
before creating temporary files. A prototype does not authorize production code.
|
||||
|
||||
After each resolution, revisit dependent questions. Add questions that are now
|
||||
precise, update invalidated answers, and remove resolved uncertainty. If a
|
||||
question is outside the destination, record its exclusion instead of expanding
|
||||
the initiative.
|
||||
|
||||
Continue while useful investigation is possible within the authorized scope.
|
||||
There is no one-decision-per-session limit. Do not create tracker issues,
|
||||
persistent Kandev tasks, additional sessions, or subagents without explicit
|
||||
authorization for those operations.
|
||||
|
||||
## Return to the design package
|
||||
|
||||
Finish discovery when no unresolved decision blocks the requirements, system
|
||||
design, or reliable implementation order. Exclude remaining uncertainty from
|
||||
the current scope explicitly, or keep the affected work blocked.
|
||||
|
||||
Move settled contracts into the owning requirements and system designs. Use
|
||||
`/record` only when its ADR criteria apply. Replace detailed map resolutions
|
||||
with summaries and links after those artifacts become authoritative.
|
||||
|
||||
Continue to `/plan` for implementation work orders. Each work order delivers
|
||||
behavior; a discovery question resolves uncertainty. The map is neither an
|
||||
implementation plan nor an additional approval checkpoint. Preserve the normal
|
||||
design-package handoff before implementation.
|
||||
@@ -13,6 +13,10 @@ Use this skill before planning or changing UI. Goal: desktop and mobile deliver
|
||||
|
||||
Before proposing a mobile design, read [Kandev Mobile UI Language](references/kandev-mobile-ui-language.md) and inspect the closest shipped mobile surface. Responsive CSS alone does not establish mobile parity.
|
||||
|
||||
For buttons, single-line inputs, selectors, or control-size audits, also read
|
||||
[Control sizing](references/control-sizing.md). It defines desktop sizes,
|
||||
touch exceptions, and the sweep procedure.
|
||||
|
||||
## When It Applies
|
||||
|
||||
Apply when task changes user-facing UI:
|
||||
@@ -46,6 +50,12 @@ When a task changes composition, navigation, overlays, touch behavior, scrolling
|
||||
- shared state, view-model, filtering/selection, and business logic versus mobile-specific presentation
|
||||
- mobile Playwright scenario proving the same user value
|
||||
|
||||
For a feature or fix design package, record the desktop and phone composition
|
||||
as ASCII previews in the plan and relevant UI work orders. Follow
|
||||
`docs/specs/guide/plans-and-work-orders.md#ascii-ui-previews`; include a compact
|
||||
preview in the final conversation handoff. During implementation, compare the
|
||||
rendered phone surface with the assigned preview's structure and annotations.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Map affected surfaces.
|
||||
@@ -64,8 +74,12 @@ When a task changes composition, navigation, overlays, touch behavior, scrolling
|
||||
3. Implement responsive UI.
|
||||
- Reuse domain hooks, state, view-model derivation, filtering/selection, and action handlers across viewports; keep responsive wrappers focused on presentation. Branch composition when the desktop interaction model depends on width or a fine pointer. Do not mount a heavyweight desktop workbench and merely hide or squeeze it on phones.
|
||||
- Use `useResponsiveBreakpoint` and existing `@kandev/ui` or mobile primitives. Reuse current Dropdown/ContextMenu primitives for contextual actions; use `Drawer` or an existing picker shell for structured phone navigation and choices. Use `useTouchDrawer` when a hover disclosure needs a coarse-pointer alternative.
|
||||
- Use 28px for ordinary desktop buttons, single-line inputs, select triggers, and combobox triggers. Use 24px only for deliberate compact inline controls. Match the desktop Start Task dialog and shared default primitives.
|
||||
- Keep phone and coarse-pointer action targets at least 44px. Scope touch dimensions to those conditions. A desktop-visible action must not inherit an unconditional `h-11` or `min-h-11`. Check both height and minimum height: changing `h-*` does not remove an oversized `min-h-*`.
|
||||
- A Radix Tooltip that happens to open after Playwright `.tap()` is not a coarse-pointer alternative. Use `useTouchDrawer` with a Drawer branch and assert the drawer surface in mobile E2E.
|
||||
- Keep coarse-pointer and mobile touch targets large enough for touch use, generally at least 44px in the active dimension. This is an active hit-area rule, not a universal desktop visual-size rule: fine-pointer desktop controls should retain the surrounding design-system density, and touch-sized classes such as `h-11` must not become the shared desktop button size.
|
||||
- When one component serves both pointer modes, keep its fine-pointer size as the base class and add the 44px size only behind a coarse-pointer or phone variant. An unqualified `h-11` or `min-h-11` on a desktop-visible shared action is a sizing bug.
|
||||
- For a primary mobile dialog action whose contract requires an actual hit target of at least 44px, prefer 48px nominal sizing when browser scaling can produce fractional bounds. Assert the rendered `boundingBox().height` (or width for a horizontal target) is at least 44px; a utility class alone does not prove the physical target.
|
||||
- Use dynamic viewport units and an explicit internal scroll region for viewport-bound/full-height or potentially overflowing surfaces. Ensure bottom-fixed controls and tall drawers clear safe-area insets; short drawers can retain the shared primitive's intrinsic sizing. Keep document-level horizontal overflow at zero.
|
||||
- If mobile substitutes an unsupported desktop view, derive an effective mobile view without overwriting the user's saved desktop preference.
|
||||
- Use semantic controls, visible labels or accessible names, focus return, and existing design-system components.
|
||||
@@ -77,6 +91,9 @@ When a task changes composition, navigation, overlays, touch behavior, scrolling
|
||||
- In this repo, name mobile test files `mobile-*.spec.ts` so the `mobile-chrome` Playwright project picks them up automatically.
|
||||
- Cover the actual mobile composition: drawer or full-height surface, visible overflow action, focused navigation, direct route, or bottom control.
|
||||
- For overlay and dense-navigation changes, assert viewport containment, internal scrolling, and the absence of document horizontal overflow where those properties are part of the regression.
|
||||
- For responsive visual or utility changes, define the base phone behavior and assert computed styles at the canonical phone viewport and just below and above the relevant breakpoint; geometry or overflow checks alone can miss a desktop style leaking into a full-screen mobile surface.
|
||||
- When responsive CSS changes an overlay or absolute child to normal flow (for example, `position: static`), re-check the parent's allocated width and height; a fixed-size desktop wrapper can under-report combined in-flow mobile content.
|
||||
- For containment regressions, compare the interactive control's bounding box with its row, drawer, or viewport bounds. An intrinsic 44px hitbox plus a document-overflow check does not prove that an ancestor is not clipping the control.
|
||||
- When a touch-only control is replaced or hidden, run `rg` across mobile E2E tests for the removed control. Replace every affected interaction with the intended gesture or alternate control, then run those tests together.
|
||||
|
||||
5. Verify visually and behaviorally.
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
# Control sizing
|
||||
|
||||
Use this guide when you add, change, or audit buttons, single-line inputs,
|
||||
select triggers, combobox triggers, or their shared wrappers.
|
||||
|
||||
## Size contract
|
||||
|
||||
| Context | Control height | Usage |
|
||||
| --- | --- | --- |
|
||||
| Desktop, fine pointer | 28px | Ordinary actions and single-line fields |
|
||||
| Compact desktop, fine pointer | 24px | Deliberate inline actions in dense chrome |
|
||||
| Phone or coarse pointer | At least 44px | Action hit areas and single-line fields |
|
||||
|
||||
The pixel values assume the standard 16px root font. Preserve root-font scaling.
|
||||
The desktop Start Task dialog is the reference for ordinary action density.
|
||||
The default `@kandev/ui` Button, Input, and SelectTrigger already use `h-7`.
|
||||
An action's importance changes its variant, not its height.
|
||||
|
||||
Use the same size for neighboring actions, inputs, and selectors within one row.
|
||||
Use matching square dimensions for standalone icon controls.
|
||||
Retain existing specialized controls, such as editor chrome and mobile pills,
|
||||
only with a named purpose and an adjacent precedent.
|
||||
|
||||
Selection cards, navigation rows, multiline fields, and menu options have content
|
||||
geometry. Do not force their entire surface to the ordinary button height.
|
||||
Their embedded ordinary buttons still follow the control contract.
|
||||
|
||||
## Shared implementation
|
||||
|
||||
Use existing shared primitives and sizing helpers before adding local classes.
|
||||
Inspect the current implementation before referencing a new helper from a plan.
|
||||
Keep the fine-pointer desktop size as the base value.
|
||||
Apply larger hit areas through a phone or coarse-pointer condition.
|
||||
Use the canonical 768px phone boundary for new shared sizing rules.
|
||||
Preserve existing overlay boundaries unless the task changes their composition.
|
||||
|
||||
Do not add a global CSS rule that assigns one height to every HTML button.
|
||||
Do not change existing compact or large variant meanings to repair individual callers.
|
||||
Use the standard variant at ordinary action call sites.
|
||||
Keep single-line fields aligned with their attached reveal, clear, and picker buttons.
|
||||
|
||||
Check `height`, `min-height`, `max-height`, padding, line height, and parent stretching together.
|
||||
An `h-7` button with `min-h-11` still occupies at least 44px.
|
||||
A helper with `md:min-h-7` does not shrink a caller's explicit `h-11`.
|
||||
SelectTrigger can also carry a `data-[size=default]:h-*` rule.
|
||||
Verify the CSS cascade in the browser after changing these combinations.
|
||||
|
||||
## Sweep procedure
|
||||
|
||||
1. Search shared primitives, application components, route files, and CSS for height overrides.
|
||||
2. Inspect ordinary Buttons, raw buttons, Inputs, SelectTriggers, combobox wrappers, and inline height styles.
|
||||
3. Follow named class constants, wrapper helpers, size props, and responsive branches to their callers.
|
||||
4. Classify each candidate as standard, compact, touch-only, content-sized, or a documented exception.
|
||||
5. Replace ordinary desktop overrides with the standard size.
|
||||
6. Preserve touch minimums and existing interaction behavior.
|
||||
7. Record each retained exception and its reason in the sweep inventory.
|
||||
8. Repeat the search after migration to find missed wrappers and newly exposed controls.
|
||||
|
||||
This search produces candidates, not confirmed defects:
|
||||
|
||||
```bash
|
||||
rg -n '(min-h-|max-h-|h-|size-)(8|9|10|11|12|\[)|size="(sm|lg)"|height\s*:' \
|
||||
apps/web/components apps/web/app apps/packages/ui/src \
|
||||
-g '*.tsx' -g '*.ts' -g '*.css' -g '!*.test.*'
|
||||
```
|
||||
|
||||
Also inspect controls with padding-only sizing and responsive height resets.
|
||||
Do not apply a blanket replacement to this search output.
|
||||
|
||||
## Rendered verification
|
||||
|
||||
Verify representative dialog, task, settings, and shared-picker surfaces.
|
||||
For each changed family, include normal, disabled, busy, and long-label states.
|
||||
Compare the actual controls, not only their containers or class strings.
|
||||
|
||||
At the standard root font, ordinary desktop controls must measure 28px within 1px.
|
||||
Compact desktop controls must measure 24px within 1px.
|
||||
A desktop assertion of only `height >= 28` permits oversized controls to pass.
|
||||
Touch actions must retain their 44px minimum active dimension.
|
||||
Standalone touch icons need both height and width checks.
|
||||
|
||||
Include desktop fine pointers, phone widths below 768px, and coarse-pointer tablets.
|
||||
Verify keyboard activation, touch activation, and the absence of overlapping hit areas.
|
||||
Preserve the existing coarse-pointer input anti-zoom rule.
|
||||
Check translated labels without reducing their text size to fit a fixed height.
|
||||
|
||||
The durable contract is in
|
||||
[Control sizing requirements](../../../../docs/specs/ui/requirements/control-sizing.md).
|
||||
@@ -35,7 +35,7 @@ Choose based on task frequency and content depth. A frequently revisited primary
|
||||
- Phone breakpoint is below 768px, matching the width at which the desktop sidebar appears. Use `useResponsiveBreakpoint`; it distinguishes phone, tablet, compact desktop, and full desktop using width plus pointer mode. Tablet is a coarse-pointer fallback between 768px and 1024px. The bottom-sheet treatment for Radix menus in `app/globals.css` is deliberately narrower at 640px — pick the boundary that matches what you are changing.
|
||||
- Reuse `@kandev/ui` primitives. `Drawer` is the standard bottom surface; `Sheet` remains useful for tablet/desktop side panels.
|
||||
- Use inset, rounded card surfaces with restrained borders/backgrounds and shadow, matching nearby components. Prefer existing component classes over copying a frozen class list.
|
||||
- Give new primary touch actions, standalone icon buttons, and menu rows an actual hitbox of at least 44px in the active dimension (`min-h-11`, `h-11 w-11`). Existing compact chrome such as `MobilePillButton` is a deliberate density exception, not a sizing precedent for unrelated controls; do not claim surrounding spacing enlarges its hitbox. Provide pressed feedback such as the existing short `active:scale-*` treatment where nearby controls use it.
|
||||
- Give phone and coarse-pointer primary actions, standalone icon buttons, and menu rows an actual hitbox of at least 44px. Apply these dimensions only within the touch condition. Ordinary desktop controls use 28px. See [Control sizing](control-sizing.md) for compact controls, shared components, and geometry checks. Existing compact chrome such as `MobilePillButton` remains a documented exception. Surrounding spacing does not enlarge a hitbox. Preserve nearby pressed feedback.
|
||||
- Keep labels and current context visible. Use truncation for long names, tabular number badges for counts, and accessible names that include enough hierarchy to disambiguate the action.
|
||||
- Use `100dvh`/`h-dvh`, not `100vh`, for viewport-filling phone layouts. Account for `env(safe-area-inset-*)` on fixed controls and drawer content.
|
||||
- In flex surfaces, the usual scroll body is `min-h-0 flex-1 overflow-y-auto overscroll-contain`. Keep headers/actions fixed and avoid nested vertical scrollers unless the interaction truly needs them.
|
||||
@@ -90,6 +90,7 @@ Use the configured `mobile-chrome` Pixel 5 project and assert user outcomes. Add
|
||||
- primary action is visible and completes the same outcome as desktop
|
||||
- drawer/menu remains inside the viewport and long content scrolls internally
|
||||
- new primary actions, standalone icon controls, and menu rows meet the 44px hitbox expectation
|
||||
- controls revealed by responsive position overrides stay inside their surface: `controlBox.x >= surfaceBox.x` and `controlBox.x + controlBox.width <= surfaceBox.x + surfaceBox.width`, with the equivalent vertical checks when relevant
|
||||
- nested choices remain usable without horizontal overflow
|
||||
- document `scrollWidth` does not exceed `clientWidth`
|
||||
- fixed controls and final rows clear the bottom safe area
|
||||
|
||||
@@ -24,6 +24,12 @@ Read these sources in full:
|
||||
Use the legacy feature specification during migration only when no replacement
|
||||
requirement or design exists.
|
||||
|
||||
Read each spec's `Implementation Plans` and related-plan links, then inventory
|
||||
all existing companion plan directories. Treat linked packages as one
|
||||
synchronized implementation record: reconcile every affected task scope, E2E
|
||||
scenario matrix, status, and exact final verification results/counts before
|
||||
completion.
|
||||
|
||||
## Outputs
|
||||
|
||||
Create:
|
||||
@@ -39,6 +45,10 @@ docs/plans/<initiative>/task-<NN>-<short-slug>.md
|
||||
|
||||
### 1. Map the change
|
||||
|
||||
Run the `/interview-me` assumption check, reusing settled specification choices.
|
||||
Resolve material blockers before decomposing the affected work. For large
|
||||
uncertain initiatives, use that skill's decision-mapping reference first.
|
||||
|
||||
Identify:
|
||||
|
||||
- The required outcomes and acceptance criteria.
|
||||
@@ -47,8 +57,9 @@ Identify:
|
||||
- Existing tests and end-to-end patterns.
|
||||
- The dependency order for implementation.
|
||||
|
||||
Check `docs/decisions/INDEX.md` for relevant decisions. Stop when the
|
||||
requirements, system design, ADRs, and code disagree on a material boundary.
|
||||
Use `python3 scripts/list-docs.py decisions --text <capability-term> --format paths`
|
||||
to locate relevant decisions. Stop when the requirements, system design, ADRs,
|
||||
and code disagree on a material boundary.
|
||||
|
||||
### 2. Write `plan.md`
|
||||
|
||||
@@ -86,6 +97,13 @@ State the result, the implementation order, and the reason for that order.
|
||||
Name exact files, symbols, schema changes, contracts, and integration points.
|
||||
Organize this section by implementation boundary or vertical slice.
|
||||
|
||||
## ASCII UI preview
|
||||
|
||||
Required when rendered UI changes. Follow the preview contract in
|
||||
`docs/specs/guide/plans-and-work-orders.md`: show labelled views, relevant
|
||||
states, desktop/phone composition, and structural requirements. Omit for
|
||||
packages without UI changes.
|
||||
|
||||
## Tests
|
||||
|
||||
Map every relevant acceptance criterion to its unit or integration evidence.
|
||||
@@ -153,6 +171,12 @@ State the implementation outcome in two or three sentences.
|
||||
|
||||
- Give one to three concrete implementation conditions.
|
||||
|
||||
## ASCII UI preview
|
||||
|
||||
For a UI work order, include its relevant view or excerpt from the plan with
|
||||
the same view label, a link to the full preview, and applicable AC references.
|
||||
Omit for work orders without rendered UI changes.
|
||||
|
||||
## Verification
|
||||
|
||||
```bash
|
||||
@@ -185,8 +209,16 @@ Name prior work orders or write `None`.
|
||||
Pending.
|
||||
````
|
||||
|
||||
Each work order must fit one focused implementation pass. Split work that has
|
||||
unrelated outcomes, independent verification, or a title that contains “and.”
|
||||
Each work order must deliver one independently verifiable outcome in one focused
|
||||
implementation pass. Prefer a narrow end-to-end slice. Split by layer only for
|
||||
a real dependency or verification boundary, not because files occupy different
|
||||
directories. Title wording does not determine task boundaries.
|
||||
|
||||
For broad migrations that cannot proceed in vertical slices, sequence compatible
|
||||
expansion, bounded caller migrations, and final removal of the old contract.
|
||||
If a batch cannot pass independently, keep it with its required integration in
|
||||
one work order. Add only dependencies that actually block the outcome. Preserve
|
||||
the exact acceptance IDs, likely files, and verification commands in each work order.
|
||||
|
||||
Use `parallel-safe` only when files are disjoint and the tasks share no schema,
|
||||
migration, generated contract, lockfile, or package configuration. A wave does
|
||||
@@ -198,9 +230,29 @@ Every work order needs exact commands. Use repository `make` targets when they
|
||||
exist. Frontend work in a fresh worktree includes the workspace dependency
|
||||
installation before the first package command.
|
||||
|
||||
Write each verification block so that a user can run the complete block in one
|
||||
shell. If commands change directories, use one directory change or isolated
|
||||
subshells.
|
||||
|
||||
When implementation is complete, replace `Pending` with every required command
|
||||
and its result. Include specification and diff gates that run outside the
|
||||
product test commands.
|
||||
|
||||
User-facing behavior needs end-to-end evidence somewhere in the work package.
|
||||
A low-level work order does not need an artificial browser test.
|
||||
|
||||
After implementation and targeted checks pass, run every listed verification
|
||||
command from its documented working directory. In a multi-command block, root
|
||||
each command independently with `(cd <dir> && ...)` or an explicit root reset;
|
||||
never rely on a preceding `cd`. Confirm each referenced path exists and stays
|
||||
inside the tool or package scope, and that every changed test suite is covered
|
||||
before marking Results complete. Record actual results, not planned or stale counts.
|
||||
|
||||
Before marking a new plan package complete, run `git diff --check --
|
||||
docs/plans/<initiative>` and `git status --short -- docs/plans/<initiative>`;
|
||||
the status check catches untracked work orders. Confirm every work order names
|
||||
existing `REQ-*`/`AC-*` IDs and an existing system-design path.
|
||||
|
||||
Do not add generic QA, review, simplify, security, or full-verification tasks.
|
||||
Task checks provide pre-PR evidence. Configured PR reviewers provide semantic
|
||||
review after the PR opens.
|
||||
@@ -208,7 +260,10 @@ review after the PR opens.
|
||||
### 5. End the design turn
|
||||
|
||||
Report the requirement IDs, system designs, plan, work orders, dependency
|
||||
order, exact checks, and open risks. Then end the turn.
|
||||
order, exact checks, and open risks. For UI changes, also render a compact
|
||||
ASCII preview inline in the final conversation summary; links alone are not
|
||||
enough. Follow the shared preview contract, including phone composition when
|
||||
it differs. Then end the turn.
|
||||
|
||||
Do not ask for plan approval or a model switch. The user reviews the artifacts
|
||||
and sends a later explicit implementation request.
|
||||
|
||||
@@ -74,6 +74,55 @@ playwright-cli tracing-stop
|
||||
# Open trace to see DOM state when click was attempted
|
||||
```
|
||||
|
||||
### Raw CDP performance traces
|
||||
|
||||
For performance measurements that need raw Chrome trace events, use a CDP
|
||||
session and `transferMode: ReturnAsStream`. `Tracing.tracingComplete` provides
|
||||
the stream handle after `Tracing.end`; decode each `IO.read` chunk as base64
|
||||
only when its `base64Encoded` flag is true, then close the stream. Guard the
|
||||
entire operation so tracing and the CDP session are cleaned up even when the
|
||||
measurement fails. Do not disable page script execution during the interaction
|
||||
because that changes the work being measured:
|
||||
|
||||
```js
|
||||
const client = await page.context().newCDPSession(page);
|
||||
let tracingStarted = false;
|
||||
let stream;
|
||||
try {
|
||||
const complete = new Promise(resolve =>
|
||||
client.once('Tracing.tracingComplete', resolve),
|
||||
);
|
||||
await client.send('Tracing.start', {
|
||||
transferMode: 'ReturnAsStream',
|
||||
categories: 'devtools.timeline,v8.execute',
|
||||
});
|
||||
tracingStarted = true;
|
||||
// Run the measured interaction here.
|
||||
await client.send('Tracing.end');
|
||||
({ stream } = await complete);
|
||||
for (;;) {
|
||||
const chunk = await client.send('IO.read', { handle: stream });
|
||||
const bytes = chunk.base64Encoded
|
||||
? Buffer.from(chunk.data, 'base64')
|
||||
: Buffer.from(chunk.data);
|
||||
processTraceChunk(bytes);
|
||||
if (chunk.eof) break;
|
||||
}
|
||||
} finally {
|
||||
if (tracingStarted) {
|
||||
try { await client.send('Tracing.end'); } catch {}
|
||||
}
|
||||
if (stream) {
|
||||
try { await client.send('IO.close', { handle: stream }); } catch {}
|
||||
}
|
||||
try { await client.detach(); } catch {}
|
||||
}
|
||||
```
|
||||
|
||||
In production code, use a bounded wait for `Tracing.tracingComplete` and keep
|
||||
the cleanup guards when the timeout fires. Do not parse every chunk as base64,
|
||||
and do not leave the CDP session attached after the trace.
|
||||
|
||||
### Analyzing Performance
|
||||
|
||||
```bash
|
||||
|
||||
+119
-108
@@ -32,91 +32,54 @@ Create a visible checklist:
|
||||
|
||||
## 1. Gather PR State
|
||||
|
||||
Before the first GitHub helper call, request any runtime network approval that
|
||||
the environment requires. If access is denied, cancelled, or interrupted, stop
|
||||
the workflow permanently; retry only transient fetch failures after access is
|
||||
approved.
|
||||
Before the first GitHub call, obtain any network approval required by the
|
||||
runtime. If the runtime denies access, stop until the user authorizes access.
|
||||
|
||||
Run `scripts/pr-state --summary <PR>` once. Record the current-head check,
|
||||
review, and PR-delivery fields described in
|
||||
`references/review-evidence.md`. For a cross-repository PR, use only the
|
||||
reported delivery fields as the push target; do not infer it from a local
|
||||
`fork` or `contributor` remote. Load that reference for exact-head review
|
||||
classification, authentication fallbacks, and hidden-thread handling. Inspect
|
||||
mergeability separately through `references/merge-conflicts.md`.
|
||||
Run `scripts/pr-state --summary <PR>` and `scripts/pr-resolve list <PR>`.
|
||||
Load [review-evidence.md](references/review-evidence.md) for snapshot fields,
|
||||
review classification, hidden threads, and access fallbacks.
|
||||
For cross-repository PRs, use the snapshot's delivery fields as the push target.
|
||||
|
||||
If the fresh mergeability query reports `mergeable=CONFLICTING` or
|
||||
`mergeStateStatus=DIRTY`, stop CI and review triage. This is an actionable PR
|
||||
blocker, not a clean or report-only terminal state. Load
|
||||
`references/merge-conflicts.md`. If the user has explicitly authorized PR
|
||||
fixup or conflict resolution, resolve the conflict now (prefer a merge of the
|
||||
fresh base unless the user requests a rebase), verify the result, push it, and
|
||||
restart this section with a new PR-state snapshot. If authorization is absent,
|
||||
report the conflict and ask for it before mutating the branch. Do not triage
|
||||
comments or checks against a conflicted head.
|
||||
Then run `gh pr view <PR> --json state,baseRefName,headRefOid,mergeable,mergeStateStatus,reviewDecision`.
|
||||
Require matching head SHAs before triage. Verify the base SHA against the
|
||||
current base-ref tip. Refresh mismatched metadata before attributing failures.
|
||||
If the PR is merged or closed, stop. For a merge, report
|
||||
`mergedAt` and `mergeCommit.oid`.
|
||||
|
||||
For pending CI, wait with `scripts/pr-await <PR>`. It blocks below the
|
||||
conversation and prints one report when every check is terminal, so a wait of
|
||||
any length costs a single round-trip. Do not re-run `scripts/pr-state
|
||||
--summary` on a timer instead: each snapshot is a separate model turn that
|
||||
re-reads the whole context, and waiting is only about 6% of what such a turn
|
||||
actually does.
|
||||
Treat peer alerts as advisory until their SHA matches the current head.
|
||||
If captured output is empty, truncated, or invalid, rerun through `rtk proxy`.
|
||||
Capture stdout and stderr separately and preserve the exit code.
|
||||
An empty capture cannot prove that the PR is clean.
|
||||
|
||||
Default `--mode all-terminal` is the cost-correct choice and returns only when
|
||||
no check is pending, so every failure arrives in one report and is fixed in one
|
||||
pass. It is also the correct choice for a reason unrelated to cost: a failed job
|
||||
can be visible while its parent workflow is still running, so an early return
|
||||
reports a partial failure set. Use `--mode first-failure` only when the user
|
||||
wants the first failure interactively; it costs an extra full CI cycle per
|
||||
failure. Pass `--deadline-min` for a user-stated limit; on exit 2 report the
|
||||
named pending checks as "CI in progress."
|
||||
If GitHub reports a conflict, load
|
||||
[merge-conflicts.md](references/merge-conflicts.md) before CI or review triage.
|
||||
An explicit fixup request authorizes conflict resolution within that request.
|
||||
Otherwise, report the conflict without changing the branch.
|
||||
|
||||
Read its exit code rather than re-deriving state: 0 clean, 1 terminal with
|
||||
findings, 2 deadline with checks still pending, 3 blocked. Exit 3 covers every
|
||||
case where a clean answer cannot be trusted: the PR merged or closed, a workflow
|
||||
needs approval, access was lost, the merge-state query failed, the snapshot
|
||||
reported `errors` or a null unresolved-thread count, or the host toolchain
|
||||
cannot run `pr-state` correctly. Never downgrade an exit 3 to "probably clean."
|
||||
If the base advanced without conflicts, use that reference's advanced-base
|
||||
procedure. Base advancement alone does not require a branch update.
|
||||
Record the head, base, synthetic merge, and focused test results.
|
||||
If either input SHA changes, repeat the relevant validation.
|
||||
Keep the helper's exit code unchanged and report any separately verified base drift.
|
||||
|
||||
Exit 1 counts blocking reviews, not only threads: a current-head
|
||||
`CHANGES_REQUESTED` review can exist with no unresolved thread attached.
|
||||
For related or stacked PRs, use the semantic-conflict procedure in the same
|
||||
reference. Reconcile changed contracts and stable IDs before delivery.
|
||||
|
||||
Every report records the jq and bash versions it ran under. That is provenance,
|
||||
not a gate: `pr-state` used to fail silently on jq 1.6 and on the bash 3.2 that
|
||||
macOS ships as `/bin/bash`, both of which made a dirty PR look clean, and both
|
||||
are fixed at the source. What guards against a degraded `pr-state` now is the
|
||||
snapshot itself, which is stronger than any version check: a summary reporting
|
||||
`errors`, one whose unresolved-thread count is null, and one that does not parse
|
||||
are all treated as unknown rather than clean. A blocked report on an old
|
||||
toolchain adds a note naming it as a possible cause. A push during the wait
|
||||
restarts the gate against the new head and is reported.
|
||||
For pending checks, load [waiting.md](references/waiting.md).
|
||||
Use `scripts/pr-await` and its final exit code.
|
||||
For queue membership or enqueue/dequeue requests, load
|
||||
[merge-queue.md](references/merge-queue.md).
|
||||
For queue removals or failed synthetic checks, also load
|
||||
[ci-troubleshooting.md](references/ci-troubleshooting.md).
|
||||
Ordinary PR-head checks do not validate a merge-group commit.
|
||||
|
||||
Do not use interactive `gh pr checks --watch` in the primary conversation: its
|
||||
TTY redraws make captured output unusable. Use the read-only `pr-poller` only
|
||||
when the user explicitly asked to wait or monitor and `pr-await` is unavailable.
|
||||
Treat a poller's unresolved/pending snapshot as provisional: it can predate a
|
||||
primary-session push or thread resolution. Re-run `scripts/pr-state --summary
|
||||
<PR>` at the current head before acting on it or declaring completion.
|
||||
If a job remains pending beyond the workflow's configured timeout, or its status
|
||||
conflicts with the GitHub UI/API, query the exact job with
|
||||
`gh api repos/<owner>/<repo>/actions/jobs/<job_id>` (or inspect the run with
|
||||
`gh run view <run_id>`) before calling CI hung or changing code. Treat the direct
|
||||
result as current-head evidence only after its `head_sha` matches
|
||||
`checks_head_sha`; otherwise report the result as stale or unknown.
|
||||
Directly supplied review findings need current-source validation even without
|
||||
a GitHub thread. Apply valid fixes within the user's authorization.
|
||||
Do not invent comment IDs or resolutions for findings without a thread.
|
||||
External replies and thread resolution still require user authorization.
|
||||
|
||||
For a cross-repository PR whose current-head snapshot is unexpectedly sparse,
|
||||
inspect `approval_required_runs`. A current-head workflow with
|
||||
`conclusion=action_required` is blocked verification, not green or skipped CI.
|
||||
Only after the user authorizes PR fixup, approve the exact run with
|
||||
`gh api --method POST repos/<base-owner>/<base-repo>/actions/runs/<run-id>/approve`,
|
||||
then re-run the summary and require jobs to materialize before polling. `gh run
|
||||
approve` is not a valid command.
|
||||
|
||||
Treat the state as clean only when the current head has no failed or pending
|
||||
checks, no merge conflict, no blocking review (an active `CHANGES_REQUESTED`
|
||||
or a review blocked at the exact current head), no unresolved review thread, no
|
||||
actionable issue comment, and qualifying exact-head semantic evidence where PR
|
||||
delivery requires it.
|
||||
If `reviewDecision=REVIEW_REQUIRED` and `mergeStateStatus=BLOCKED`, report the
|
||||
human approval gate. Green checks do not authorize self-approval or merging.
|
||||
|
||||
## 2. Fix CI Failures
|
||||
|
||||
@@ -142,9 +105,16 @@ when `origin` points to a fork. Inspect the parent workflow/run to determine
|
||||
whether a newer base commit caused the failure before changing product or docs
|
||||
code. If the fix is already upstream, update or rebase the branch only when
|
||||
authorized, rerun affected checks, and invalidate all prior exact-head evidence.
|
||||
If the installed `gh pr view --json` does not expose the base OID, use
|
||||
`BASE_SHA=$(gh api repos/<owner>/<repo>/pulls/<PR> --jq .base.sha)` alongside
|
||||
`gh pr view <PR> --json baseRefName,headRefOid` for the ref names. Run this
|
||||
fallback under `set -euo pipefail`, require a non-empty `BASE_SHA`, verify the
|
||||
fetched base tip separately, and only then run
|
||||
`git merge-tree --write-tree <base-remote>/<base-ref> HEAD`.
|
||||
|
||||
For unfamiliar, infrastructure, or E2E failures, load
|
||||
`references/ci-troubleshooting.md` before changing code.
|
||||
`references/ci-troubleshooting.md` and, for transport or queue evidence,
|
||||
`references/transport-troubleshooting.md` before changing code.
|
||||
Also load it for unexpected zero-duration or no-op manual-review runs: event
|
||||
and workflow provenance can explain them without a product-code change.
|
||||
|
||||
@@ -168,26 +138,31 @@ assertion with the current implementation and concurrent or sibling PRs before
|
||||
editing. If it is a stale test expectation, the smallest valid remediation may
|
||||
be a test-only assertion update: keep it limited to the reported failure, run
|
||||
the focused test, and call out that scope. Do not change unrelated production
|
||||
behavior or duplicate a larger sibling change.
|
||||
behavior or duplicate a larger sibling change; for request-count/dedup assertions covering multiple hydration or effect continuations, use a deferred response, assert the count while it remains pending, then resolve and drain it before unmount/return so an immediately resolved mock cannot let cleanup timers create a false duplicate.
|
||||
When a remediation changes a documented behavior or contract, update the
|
||||
authoritative spec/guidance, plan, and task file when present; commit docs and
|
||||
code together, keep regression tests and verification commands aligned, and
|
||||
re-check the documentation before completion. Record why no update is needed
|
||||
when the behavior remains internal.
|
||||
authoritative spec/guidance, plan, and task file when present; refresh each
|
||||
Verification/Results section with exact post-fixup commands and outcomes; commit
|
||||
docs and code together, keep regression tests and verification commands aligned,
|
||||
and re-check the documentation before completion; record why no update is needed when the behavior remains internal.
|
||||
|
||||
## 3. Triage And Address Reviews
|
||||
|
||||
Use `scripts/pr-resolve list <PR>` to obtain unresolved threads. Its previews
|
||||
can be truncated, so expand every listed review thread with
|
||||
`scripts/pr-resolve show <PR> <thread_id>` before deciding how to handle it.
|
||||
Use `scripts/pr-state --comment <comment_id>` only for a flat comment view when
|
||||
a thread context is not available. Validate against the current head, the spec,
|
||||
and existing architecture before editing or replying.
|
||||
Use `scripts/pr-resolve list <PR>` to obtain unresolved threads. Before handling multiple
|
||||
threads, make a thread-to-finding map and one body file per thread. Its previews can be
|
||||
truncated, so run `scripts/pr-resolve show <PR> <thread_id>` immediately before each
|
||||
write; verify comment/thread IDs and that the body names that thread's finding, file, and
|
||||
commit. Use `scripts/pr-state --comment <comment_id>` only for a flat comment view when no
|
||||
thread context is available. Validate against the current head, spec, and architecture before editing or replying.
|
||||
|
||||
Every unresolved thread requires an explicit disposition, regardless of its
|
||||
author, bot identity, visibility, or apparent severity. Record exactly one of
|
||||
these dispositions:
|
||||
|
||||
When an automated review identifies a security-boundary risk and the user
|
||||
explicitly chooses the behavior, record the accepted risk, trust/revocation
|
||||
responsibility, and targeted regression coverage in the final response, PR body,
|
||||
and authoritative spec. Do not silently encode the compromise.
|
||||
|
||||
- `actionable`: make the valid code or test change, verify it, cite the
|
||||
resulting commit in the reply, and resolve the thread when writes are
|
||||
authorized.
|
||||
@@ -246,11 +221,15 @@ summary's authoritative head repository and ref only when
|
||||
`pr.head_ref_oid` to equal local `HEAD`; an upstream remote comparison is not
|
||||
sufficient. Run broad `/verify` only if the user explicitly requests it or the
|
||||
PR/CI finding requires it.
|
||||
After every push, also run a fresh
|
||||
`gh pr view <PR> --json baseRefName,headRefOid,mergeable,mergeStateStatus` (or
|
||||
equivalent) at that pushed head. Require `headRefOid` to equal local `HEAD` and
|
||||
confirm `mergeable` is not `CONFLICTING` and `mergeStateStatus` is not `DIRTY`;
|
||||
`scripts/pr-state --summary` does not include mergeability.
|
||||
After every push, run fresh `gh pr view <PR> --json baseRefName,headRefOid,mergeable,mergeStateStatus`
|
||||
(or equivalent) at the pushed head; if GitHub briefly returns an older head, retry
|
||||
the query and exact-head `pr-state` with short bounded backoff before triaging or reporting.
|
||||
Require local `HEAD`, `headRefOid`, and `checks_head_sha` to match; confirm
|
||||
`mergeable` is not `CONFLICTING` and `mergeStateStatus` is not `DIRTY`; `scripts/pr-state --summary` does not include mergeability.
|
||||
|
||||
If a remediation changes rendered UI, invalidate screenshots captured before
|
||||
fixup and recapture and re-publish every affected viewport after the final
|
||||
commit. Never leave pre-fixup screenshots in the PR.
|
||||
|
||||
Immediately before a remediation commit or push—and again after long-running
|
||||
remediation—refresh PR state. Require the PR to remain open and its head ref to
|
||||
@@ -260,10 +239,10 @@ local upstream tip; after the push, require the PR head OID to equal local
|
||||
stale push: preserve the local fix and ask before creating a clean follow-up.
|
||||
|
||||
After any rebase or force-push, fetch the PR base and compare local `HEAD`, the upstream tip, and `pr.head_ref_oid`; rerun affected checks.
|
||||
A rebase onto a newer base invalidates prior evidence: rerun the exact failed
|
||||
command and any package-level gate required by whole-file rules (for example
|
||||
max-lines). Then rerun `scripts/pr-resolve list <PR>` and `scripts/pr-state --summary <PR>`; distinguish stale/current
|
||||
failures and report pending checks separately. Use `--force-with-lease`, never an unconditional force-push.
|
||||
A merge-commit head requires `--rebase-merges` or a verified merge-only delta;
|
||||
after long hooks/tests, compare the latest authoritative base with the rebase
|
||||
base and reconcile if changed; otherwise a rebase invalidates prior evidence:
|
||||
rerun affected checks, `scripts/pr-resolve list <PR>`, and `scripts/pr-state --summary <PR>`; use `--force-with-lease`, never an unconditional force-push.
|
||||
If the rebase or conflict resolution touched `AGENTS.md`, `CLAUDE.md`, or a
|
||||
skill/reference file, run the shared harness validation in
|
||||
`.agents/skills/harness-improvement/references/validation.md` before pushing.
|
||||
@@ -276,10 +255,18 @@ After every push, re-fetch current-head state and run
|
||||
consolidation, verify the target head before commenting/closing the superseded
|
||||
PR; preserve its branch unless deletion is requested. Automated reviewers may
|
||||
resolve or replace threads; do not reply to or resolve a thread that fresh state
|
||||
reports as resolved. Before replying to a pre-push thread, run
|
||||
`scripts/pr-resolve show <PR> <thread_id>`; if it reports `resolved: true` (often
|
||||
with an `Addressed in commit ...` marker), record the thread as auto-resolved and
|
||||
do not post a duplicate reply. Continue replying/resolving only for `resolved: false` threads, including hidden unresolved threads.
|
||||
reports as resolved unless an authorized later code/policy change invalidates
|
||||
the rationale for that resolution. In that exception, re-fetch/show the thread,
|
||||
explicitly reopen it with `scripts/pr-resolve reopen <PR> <thread_id>`, then use
|
||||
the reply helper to reply/resolve/react and verify the final thread. Before an
|
||||
ordinary reply to a pre-push thread, run `scripts/pr-resolve show <PR>
|
||||
<thread_id>`; if it reports `resolved: true` (often with an `Addressed in commit
|
||||
...` marker), record the thread as auto-resolved and do not post a duplicate
|
||||
reply. Continue replying/resolving only for `resolved: false` threads, including
|
||||
hidden unresolved threads.
|
||||
If a finding cites an obsolete commit, inspect the cited lines at the current PR
|
||||
head before editing. Apply only the missing portion, and do not duplicate an
|
||||
assertion that is already present or reply to a resolved or stale thread.
|
||||
Treat each fresh summary as a new review-evidence snapshot: inspect every
|
||||
non-empty body in `review_evidence.exact_current_head_reviews[]`, even when
|
||||
`unresolved_review_thread_count=0` and `scripts/pr-resolve list` is empty.
|
||||
@@ -303,12 +290,22 @@ changes. Declare the PR clean only when the
|
||||
exact-current-head review classification reports no unaddressed findings,
|
||||
`checks_snapshot_complete=true`, `failed_checks=[]`, `pending_checks=[]`,
|
||||
`approval_required_runs=[]`, `actionable_issue_comment_count=0`,
|
||||
`unresolved_review_thread_count=0`, `hidden_unresolved_threads=[]`, there is no
|
||||
merge conflict, and
|
||||
`scripts/pr-resolve list <PR>` is empty. Within
|
||||
`unresolved_review_thread_count=0`, `hidden_unresolved_threads=[]`,
|
||||
there is no merge conflict, and `scripts/pr-resolve list <PR>` is empty.
|
||||
Require either `base_advanced_since_head=false` or recorded merge-result
|
||||
validation for the current head and current base, as defined in
|
||||
`references/merge-conflicts.md`. An unknown base relationship is not sufficient.
|
||||
Within
|
||||
the user's monitoring limit, continue checking after resolutions until automated
|
||||
review jobs are terminal; otherwise report the exact pending check names.
|
||||
|
||||
When remediation changes tests or validation, reconcile any validation commands
|
||||
or counts claimed in the live PR description with the final verification before
|
||||
declaring fixup complete. Reuse `/pr`'s live-body preservation and REST-fallback
|
||||
procedure, preserve intervening bot or maintainer text, and re-fetch exact-head
|
||||
state afterward; a body PATCH triggers workflows, so restart `pr-state`/`pr-await`
|
||||
before treating pre-PATCH CI as current.
|
||||
|
||||
If the user explicitly requested a persistent Kandev plan update and the task
|
||||
has an external Kandev plan, call `get_task_plan_kandev` before fixup and
|
||||
`update_task_plan_kandev` after fixup with the remediation commit, final
|
||||
@@ -319,10 +316,10 @@ commit. Record the prior head's fixup evidence before a plan commit/push: that
|
||||
push restarts CI and invalidates the snapshot. For tracked `docs/plans/**`
|
||||
artifacts, keep prose head-agnostic and record remediation scope/local
|
||||
verification before that commit; then rerun `scripts/pr-state --summary` and
|
||||
`scripts/pr-resolve list` for the new head and report its pending checks
|
||||
separately. Mark prior current-head claims historical/superseded when a new
|
||||
head replaces them; report only the latest head's SHA, CI/review counts, and
|
||||
mergeability. Do not leave planned verification marked unstarted after it has run.
|
||||
`scripts/pr-resolve list` for the new head and report pending checks separately; keep
|
||||
exact-head verification/queue work externally pending, not complete, until pending
|
||||
checks are empty. Mark prior current-head claims historical/superseded when a new
|
||||
head replaces them; report only the latest head's SHA, CI/review counts, and mergeability; do not leave planned verification marked unstarted after it has run.
|
||||
|
||||
Before declaring fixup complete, verify `git status --short` is clean,
|
||||
`git rev-parse HEAD` equals `git rev-parse @{upstream}`, the PR head equals
|
||||
@@ -346,6 +343,20 @@ and fail when another worktree owns it, even after the remote merge succeeds.
|
||||
Report the remote merge separately. Delete a remote or local branch only when
|
||||
requested and through a worktree-safe cleanup flow.
|
||||
|
||||
When branch protection requires a merge queue, after the exact current head is
|
||||
clean and all checks pass, run `gh pr merge <PR> --auto
|
||||
--match-head-commit <SHA>` without a merge strategy. In this mode the command
|
||||
requests queue admission, not a direct merge. Verify the
|
||||
`added_to_merge_queue` timeline event and
|
||||
`pullRequest.mergeQueueEntry { id state position estimatedTimeToMerge headCommit { oid } }` with GraphQL; treat "already queued" as verification, not a retry.
|
||||
Compare the expected SHA with `pullRequest.headRefOid`, not the queue commit; keep the PR open while queue checks run. Do not rely on a null `autoMergeRequest` or
|
||||
`mergeStateStatus=UNKNOWN` to determine queue status.
|
||||
|
||||
For "monitor until merge", poll queue/PR state with bounded cadence; require
|
||||
`MERGED`, `mergedAt`, `mergeCommit.oid`, and `isInMergeQueue=false`.
|
||||
`QUEUED`/`AWAITING_CHECKS` and queued `CLEAN` remain pending; inspect
|
||||
`references/merge-queue.md` for synthetic checks before reporting a blocker.
|
||||
|
||||
## Guardrails
|
||||
|
||||
- Do not create Kandev subtasks unless the user explicitly asks for task
|
||||
|
||||
@@ -31,9 +31,93 @@ gh api repos/<owner>/<repo>/actions/jobs/<job_id> \
|
||||
scripts/pr-state --job-log <job_id>
|
||||
```
|
||||
|
||||
Treat an unavailable log stream as unknown evidence, not a product failure.
|
||||
If the direct job-log request returns 404 while the job is queued or in progress,
|
||||
wait for that job to complete before retrying; it is not missing evidence.
|
||||
Do not wait for the whole workflow only to obtain a log for a terminal job.
|
||||
Inspect the job endpoint and fetch its job log directly when the aggregate log
|
||||
command says logs are unavailable.
|
||||
|
||||
**Merge queue removals:** If a PR is removed from the merge queue, or its
|
||||
ordinary checks are green while the queue reports a failure, inspect the issue
|
||||
timeline for `added_to_merge_queue` and `removed_from_merge_queue` events. Then
|
||||
query the queue runs directly; ordinary branch runs and `gh pr checks` omit
|
||||
them:
|
||||
|
||||
```bash
|
||||
gh api graphql \
|
||||
-f query='query($owner:String!,$repo:String!,$number:Int!) {
|
||||
repository(owner:$owner,name:$repo) {
|
||||
pullRequest(number:$number) {
|
||||
timelineItems(first:100, itemTypes:[ADDED_TO_MERGE_QUEUE_EVENT,REMOVED_FROM_MERGE_QUEUE_EVENT]) {
|
||||
nodes {
|
||||
__typename
|
||||
... on AddedToMergeQueueEvent { createdAt beforeCommit { oid } }
|
||||
... on RemovedFromMergeQueueEvent { createdAt reason beforeCommit { oid } }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}' -f owner=<owner> -f repo=<repo> -F number=<PR> \
|
||||
--jq '.data.repository.pullRequest.timelineItems.nodes[] | {type:.__typename, createdAt, reason, before_commit:(.beforeCommit.oid // null)}'
|
||||
gh run list --event merge_group
|
||||
gh run list --commit <synthetic-queue-sha>
|
||||
gh run view <run-id> --json jobs
|
||||
```
|
||||
|
||||
Record each event's `createdAt`, removal `reason`, and `beforeCommit.oid`. Map
|
||||
that OID to the corresponding `merge_group` run through commit ancestry,
|
||||
identify the aggregate gate's failed step, and compare cancelled-job timestamps
|
||||
with the workflow's `timeout-minutes`. Distinguish timeout/cancellation from a
|
||||
test assertion before changing product code. A green PR-head snapshot does not
|
||||
prove the merge-group gate passed.
|
||||
|
||||
**Synthetic `merge_group` failure:** When a merge-group run fails, record its
|
||||
event, head SHA, leaf job or shard, and failed assertion. Compare the synthetic
|
||||
tree and reported files with the PR head and authoritative base, then reproduce
|
||||
the exact assertion on the PR head and on the parent or last-passing SHA with
|
||||
retries disabled and repeated runs. Classify it as queue, base, or fixture
|
||||
infrastructure only after independent evidence supports that conclusion; do not
|
||||
make speculative source changes or push a stale head. Verify
|
||||
`checks_head_sha` matches the PR head before acting, and stop if the PR merged
|
||||
or closed.
|
||||
|
||||
**Isolated frontend assertion in an unchanged test:** Reproduce the exact
|
||||
focused test locally. If it passes and has no relationship to the PR diff,
|
||||
rerun only the failed workflow jobs once; classify it as transient only when
|
||||
that rerun passes. A repeatable assertion remains a fixup failure even when the
|
||||
test file is unchanged.
|
||||
|
||||
If a queued PR needs an authorized branch update, do not push blindly into the
|
||||
queue. Record the PR head/base OIDs and queue event IDs, confirm its queue state,
|
||||
dequeue it through the repository-approved path before changing the branch, then
|
||||
verify the exact head and re-enqueue only after the push. Treat the new
|
||||
`merge_group` SHA and run as fresh evidence; a base race or queue-state change
|
||||
invalidates prior head checks.
|
||||
|
||||
Treat unavailable/transport-only logs as unknown, not product failure. For a
|
||||
queued 404 wait for terminal state; then download `test-results-<shard>` and inspect
|
||||
`error-context.md`, traces, and screenshots before rerunning or editing.
|
||||
|
||||
**Malformed or truncated GitHub API responses:** If a workflow fails while
|
||||
parsing `gh api` JSON, reports an unexpected end of input, or receives a
|
||||
successful response with missing required fields, preserve the raw response and
|
||||
HTTP status in a temporary file. Treat the result as unknown rather than as an
|
||||
empty or clean payload. Inspect the workflow's expected response with a focused
|
||||
contract test. For workflow-owned idempotent GET/PATCH requests, use bounded
|
||||
short-backoff retries, validate that the GET body is complete JSON before
|
||||
parsing, and silence PATCH output when the next step expects no response body.
|
||||
Rerun the failed job once, and stop with the transport failure if the same
|
||||
response problem repeats; do not add unbounded retries or change product code to
|
||||
accommodate a truncated response.
|
||||
|
||||
### Static artifact byte mismatches
|
||||
|
||||
If an object upload and public GET both succeed but a strict `cmp` fails, save
|
||||
and compare the origin artifact with the public body before changing the
|
||||
publisher. A public HTML body containing `__cf_email__`, `/cdn-cgi/`, or
|
||||
`email-decode.min.js` indicates Cloudflare edge transformation rather than a
|
||||
failed upload. Preserve the byte-for-byte comparison and fix origin metadata
|
||||
with `Cache-Control: no-transform`, or disable Email Obfuscation for the
|
||||
dedicated hostname; verify a fresh upload and public body match exactly.
|
||||
|
||||
When the aggregate command exposes only a merge/report job, obtain every failed
|
||||
shard `job_id` from `scripts/pr-state --summary <PR>` and inspect at least one
|
||||
failure from each shard before changing code:
|
||||
@@ -91,8 +175,13 @@ an actionable configuration error.
|
||||
**Merge-ref validation drift:** GitHub can run a PR check against the synthetic
|
||||
merge ref, where a current-base deletion makes a cited path or coverage entry
|
||||
appear missing even though it exists on the PR head. Compare the failing path
|
||||
with `origin/<baseRefName>` before changing the PR. Update a genuinely stale
|
||||
manifest or coverage entry, but do not rebase solely to satisfy this check.
|
||||
with `origin/<baseRefName>` before changing the PR. Whole-file limits can also
|
||||
fail only on the synthetic merge when incoming base content makes the merged
|
||||
file larger than the PR head. Compare the exact PR-head file with the
|
||||
`refs/pull/<PR>/merge` version (for example through the Contents API), then
|
||||
rerun the size/line check on a fresh merge result after any base merge. Update
|
||||
a genuinely stale manifest or coverage entry, but do not rebase solely to
|
||||
satisfy this check.
|
||||
|
||||
**Cancelled concurrency duplicates:** A required check with
|
||||
`conclusion=cancelled`, 0s job durations, unexpanded `${{ matrix.* }}` job
|
||||
@@ -105,15 +194,45 @@ non-cancelled run for the same head SHA passed, then trigger one clean run
|
||||
configured `timeout-minutes`, inspect its concrete steps and logs. When the
|
||||
race/test step has no assertion failure and one rerun repeats the same timeout,
|
||||
classify it as CI infrastructure, stop rerunning, and report the exact check,
|
||||
timeout, and pending-state evidence.
|
||||
timeout, and pending-state evidence. If every E2E matrix shard's test step
|
||||
remains `in_progress` past the configured timeout without assertion or log
|
||||
progress, inspect the exact jobs, classify the matrix stall as runner
|
||||
infrastructure, and cancel the run once. If GitHub rejects a rerun or leaves no
|
||||
clean run, create a fresh pull-request event with a harmless CI-only commit
|
||||
only when authorized; verify the new head and run ID, and do not loop reruns.
|
||||
|
||||
**Manual-review event provenance:** For an unexpected zero-duration or no-op
|
||||
manual bot-mention run, inspect the workflow at the repository default branch
|
||||
and the event payload before assuming the PR is behind `main`. Privileged
|
||||
`issue_comment` and `pull_request_target` runs use the trusted default-branch
|
||||
workflow definition, not a newly synced PR workflow. Compare the run's
|
||||
workflow path, event, and head metadata with that definition; land a workflow
|
||||
fix on the default branch, then reproduce with a fresh comment.
|
||||
Before cancelling or rerunning a stale pending report, re-fetch the PR head,
|
||||
parent run, and named job. Require each `head_sha` to match the current
|
||||
`checks_head_sha`; if the job is already terminal-success, do nothing. When a
|
||||
shard succeeds while its parent remains active, enumerate nonterminal jobs and
|
||||
distinguish report/final-gate cleanup from a hung shard. Because cancellation is
|
||||
run-scoped, cancel only after confirming the matching-head run is still stalled
|
||||
and no active job or gate would be collateral damage. Verify with fresh
|
||||
`scripts/pr-state --summary`, `scripts/pr-resolve list`, and mergeability.
|
||||
|
||||
**Rerun attempt semantics:** `gh run list` can retain a prior failed conclusion
|
||||
while a later `run_attempt` of the same workflow is still in progress. Query the
|
||||
REST run's `run_attempt`, `status`, `conclusion`, `head_sha`, and current jobs;
|
||||
classify only the current attempt after its parent workflow is terminal.
|
||||
|
||||
**Privileged workflow provenance and bootstrap:** For an unexpected no-op or
|
||||
old-behavior result in an `issue_comment` or `pull_request_target` workflow,
|
||||
including a PR-produced cross-job URL or SHA that is normalized or truncated,
|
||||
inspect the exact job log, event, workflow SHA, and base SHA. Compare the
|
||||
producer output with the consumer environment and with the workflow at the
|
||||
default branch, for example:
|
||||
|
||||
```bash
|
||||
git show <workflow-sha>:.github/workflows/<file>
|
||||
```
|
||||
|
||||
These privileged runs use the trusted default-branch workflow definition, so a
|
||||
fix committed only on the PR head cannot affect the current run. Validate the
|
||||
change with a focused contract test, rerun the failed job once, and if the log
|
||||
still proves default-branch provenance, report the base-controlled blocker and
|
||||
land the workflow fix through the authorized path before triggering a fresh
|
||||
event. Do not keep patching or rerunning the PR branch to exercise a workflow
|
||||
definition GitHub is not using.
|
||||
|
||||
**Semantic PR title transport failures:** If `pr-title` /
|
||||
`amannn/action-semantic-pull-request` fails with transport or response parsing
|
||||
@@ -142,8 +261,10 @@ Only debug code if the rerun fails with an actual lint, test, or build error.
|
||||
before tests run, check for dependency or registry failures. Patterns such as
|
||||
`packages.microsoft.com ... 403 Forbidden`, `docker/login-action@v3`,
|
||||
`Error response from daemon: Get "https://ghcr.io/v2/"`, `ghcr.io/token`,
|
||||
`context deadline exceeded`, or `Client.Timeout exceeded while awaiting
|
||||
headers` are infrastructure/package-registry issues, not app or test failures.
|
||||
`docker buildx imagetools inspect`, `Could not resolve an immutable digest`,
|
||||
`ghcr.io/kdlbs/kandev-ci:runtime-latest`, `context deadline exceeded`, or
|
||||
`Client.Timeout exceeded while awaiting headers` are infrastructure/package-
|
||||
registry issues, not app or test failures.
|
||||
If GitHub rejects `gh run rerun <run-id> --failed` while the workflow is still
|
||||
active, wait for the workflow/report job to finish and retry.
|
||||
|
||||
@@ -202,12 +323,32 @@ If any failing check is an E2E test:
|
||||
in-DOM React render errors that often show up in
|
||||
`e2e/test-results/<test>/error-context.md`.
|
||||
|
||||
If a failed E2E action leaves the browser on a detail route, inspect the
|
||||
rendered confirmation dialog and recent interaction-contract changes before
|
||||
changing router code or increasing route waits. A new intentional confirmation
|
||||
dialog is a navigation gate: update the test to assert and confirm the dialog
|
||||
instead of altering navigation to bypass it.
|
||||
|
||||
Before attributing an E2E failure to product code, compare the failed run SHA
|
||||
and `checks_head_sha` with local `HEAD`. PR workflows may test GitHub's
|
||||
synthetic merge commit, so reproduce the exact base-plus-head merge (or checked
|
||||
merge ref) with CI environment, retries disabled, and the same shard/order;
|
||||
verify the checkout SHA before interpreting the result.
|
||||
|
||||
If a focused reproduction reports `No tests found`, a title mismatch, or a 404
|
||||
for a workflow or API path, first verify the checked-out test title and compare
|
||||
the PR with the authoritative current base: record `base_head_oid`, the merge
|
||||
base, and `git diff <base>...HEAD`. An old branch can make an apparent product
|
||||
failure disappear because the fix and updated test already landed on the base.
|
||||
If the fix is already upstream, update the branch only with authorization and
|
||||
invalidate all prior exact-head CI evidence.
|
||||
|
||||
When a shard is cancelled and aggregate E2E/report gates fail, treat the
|
||||
aggregate failures as cascading until the concrete shard is inspected. Identify
|
||||
the cancelled job and timeout, run its exact manifest-selected shard without
|
||||
retry masking, then rerun the failed workflow jobs once. Classify it as
|
||||
infrastructure only if the timeout repeats without an assertion failure.
|
||||
|
||||
Useful focused commands:
|
||||
|
||||
```bash
|
||||
@@ -242,14 +383,18 @@ gh run view <run-id> --json status,conclusion,jobs \
|
||||
|
||||
For an explicit user requirement that an E2E run contain no flakes or retries,
|
||||
run the deterministic blob audit after the exact-head E2E merge report
|
||||
completes:
|
||||
completes. Download the matching artifacts first:
|
||||
|
||||
```bash
|
||||
python3 scripts/playwright-blob-audit <downloaded-blob-report-dir>
|
||||
gh run download <run-id> --pattern 'blob-report-*' --dir <tmp>
|
||||
python3 scripts/playwright-blob-audit <tmp>
|
||||
```
|
||||
|
||||
It recursively reads `report-*.zip` and `report.jsonl` artifacts, reports
|
||||
attempts, retry attempts, `onTestEnd` status counts, and results with errors,
|
||||
and exits nonzero for retries, errors, parse failures, or unexpected statuses.
|
||||
Do not make this extra artifact audit part of ordinary PR fixup; a normal green
|
||||
aggregate check is sufficient unless the no-flakes requirement is explicit.
|
||||
aggregate check is sufficient unless the no-flakes requirement is explicit. A
|
||||
nonzero audit keeps the PR from being called clean or flake-free. When a
|
||||
comparable baseline audit exists, report the PR-versus-baseline retry/error delta;
|
||||
shared baseline flakes are evidence, not permission to call the PR flake-free.
|
||||
|
||||
@@ -10,10 +10,23 @@ Inspect GitHub's mergeability state:
|
||||
gh pr view <PR> --json number,url,baseRefName,headRefName,mergeable,mergeStateStatus
|
||||
```
|
||||
|
||||
The `gh pr view --json` field is `baseRefName`, not `baseRefOid`; do not request
|
||||
the unsupported field. Resolve the base OID with `git ls-remote` or the base
|
||||
fields in `scripts/pr-state`. When exact OIDs are required, query `gh api repos/<owner>/<repo>/pulls/<PR> --jq '{base_sha:.base.sha,base_ref:.base.ref,head_sha:.head.sha,head_ref:.head.ref}'` and verify the fetched base tip matches `base_sha` before merging.
|
||||
|
||||
Treat `mergeable:"CONFLICTING"` or `mergeStateStatus:"DIRTY"` as an actionable merge-conflict blocker. Treat `mergeable:"UNKNOWN"` as inconclusive: wait one short cadence and query again before deciding. States such as `BEHIND`, `BLOCKED`, `UNSTABLE`, or `HAS_HOOKS` may require an update or more CI/review work, but they are not by themselves proof of file-level conflicts.
|
||||
|
||||
Always use the freshly queried `baseRefName`. GitHub can retarget a stacked child PR after its parent merges, so neither the Git upstream nor a base branch remembered from an earlier fixup round is authoritative.
|
||||
|
||||
Before any checkout, merge, or rebase, confirm that the current worktree owns
|
||||
the PR head. After reading `headRefName` and `headRefOid`, run
|
||||
`git worktree list --porcelain`, locate the worktree whose branch matches
|
||||
`headRefName`, and operate there. Verify `git branch --show-current`,
|
||||
`git rev-parse HEAD`, and `git status --short` before mutating anything. If no
|
||||
matching worktree exists, state that and create or use an explicit worktree only
|
||||
within the authorized workflow; never risk mutating `main` or another task's
|
||||
worktree.
|
||||
|
||||
Inspect the local worktree:
|
||||
|
||||
```bash
|
||||
@@ -28,6 +41,10 @@ If `git ls-files -u` prints entries, or conflict markers are present in tracked
|
||||
|
||||
### When GitHub reports conflicts and the local index is clean
|
||||
|
||||
Capture the PR branch's remote head before fetching the base or starting the
|
||||
merge. Recheck it immediately before pushing; use the explicit lease shown in
|
||||
**Push after rebasing or merging**.
|
||||
|
||||
1. Fetch the latest base branch:
|
||||
```bash
|
||||
git fetch origin <baseRefName>
|
||||
@@ -51,16 +68,139 @@ If `git ls-files -u` prints entries, or conflict markers are present in tracked
|
||||
git diff --stat origin/<baseRefName>
|
||||
```
|
||||
|
||||
Before committing a merged specification or system-design file, rerun the
|
||||
repository's specification lint, including size checks; a marker-free merge
|
||||
can still fail the file-size hook. Do not raise a legacy size exception to
|
||||
hide that failure.
|
||||
|
||||
Confirm that the final diff against the current GitHub base contains only the PR's intended delta. A clean index is insufficient if a retargeted stacked PR accidentally retains its merged parent's changes.
|
||||
After the operation completes, inspect each resolved path with
|
||||
`git diff -- origin/<baseRefName>...HEAD -- <resolved-file>` and reread the
|
||||
complete surrounding comments and logic; this catches a duplicate-hunk
|
||||
resolution that silently drops an explanatory line.
|
||||
|
||||
Incoming base commits may already contain a partial or similar fix. Compare
|
||||
their helpers and tests with the PR side before staging, and deduplicate
|
||||
overlapping test symbols or behavior rather than blindly preserving both.
|
||||
|
||||
If the base extracted logic into a new helper or type file, or moved its
|
||||
tests, while the PR changed the old inline location, a marker-only merge can
|
||||
silently drop the PR invariant. Inspect the extracted or renamed base file
|
||||
and moved tests, port the PR-specific behavior into the new owner, update
|
||||
imports and exports, then run the focused helper and consumer tests. Verify
|
||||
with `git diff origin/<baseRefName>...HEAD -- <resolved-files>` and the
|
||||
affected test suite.
|
||||
|
||||
If the current base moved a flat legacy spec into `requirements/` and
|
||||
`system-design/` paths, preserve the canonical base split. Move branch-only
|
||||
authority into the correct migrated part, add branch-only documents to the
|
||||
system README maps, and remove stale old-path entries from
|
||||
`docs/specs/spec-lint.json`. Then run `python3 scripts/lint-spec-files.py
|
||||
--all` and inspect `git diff <base>...HEAD` for the intended migration delta.
|
||||
Verify again with `git ls-files -u`, the marker scan, and the full spec lint.
|
||||
|
||||
If conflict resolution touches `apps/backend/go.mod` or `apps/backend/go.sum`,
|
||||
marker-free text is not enough: a union can silently lose a required module
|
||||
checksum. From `apps/backend`, run `go mod tidy` and then `go mod verify`
|
||||
sequentially, inspect that only intended module files changed, and run the
|
||||
affected backend package tests or build before committing.
|
||||
|
||||
For structured files (especially JSON catalogs), validate syntax and duplicate
|
||||
keys before continuing. JSON's default parser may silently keep the last
|
||||
duplicate key; use an `object_pairs_hook` that raises on duplicates, then run
|
||||
the affected formatter/test and `git diff --check`.
|
||||
the affected formatter/test and `git diff --check`. Also run the
|
||||
authoritative schema or domain validator when the file's semantics span
|
||||
arrays or ownership assignments; object-key checks cannot catch duplicate
|
||||
array-level assignments.
|
||||
|
||||
When the resulting `HEAD` is a merge commit, do not use `HEAD^1..HEAD` or a
|
||||
parent diff as the PR delta: depending on parent order it can include the
|
||||
merged base or omit the branch's changes. Use the authoritative three-dot
|
||||
range, `git diff <base-remote>/<base-ref>...HEAD`, and inspect the merge
|
||||
parents with `git log --first-parent` only for history. If
|
||||
`git diff --cached --check` reports whitespace that came from the incoming
|
||||
base, compare the base-only range separately before attributing it to the PR;
|
||||
keep the strict check for whitespace introduced by the PR or resolution.
|
||||
|
||||
### When the base advanced without conflicts
|
||||
|
||||
An advanced base is not itself a reason to rebase or merge the PR. When exact-
|
||||
head CI is green, the base advanced, and GitHub reports no file conflict,
|
||||
compare the paths changed on both sides of the old merge base:
|
||||
|
||||
```bash
|
||||
git diff --name-only <old-merge-base>..<authoritative-base> > /tmp/base-paths
|
||||
git diff --name-only <old-merge-base>..HEAD > /tmp/head-paths
|
||||
comm -12 <(sort /tmp/base-paths) <(sort /tmp/head-paths)
|
||||
```
|
||||
|
||||
Build the exact conflict-free synthetic merge without changing the PR branch.
|
||||
Use the overlapping paths and shared contracts to select focused checks.
|
||||
Disjoint paths can still interact through imports, schemas, or configuration.
|
||||
`git merge-tree --write-tree` returns a tree object;
|
||||
turn it into an unreachable merge commit and inspect it in a detached
|
||||
worktree:
|
||||
|
||||
```bash
|
||||
SYNTHETIC_TREE="$(git merge-tree --write-tree <authoritative-base> HEAD)"
|
||||
SYNTHETIC_COMMIT="$(printf 'review synthetic merge\n' | \
|
||||
git commit-tree "$SYNTHETIC_TREE" -p <authoritative-base> -p HEAD)"
|
||||
git worktree add --detach /tmp/pr-merge-review-<number> "$SYNTHETIC_COMMIT"
|
||||
# Run focused checks for affected paths and shared contracts in that worktree.
|
||||
git worktree remove --force /tmp/pr-merge-review-<number>
|
||||
```
|
||||
|
||||
Do not push or create a branch for the synthetic commit. Frontend checks need
|
||||
the detached worktree's own `apps/node_modules` installation; do not assume a
|
||||
sibling worktree's dependencies are available. Verify the synthetic merge,
|
||||
focused tests, clean normal worktree, local `HEAD` equal to its upstream and PR
|
||||
head, and a fresh GitHub query still reporting `MERGEABLE`/`CLEAN`.
|
||||
|
||||
Record the immutable PR head, authoritative base, synthetic commit, commands,
|
||||
and results. This evidence satisfies the base-drift completion gate only while
|
||||
both input SHAs remain current. `base_advanced_since_head` remains true because
|
||||
the PR branch did not change. Preserve the helper's exit code and report the
|
||||
separate merge-result validation. Human approval and merge-queue gates still apply.
|
||||
|
||||
### When related PRs conflict semantically without file conflicts
|
||||
|
||||
GitHub's mergeability state covers file-level compatibility, not whether
|
||||
stacked or parallel PRs still agree on the durable contract. Use this path when
|
||||
a plan, review, or PR names a dependency and the current PR or landed dependency
|
||||
has overlapping `REQ-*`/`AC-*` IDs, conflicting design statements, or a changed
|
||||
API/interface contract even though GitHub reports `MERGEABLE`.
|
||||
|
||||
1. Resolve the current head and merged state of every related PR. Treat refs in
|
||||
an older plan or review as snapshots; re-read the affected requirements,
|
||||
system designs, plans, work orders, and test annotations at the current
|
||||
heads.
|
||||
2. Identify the landed contract as authoritative. Preserve its stable IDs and
|
||||
meanings; allocate the next unused IDs for distinct behavior instead of
|
||||
renumbering or reusing an existing ID to hide the collision.
|
||||
3. Reconcile all affected traceability links, including every `REQ-*`/`AC-*`
|
||||
reference in requirements, system designs, plans, work orders, and test
|
||||
`@covers` annotations, plus implementation assumptions to the current interface.
|
||||
Do not call isolated green checks composable until the producer and consumer
|
||||
agree on the same contract.
|
||||
4. Run `python3 scripts/lint-spec-files.py --all` and the focused producer,
|
||||
consumer, and integration tests. After any integration push, refresh the
|
||||
exact PR head and repeat the dependency and contract comparison.
|
||||
|
||||
Verify that the related PR heads and merged state were recorded, no duplicate
|
||||
or repurposed stable IDs remain, all affected artifacts reference the same
|
||||
contract, spec lint and focused tests pass, and the final exact-head evidence
|
||||
was collected after the last integration push.
|
||||
|
||||
### Queue synthetic-base conflicts
|
||||
|
||||
A merge-queue coordinator may provide a synthetic base that conflicts with
|
||||
the PR even when the PR reports `MERGEABLE/CLEAN` and exact-head checks pass.
|
||||
Treat this as queue-order merge evidence, not a CI failure. Verify the SHA is
|
||||
a commit, run `git merge-tree <synthetic-base> HEAD`, and inspect both
|
||||
`git show <synthetic-base>:<path>` and `git show HEAD:<path>` (or staged blobs).
|
||||
Preserve both sides manually, then require `git merge-tree <synthetic-base>
|
||||
<new-head>` to be conflict-free before focused checks, a hooked commit, push,
|
||||
and fresh exact-head PR state.
|
||||
|
||||
### When the local index is already unmerged
|
||||
|
||||
@@ -108,7 +248,10 @@ Do not discard unrelated user changes to make a merge/rebase easier. If unrelate
|
||||
Before a force-push after a rebase or conflict merge, run the task-defined checks
|
||||
for every affected package. For web changes, run the affected test suite plus
|
||||
`cd apps/web && pnpm run typecheck` and the web lint gate; use the equivalent
|
||||
package checks for backend changes. Cleared markers do not prove a semantic
|
||||
package checks for backend changes. Run managed E2E with `pnpm e2e:run` after
|
||||
the merge, or rebuild both `make -C apps/backend build` and
|
||||
`make -C apps/backend e2e-plugin-package` before any `--no-build` run, because
|
||||
global setup rejects stale packaged fixtures. Cleared markers do not prove a semantic
|
||||
resolution: duplicate imports/hooks and stale localized mocks require these
|
||||
checks to pass.
|
||||
|
||||
@@ -145,3 +288,19 @@ An intervening fetch can update the remote-tracking ref consulted by generic
|
||||
`--force-with-lease`. If the prior remote SHA was not captured, stop and capture
|
||||
it before rewriting when possible; use the generic form only as an explicit last
|
||||
resort, and never use an unconditional force push.
|
||||
|
||||
After a conflict merge is pushed, run `scripts/pr-await <PR>` for the new head
|
||||
and then refresh `scripts/pr-state --summary <PR>` and
|
||||
`scripts/pr-resolve list <PR>`. Treat the merge commit's checks and review state
|
||||
as new evidence; do not reuse the pre-merge report.
|
||||
|
||||
If preserving both sides of a conflict makes a whole-file hook exceed its
|
||||
max-lines or size limit, remove duplicated guidance or refactor the file while
|
||||
keeping both contracts. Rerun the affected lint; never bypass the hook or add
|
||||
an unexplained exception just to complete the merge.
|
||||
|
||||
If the user explicitly requires a one-commit PR, resolve and verify the current
|
||||
base first, then use a soft reset to that base, recommit through normal hooks,
|
||||
and push with the captured exact-head lease. Verify
|
||||
`git rev-list --count <base>..HEAD` and the live PR head. Otherwise retain the
|
||||
merge commit created by conflict resolution.
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
# Merge Queue Coordination
|
||||
|
||||
Load this reference when the user requests merge-queue coordination, or when
|
||||
authoritative GitHub data shows a non-null `mergeQueueEntry`. Queue mutations
|
||||
are external writes: dequeue and enqueue only with explicit user
|
||||
authorization. `scripts/pr-await` validates the pull-request head; it does not
|
||||
prove the synthetic merge-group commit passed. For review-only work, report
|
||||
queue membership as a mutation blocker and do not dequeue just to inspect or
|
||||
reply.
|
||||
|
||||
## Identify the queue attempt
|
||||
|
||||
Use the pull-request GraphQL node ID for mutations. It is different from the
|
||||
active merge-queue entry ID. Record the current pull-request head and base, the
|
||||
queue entry state and position, and the synthetic commit used for its checks:
|
||||
|
||||
```bash
|
||||
gh api graphql \
|
||||
-f query='query($owner:String!,$repo:String!,$number:Int!) {
|
||||
repository(owner:$owner,name:$repo) {
|
||||
pullRequest(number:$number) {
|
||||
id
|
||||
headRefName
|
||||
headRefOid
|
||||
baseRefName
|
||||
mergeQueueEntry {
|
||||
id
|
||||
state
|
||||
position
|
||||
headCommit {
|
||||
oid
|
||||
statusCheckRollup { state }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}' -f owner=<owner> -f repo=<repo> -F number=<PR>
|
||||
```
|
||||
|
||||
The `pullRequest.id` is the PR node ID. Do not pass
|
||||
`mergeQueueEntry.id` to `dequeuePullRequest` or `enqueuePullRequest`.
|
||||
|
||||
## Authorized branch updates
|
||||
|
||||
1. Capture the reported PR head OID and authoritative base name. Confirm the
|
||||
linked-worktree preflight in `merge-conflicts.md` before mutating a branch.
|
||||
2. If the PR is queued, dequeue it through the repository-approved path before
|
||||
pushing. A push rejected with `GH006` means the queue still owns the branch;
|
||||
do not retry the push or force-push around that protection. Re-query
|
||||
`pullRequest.mergeQueueEntry` until it is null after the dequeue request;
|
||||
queue state is authoritative while the mutation propagates.
|
||||
3. Fetch the fresh base, merge or resolve it using the conflict procedure, run
|
||||
the affected gates, and commit with normal hooks. Recheck the remote head
|
||||
lease immediately before pushing.
|
||||
4. After the push, verify the PR head OID, exact-head checks, review state, and
|
||||
mergeability. Every push or base reconciliation invalidates earlier CI and
|
||||
review evidence.
|
||||
5. Re-enqueue only after the pushed head is verified. Pass the PR node ID and
|
||||
the newly verified head as `expectedHeadOid`; do not use a stale queue-entry
|
||||
ID or stale head. GitHub may restore queue membership automatically after
|
||||
required checks pass; if it does, monitor the existing entry instead of
|
||||
enqueueing it a second time.
|
||||
|
||||
The GraphQL mutation shapes are:
|
||||
|
||||
```bash
|
||||
gh api graphql \
|
||||
-f query='mutation($prNodeId:ID!) {
|
||||
dequeuePullRequest(input:{id:$prNodeId}) {
|
||||
clientMutationId
|
||||
}
|
||||
}' -f prNodeId=<pull-request-node-id>
|
||||
|
||||
gh api graphql \
|
||||
-f query='mutation($pullRequestId:ID!,$expectedHeadOid:GitObjectID!) {
|
||||
enqueuePullRequest(input:{pullRequestId:$pullRequestId,expectedHeadOid:$expectedHeadOid}) {
|
||||
clientMutationId
|
||||
}
|
||||
}' -f pullRequestId=<pull-request-node-id> \
|
||||
-f expectedHeadOid=<verified-head-oid>
|
||||
```
|
||||
|
||||
Use the repository's approved UI or API if these mutations are unavailable to
|
||||
the current token. Do not substitute a queue-entry ID, and do not retry a
|
||||
failed mutation without rereading the PR and queue state.
|
||||
|
||||
## Monitor the synthetic result
|
||||
|
||||
After enqueueing, reread `mergeQueueEntry` and its `headCommit` rollup. Treat
|
||||
`QUEUED` and `AWAITING_CHECKS` as pending. A persistently `UNMERGEABLE` entry is
|
||||
an explicit signal to stop, report the state, and obtain authorization before
|
||||
dequeueing or reconciling again. Stop on `MERGED` or `CLOSED`, or on a clearly
|
||||
documented external blocker. Map queue-removal timeline events and their
|
||||
`beforeCommit.oid` to the corresponding `merge_group` run as described in
|
||||
`ci-troubleshooting.md`; ordinary `gh pr checks` does not cover that synthetic
|
||||
commit.
|
||||
|
||||
For a request to monitor until the PR is merged, include the pull request's
|
||||
`state`, `mergedAt`, `mergeCommit { oid }`, and `isInMergeQueue` in each bounded
|
||||
poll. Do not report completion from a clean head check, a non-null merge entry,
|
||||
or a null `autoMergeRequest`: require `state=MERGED`, both merge fields, and
|
||||
`isInMergeQueue=false`. Stop on `CLOSED` or a clearly documented external
|
||||
blocker, and keep the final snapshot with the synthetic commit and queue state.
|
||||
|
||||
If the queue entry is `UNMERGEABLE` while the PR API reports
|
||||
`mergeable=UNKNOWN`, fetch the authoritative base and run
|
||||
`git merge-tree --write-tree HEAD origin/<base>` (or the equivalent current
|
||||
base ref) before mutating the branch. Use the conflict procedure only when the
|
||||
preflight finds a real conflict; otherwise treat the GitHub state as transient
|
||||
and re-query it.
|
||||
|
||||
Recheck the PR head after any queue transition. A new push, base race, queue
|
||||
ejection, or changed expected head starts a new evidence cycle: rerun the exact
|
||||
head gates, then inspect the new merge-group commit and checks.
|
||||
@@ -82,7 +82,9 @@ unknown, do not call review clean/blocked; retry once, then use
|
||||
is nonzero while visible threads are empty, fetch the authoritative thread list
|
||||
and full bodies with `scripts/pr-resolve show <PR> <THREAD_ID>`; use
|
||||
`scripts/pr-state --comment <comment_id>` only when a flat comment view is all
|
||||
that is available.
|
||||
that is available. The numeric ID may identify either a review comment or a
|
||||
top-level issue comment; the helper falls back between both endpoints and emits
|
||||
`comment_type`, but it does not resolve a review thread.
|
||||
|
||||
If `branch:"unknown"` or PR-view resolution is transient, retry the explicit
|
||||
PR-number command once before using direct targeted GitHub fallback. Do not
|
||||
@@ -118,19 +120,8 @@ body with `scripts/pr-resolve show <PR> <THREAD_ID>`; use the flat comment comma
|
||||
only for a comment without thread context. A listed thread that is already
|
||||
resolved is stale summary state: re-poll and do not reply again.
|
||||
|
||||
Poll at 30-second cadence with a 20-minute cap using bounded one-shot commands;
|
||||
avoid long inline loops and `gh pr checks --watch`. In default monitoring,
|
||||
stop early on a required failure. For an explicit fixed-duration request, use
|
||||
strict-deadline mode: accumulate failures and comments until the absolute
|
||||
deadline, stopping early only if the PR is merged/closed or access is revoked.
|
||||
Queued/in-progress jobs are pending, not speculative-fix triggers. On an
|
||||
explicit wait-through-CI request without a fixed deadline, use the same
|
||||
20-minute absolute cap: repeat bounded checks until failures, pending checks,
|
||||
and unresolved-thread count are all empty/zero, or stop at the deadline and
|
||||
report remaining pending checks or unresolved threads. A nonzero unresolved
|
||||
count is a blocker even when every remaining thread is informational, optional,
|
||||
or invalid. Preserve early stopping
|
||||
for failures and merged/closed or access-revoked conditions.
|
||||
For wait modes, deadlines, exit codes, and interrupted waiters, load
|
||||
[waiting.md](waiting.md). It owns the monitoring procedure.
|
||||
|
||||
For E2E-only pending work, summarize a saved snapshot before printing shards:
|
||||
|
||||
@@ -140,5 +131,4 @@ jq '{failed_checks, pending_count:(.pending_checks|length), unresolved_review_th
|
||||
jq -r '.pending_checks[] | "\(.status) | \(.name)"' /tmp/prstate-<PR>.json
|
||||
```
|
||||
|
||||
If a manual poll is interrupted, terminate only polling processes you started.
|
||||
Use raw `scripts/pr-state <PR>` only for an odd-state diagnostic.
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
# PR Fixup Transport Troubleshooting
|
||||
|
||||
Load this reference when CI setup fails before tests, or when GraphQL evidence
|
||||
is unavailable but authenticated REST calls still work.
|
||||
|
||||
**Corepack/pnpm bootstrap transport failures:** If `pnpm install --frozen-lockfile`
|
||||
fails while Corepack downloads pnpm, before repository tests start, and the log
|
||||
shows a Node/undici transport or parser assertion, classify it as runner or
|
||||
registry infrastructure. Verify the exact job head and terminal parent run,
|
||||
then rerun the failed job once with `gh run rerun <run-id> --failed`; recheck
|
||||
`scripts/pr-state --summary` against that same head. If it repeats, inspect the
|
||||
runtime image/Corepack toolchain instead of changing application code.
|
||||
|
||||
**GraphQL/rate-limit degradation:** If `pr-await` or `pr-state` cannot read
|
||||
GraphQL while REST remains available, query the REST pull request for state,
|
||||
mergeability, and head SHA, then `/commits/<head_sha>/check-runs?per_page=100`
|
||||
for checks. Require the REST head to equal the check snapshot head. REST cannot
|
||||
prove review-thread resolution, so keep thread evidence unknown until
|
||||
`scripts/pr-resolve` or a connector succeeds; never call the PR clean from
|
||||
partial REST evidence. A queued job with `runner_id=0`, no runner name, and no
|
||||
completed steps is hosted-runner backlog, not a test hang; do not rerun or
|
||||
cancel solely because it is queued. Finish with a fresh state, thread, and
|
||||
mergeability audit.
|
||||
@@ -0,0 +1,79 @@
|
||||
# Waiting for PR checks
|
||||
|
||||
Load this reference when checks are pending or a user requests PR monitoring.
|
||||
Use `scripts/pr-await <PR>` for the wait. It owns the polling loop and returns
|
||||
one report. Do not run `pr-state` on a timer in the primary conversation.
|
||||
|
||||
## Select the wait mode
|
||||
|
||||
- Use `--mode all-terminal` by default. It waits for parent workflows and all
|
||||
registered checks to finish before reporting the complete failure set.
|
||||
- For an explicit fixed-duration hold, use
|
||||
`--mode strict-deadline --deadline-min <N>`. It continues after terminal CI
|
||||
until the deadline, unless the PR closes or evidence becomes blocked.
|
||||
- For an upper time limit that permits an early result, use
|
||||
`--mode all-terminal --deadline-min <N>`.
|
||||
- Use `--mode first-failure` only when the user requests the first failure.
|
||||
|
||||
The default deadline is 45 minutes and the default cadence is 60 seconds.
|
||||
Pass an explicit deadline for a user-specified limit. Use `--interval-sec <S>`
|
||||
when the user specifies a cadence. Pending matrix counts can grow as jobs appear.
|
||||
|
||||
## Interpret the final result
|
||||
|
||||
Read the final tool result's `exit_code`, including for PTY/session commands.
|
||||
|
||||
| Exit | Meaning | Action |
|
||||
| --- | --- | --- |
|
||||
| 0 | Terminal CI/review counts are clean | Refresh and classify review bodies before delivery. |
|
||||
| 1 | Terminal findings, conflicts, or base drift | Triage the reported findings. |
|
||||
| 2 | Pending checks or an unconfirmed terminal rollup at the deadline | Report the pending work at the user's limit. Otherwise continue waiting. |
|
||||
| 3 | Closed PR or unavailable/blocked evidence | Read the stated reason. Never infer a clean result. |
|
||||
|
||||
If exit 1 reports only base drift, use the advanced-base procedure in
|
||||
[merge-conflicts.md](merge-conflicts.md). Record its separate validation result.
|
||||
The helper's exit code remains 1 even when that validation succeeds.
|
||||
|
||||
If no user limit prevents further waiting, rerun after exit 2 with a larger
|
||||
deadline. Do not replace the waiter with timer-driven snapshots.
|
||||
An `all-terminal` result can arrive before the deadline. Report actual elapsed time.
|
||||
Strict-deadline returns 0 or 1 for confirmed terminal evidence, 2 for pending or
|
||||
unconfirmed work, and 3 for blocked evidence.
|
||||
|
||||
Exit 3 includes approval-required runs, access errors, unknown mergeability,
|
||||
incomplete snapshots, and unknown required-check policy. The reported toolchain
|
||||
versions are diagnostic context, not substitute evidence.
|
||||
|
||||
## Refresh after waiting
|
||||
|
||||
Run `scripts/pr-state --summary <PR>` and `scripts/pr-resolve list <PR>` after
|
||||
each report. Require the check SHA to match the fresh PR head.
|
||||
If delayed checks appear, restart the waiter. A sparse early rollup cannot
|
||||
prove completion. The helper requires two matching terminal snapshots.
|
||||
|
||||
Inspect every current-head review body, including aggregate bot reviews.
|
||||
If earlier snapshots contained top-level findings absent from the new head's
|
||||
review list, run one `scripts/pr-state --summary --all` audit.
|
||||
Revalidate those findings against current source.
|
||||
|
||||
If a job exceeds its configured timeout or contradicts the rollup, query its
|
||||
exact job/run before diagnosing a hang. Verify that its SHA matches the PR head.
|
||||
For fork workflow approval, inspect `approval_required_runs`.
|
||||
An authorized fixup can approve the exact run through
|
||||
`gh api --method POST repos/<owner>/<repo>/actions/runs/<run-id>/approve`.
|
||||
Then refresh state and wait for jobs to appear. `gh run approve` is invalid.
|
||||
|
||||
If the PR becomes `MERGED` or `CLOSED`, stop. For a merge, report `mergedAt`
|
||||
and `mergeCommit.oid`. Do not recreate, update, or re-enqueue its stale branch.
|
||||
For synthetic merge-group checks, use [merge-queue.md](merge-queue.md).
|
||||
|
||||
## Interrupted or unavailable waiters
|
||||
|
||||
Retain the session handle until the command ends. An interrupted waiter has no
|
||||
verdict. If the handle is lost, inspect and stop only its owned process tree
|
||||
before starting a replacement. Never launch duplicate monitors.
|
||||
|
||||
Use the read-only `pr-poller` only when the user explicitly requests monitoring
|
||||
and `pr-await` is unavailable. Give it the user's deadline, or a 20-minute cap
|
||||
when no limit exists. Treat its output as provisional and refresh before acting.
|
||||
Do not use interactive `gh pr checks --watch` in the primary conversation.
|
||||
@@ -107,6 +107,13 @@ explicitly requests task tracking.
|
||||
|
||||
Do not fall back to hand-composed `--body` prose. If creation fails, surface the exact stderr, fix the template/body-file problem, and retry with `--body-file`.
|
||||
|
||||
If `gh pr create` fails after the branch is pushed with a credential-lease
|
||||
or repository-scope error, use the REST fallback with the same template body.
|
||||
Build the payload with `jq --rawfile` and submit it with
|
||||
`gh api --method POST repos/<owner>/<repo>/pulls --input <payload-file>`;
|
||||
preserve the validated title, head, base, and body, and never hand-escape
|
||||
Markdown or JSON.
|
||||
|
||||
6. **If ready (not draft):** For GitHub, do not begin `/pr-fixup` until any
|
||||
required screenshot embedding in step 7 is complete.
|
||||
|
||||
@@ -118,6 +125,9 @@ required screenshot embedding in step 7 is complete.
|
||||
treating capture as blocked. Name mobile specs `mobile-*.spec.ts`, write
|
||||
assets to ignored `apps/web/.pr-assets`, inspect/compress them, then remove
|
||||
the temporary spec and confirm `git status` is clean.
|
||||
- After opening a popover or dialog, assert that the intended surface is
|
||||
visible and await finite active CSS animations (`element.getAnimations().finished`)
|
||||
before capture; do not publish a mid-transition asset.
|
||||
- For disposable capture specs, prefer the existing `prCapture` fixture from
|
||||
`apps/web/e2e/fixtures/test-base.ts`; it writes the expected filenames and
|
||||
manifest for PR assets.
|
||||
@@ -184,11 +194,19 @@ required screenshot embedding in step 7 is complete.
|
||||
PAYLOAD="/tmp/pr-body-<PR_NUMBER>-payload.json"
|
||||
jq -n --rawfile body "<body-file>" '{body: $body}' > "$PAYLOAD"
|
||||
jq empty "$PAYLOAD"
|
||||
gh api --method PATCH repos/:owner/:repo/pulls/<PR_NUMBER> --input "$PAYLOAD"
|
||||
gh api --method PATCH repos/:owner/:repo/pulls/<PR_NUMBER> --input "$PAYLOAD" --silent
|
||||
```
|
||||
For a write-only PATCH, `--silent` prevents `gh` from decoding a response
|
||||
body that the caller does not consume. Redirecting stdout to `/dev/null`
|
||||
does not prevent a truncated or empty JSON response from making `gh` report
|
||||
an error after the mutation succeeds. Read the resource back with a
|
||||
separate GET and verify the intended change.
|
||||
**JSON payloads:** Use `jq` (or an equivalent JSON tool) to build and
|
||||
validate payloads without interpolating untrusted Markdown into shell
|
||||
syntax. Keep the REST fallback byte-preserving for command substitutions,
|
||||
syntax. When command output is redirected, piped, or consumed by `jq` or
|
||||
another parser, run the command through `rtk proxy` so stdout is
|
||||
byte-preserving; normal RTK filtering can truncate or annotate machine-readable
|
||||
output. Keep the REST fallback byte-preserving for command substitutions,
|
||||
`xargs`, and any other consumer that expects unmodified Git or JSON output.
|
||||
|
||||
**Preserve the existing PR description:** The PR body is a shared, mutable
|
||||
|
||||
@@ -27,7 +27,8 @@ change, plan sequence, or temporary migration step.
|
||||
1. Choose an ID in the form `YYYY-MM-DD-short-title`.
|
||||
2. Make sure that `docs/decisions/<id>.md` does not exist.
|
||||
3. Create the ADR with the template below.
|
||||
4. Add it to `docs/decisions/INDEX.md`.
|
||||
4. Run `python3 scripts/list-docs.py decisions --format paths` to confirm the
|
||||
new ADR is discoverable. Do not edit a catalog row.
|
||||
5. Reconcile affected specifications.
|
||||
|
||||
Existing numeric ADR IDs remain valid. Do not rename them.
|
||||
@@ -58,8 +59,9 @@ State each meaningful alternative and why it was not selected.
|
||||
|
||||
## Reconcile specifications
|
||||
|
||||
Read `docs/specs/README.md`, the owning system index, and the relevant files in
|
||||
`docs/specs/guide/`.
|
||||
Read `docs/specs/README.md`, the owning system `README.md`, and the relevant
|
||||
files in `docs/specs/guide/`. Use the catalog command to locate the owning
|
||||
requirements and designs.
|
||||
|
||||
Apply these rules:
|
||||
|
||||
@@ -78,7 +80,10 @@ Do not copy the ADR into requirements or system design. Link the ADR from the
|
||||
affected design. Requirements contain observable outcomes, not decision
|
||||
rationale.
|
||||
|
||||
During legacy migration, use `docs/specs/INDEX.md` to locate existing sources.
|
||||
During legacy migration, use this command to locate existing sources:
|
||||
|
||||
python3 scripts/list-docs.py specs --kind legacy --format paths
|
||||
|
||||
Do not create a new generic `spec.md` file.
|
||||
|
||||
## Validation
|
||||
@@ -87,6 +92,7 @@ Run:
|
||||
|
||||
```bash
|
||||
python3 scripts/lint-spec-files.py --all
|
||||
python3 scripts/list-docs.py validate
|
||||
git diff --check -- docs/decisions docs/specs
|
||||
```
|
||||
|
||||
|
||||
@@ -52,9 +52,17 @@ Keep a visible task list:
|
||||
6. Execute work orders with TDD after an explicit implementation request.
|
||||
7. Open the PR, address valid findings, record changes, and report.
|
||||
|
||||
When task-plan MCP tools are available, use the Kandev task plan for live phase
|
||||
progress and update it after each completed design phase. Keep
|
||||
`docs/plans/<initiative>/plan.md` and its work orders as the durable
|
||||
implementation package; neither plan replaces the other.
|
||||
|
||||
## Phases 1 to 4: Design
|
||||
|
||||
Use `/interview-me` only when the request needs clarification.
|
||||
Run the `/interview-me` assumption check before drafting the package. Reuse
|
||||
confirmed choices across phases; ask only about material unresolved decisions.
|
||||
For large uncertain initiatives, use its optional decision-mapping reference
|
||||
before creating detailed work orders. Continue the package once blockers resolve.
|
||||
|
||||
Run the `/spec` ownership gate before you choose paths. Search adjacent systems
|
||||
for the same capability. Choose the owner from the durable contract, not from
|
||||
@@ -91,6 +99,10 @@ Each work order must contain:
|
||||
Work orders do not name a worker role or model tier. Waves can identify
|
||||
parallel-safe candidates. They do not authorize subagents.
|
||||
|
||||
For rendered UI changes, include ASCII previews in the plan and relevant UI
|
||||
work orders through `/plan`. Follow the shared contract in
|
||||
`docs/specs/guide/plans-and-work-orders.md#ascii-ui-previews`.
|
||||
|
||||
## Design-package handoff
|
||||
|
||||
Before implementation, report:
|
||||
@@ -100,6 +112,8 @@ Before implementation, report:
|
||||
- The plan and work-order paths.
|
||||
- Dependency order and exact verification commands.
|
||||
- Open risks and exclusions.
|
||||
- For UI changes, a compact inline ASCII preview of the proposed UI, with a
|
||||
distinct phone view when composition differs, plus links to the full previews.
|
||||
|
||||
Then end the turn. Do not ask the user to approve the package or switch models.
|
||||
The user reviews the files and sends a later implementation request.
|
||||
@@ -122,7 +136,15 @@ For each work order, in dependency order:
|
||||
2. Change only that work order to `in_progress`.
|
||||
3. Implement with `/tdd`. Use `/e2e` and `/mobile-parity` when applicable.
|
||||
4. Run the exact targeted verification commands.
|
||||
5. Change the work order to `done` and synchronize `plan.md`.
|
||||
5. After implementation and targeted checks pass, change only the current work
|
||||
order to `done`.
|
||||
6. After every linked work order passes, verify the implementation still matches
|
||||
each paired draft system design. If it does, promote that design from `draft`
|
||||
to `current`, synchronize affected spec frontmatter and matching
|
||||
`docs/specs/INDEX.md` entries from `building` to `shipped`, then change the
|
||||
plan to `implemented`. If implementation diverged or an open design issue
|
||||
remains, leave the design draft and report the mismatch instead of completing
|
||||
the package.
|
||||
|
||||
If the user authorizes subagents, launch only parallel-safe work orders in the
|
||||
requested wave. Use native delegation with no full-history fork. Give each
|
||||
|
||||
@@ -36,16 +36,30 @@ Route the request before you write:
|
||||
|
||||
Do not create a generic `spec.md` file.
|
||||
|
||||
When routing to `docs/specs/product/`, read `docs/specs/product/README.md`
|
||||
before editing. Treat its **Product document index** as the local index: read
|
||||
every linked product document and any co-located `INDEX.md`, `AGENTS.md`,
|
||||
`CLAUDE.md`, or other instruction file when present. Product files provide
|
||||
cross-system context, not feature requirements; preserve proposed and
|
||||
open-question language instead of promoting it to an active contract without
|
||||
confirmation.
|
||||
|
||||
## Workflow
|
||||
|
||||
### 1. Locate the owning system
|
||||
|
||||
Read `docs/specs/README.md` and the likely system `README.md`. If the system has
|
||||
not migrated, use `docs/specs/INDEX.md` to locate the legacy source.
|
||||
Read `docs/specs/README.md` and the likely system `README.md`. If the system
|
||||
has not migrated, run this command to locate the legacy source:
|
||||
|
||||
Search all system indexes, requirements, and designs for the capability name
|
||||
and its main nouns. Update an existing capability when it owns the same actor,
|
||||
lifecycle, and contract.
|
||||
python3 scripts/list-docs.py specs --kind legacy --format paths
|
||||
|
||||
Search the catalog, requirements, and designs for the capability name and its
|
||||
main nouns:
|
||||
|
||||
python3 scripts/list-docs.py specs --text <capability-term> --format paths
|
||||
|
||||
Update an existing capability when it owns the same actor, lifecycle, and
|
||||
contract.
|
||||
|
||||
Choose the system that owns the source of truth and durable contract. Do not
|
||||
choose an owner from the code directories that change. Record one sentence in
|
||||
@@ -65,9 +79,10 @@ requirements. A new system needs a `README.md` based on the system template.
|
||||
|
||||
### 2. Confirm intent
|
||||
|
||||
Use `/interview-me` when a missing product choice changes behavior, ownership,
|
||||
permissions, persistence, or a public contract. Do not hide an unresolved
|
||||
choice in a draft.
|
||||
Run the `/interview-me` assumption check, reusing answers from earlier phases.
|
||||
Resolve material choices before writing the affected contract. Preserve settled
|
||||
terminology and decision rationale in the owning artifacts through that skill.
|
||||
Do not hide an unresolved choice in a draft.
|
||||
|
||||
### 3. Write requirements
|
||||
|
||||
@@ -113,14 +128,28 @@ design can include backend services, storage, projections, frontend components,
|
||||
responsive behavior, and tests. Do not create a parallel UI design for those
|
||||
same requirements.
|
||||
|
||||
### 5. Update the system index
|
||||
### 5. Update the system boundary
|
||||
|
||||
Add the new documents to the system `README.md`. State the system boundary and
|
||||
link adjacent systems when ownership can be confused.
|
||||
Update the system `README.md` only when the system boundary, migration record,
|
||||
or related-system links change. State the system boundary and link adjacent
|
||||
systems when ownership can be confused. Do not add a requirement or
|
||||
system-design list.
|
||||
|
||||
Before and after adding required links, run `wc -c <system>/README.md`. Near
|
||||
the 12 KiB `system-index` limit, keep every required link but use concise
|
||||
labels or other non-semantic compression; never add a size exception. Rerun
|
||||
the specification linter after the index update. Also search the README for
|
||||
count or list summaries, update them when the authoritative pair count changes,
|
||||
and verify that each stated count matches the indexed requirement/design pairs.
|
||||
|
||||
During migration, name the new source as authoritative. Replace the old source
|
||||
with a link or archive it. Do not leave two editable sources of truth.
|
||||
|
||||
If a migration branch merges or rebases a moving base, re-inventory the
|
||||
migration root after the update. Review files newly added by the base, migrate
|
||||
them or explicitly record them as unmigrated additions before marking the
|
||||
migration complete, then rerun the full specification lint.
|
||||
|
||||
### 6. Validate
|
||||
|
||||
Review the artifacts before you run the linter:
|
||||
@@ -131,13 +160,18 @@ Review the artifacts before you run the linter:
|
||||
details.
|
||||
- Every acceptance criterion states a testable behavior. No criterion delegates
|
||||
its meaning to migrated source detail.
|
||||
- Selection, restoration, and recovery criteria state candidate eligibility,
|
||||
invalid or ambiguous fallback behavior, and forbidden side effects.
|
||||
- Designs map requirement IDs without copying requirement text.
|
||||
- Each design identifier that names existing code matches the current source.
|
||||
Use `rg` to confirm exact symbols before the artifact is complete.
|
||||
- New files do not copy the legacy `Migrated source detail` wrapper.
|
||||
- New artifacts appear in the owning system index.
|
||||
- New artifacts appear in the catalog command output for the owning system.
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
python3 scripts/list-docs.py validate
|
||||
python3 scripts/lint-spec-files.test.py
|
||||
python3 scripts/lint-spec-files.py --all
|
||||
git diff --check -- docs/specs docs/decisions
|
||||
@@ -146,6 +180,10 @@ git diff --check -- docs/specs docs/decisions
|
||||
If a file reaches its size limit, split it by capability, lifecycle, or contract
|
||||
boundary. Do not add a size exception for a new document.
|
||||
|
||||
An existing `legacy_size_exceptions` value is a frozen ratchet. When a legacy
|
||||
file grows, reduce or split the content and lower the exception to the resulting
|
||||
exact byte size; never raise the ceiling merely to silence lint.
|
||||
|
||||
## Design-package behavior
|
||||
|
||||
When this skill runs inside `/spec-driven-development` or `/fix`, continue to
|
||||
|
||||
@@ -33,6 +33,12 @@ For UI rendering bugs, prefer extracting or using a pure helper and testing that
|
||||
|
||||
## Determine test scope
|
||||
|
||||
When implementing a UI work order, read its ASCII UI preview and linked plan
|
||||
before changing the surface. Use the structural requirements to guide the
|
||||
existing rendered checks; do not test ASCII whitespace or treat it as exact
|
||||
pixel geometry. Keep any design revisions synchronized through
|
||||
`docs/specs/guide/plans-and-work-orders.md#ascii-ui-previews`.
|
||||
|
||||
- **Go unit** (`apps/backend/`): test file next to source as `*_test.go`. Run:
|
||||
```bash
|
||||
cd apps/backend && go test -v -run TestName ./internal/path/to/package/...
|
||||
@@ -43,6 +49,8 @@ For UI rendering bugs, prefer extracting or using a pure helper and testing that
|
||||
```
|
||||
- **Web E2E** (`apps/web/e2e/`): follow `/e2e` when the current task needs Playwright tests.
|
||||
|
||||
For Go test fixtures, load [backend-tests.md](references/backend-tests.md).
|
||||
|
||||
Choose the right level:
|
||||
- **Unit:** pure logic or isolated service behavior.
|
||||
- **Integration:** handler/service/repository boundaries, SQLite-backed flows, filesystem behavior, or process boundaries.
|
||||
@@ -50,11 +58,27 @@ Choose the right level:
|
||||
|
||||
Prefer state/output assertions over interaction assertions. Mock only slow, nondeterministic, or external boundaries; use real implementations or fakes when they keep the test deterministic.
|
||||
|
||||
When one behavior has separate wheel, keyboard, touch, or pointer handlers,
|
||||
inventory and test every handler with distinct branching. For input at a hard
|
||||
boundary where the underlying position cannot change, dispatch the input event
|
||||
or gesture directly. Cover the eligible direction at the boundary, the wrong
|
||||
direction or off-boundary no-op, one physical gesture firing at most once, and
|
||||
listener cleanup when the change adds subscriptions. An E2E case for one input
|
||||
modality does not cover the other handler paths.
|
||||
|
||||
When a work order names an `AC-*` acceptance criterion, keep the mapping visible
|
||||
in the test name or a nearby `@covers AC-...` comment. Do not copy the complete
|
||||
requirement into the test.
|
||||
|
||||
For failure-path tests, inject the error at the boundary the production code claims to handle and exercise the real downstream call chain; do not short-circuit by mocking the handler under test.
|
||||
When production code has a defensive nil/fallback branch or sanitizes an internal error into a public error, inject that alternate return at the interface boundary and assert the public result plus absence of internal details or sensitive content.
|
||||
|
||||
For parser, canonicalization, or sanitization tests, build fixtures from the
|
||||
producer's exact serialized shape. Include boundary cases such as optional
|
||||
whitespace, adjacent records, terminators without a newline, and malformed or
|
||||
unclosed input; assert that untrusted content is removed or fails closed while
|
||||
independent following content is preserved. A test-only normalized fixture can
|
||||
pass while the real wire framing still fails.
|
||||
|
||||
### Cross-layer contracts
|
||||
|
||||
@@ -64,6 +88,13 @@ merge, reconnect/readiness handler, and consumer before editing. Add focused
|
||||
coverage at the affected boundaries, including refresh/reconnect and an update
|
||||
that omits the field, so a partial payload cannot silently discard an existing
|
||||
value.
|
||||
For MCP/API tool registration or mode-gated catalogs, add positive tests for
|
||||
intended modes and negative tests asserting tool absence in every excluded mode.
|
||||
|
||||
When content passes through multiple canonicalizers or a delayed/created-session
|
||||
path, test each handoff with distinct sentinels and assert exact equality at
|
||||
persistence and dispatch. Carry the exact value produced at acceptance through
|
||||
later stages instead of re-deriving it from mutable source data at launch.
|
||||
|
||||
### Concurrent and event-driven behavior
|
||||
|
||||
@@ -75,14 +106,50 @@ competing operation, then release. Exercise the real delivery path where
|
||||
practical, and prove the old interleaving fails before the fix. Assert both the
|
||||
winner state and the untouched replacement state, including relevant buffers,
|
||||
signals, or queue ownership. Cover stale events acting after a replacement
|
||||
operation begins, cancellation/retry ownership, and at-most-once delivery when
|
||||
they apply. Run affected Go packages with `-race`.
|
||||
operation begins, stale-owner handoffs plus same-owner invalidation/no-replay,
|
||||
cancellation/retry ownership, and at-most-once delivery when they apply. Run
|
||||
affected Go packages with `-race`.
|
||||
|
||||
When a source can deliver either a complete snapshot or a partial event, define
|
||||
the omission semantics and test both forms. Capture a request-start revision or
|
||||
epoch before every asynchronous request, inventory all callers, and use
|
||||
deferred response/event tests to prove ordering. React loading state does not
|
||||
serialize same-tick callbacks; use an immediate ref or shared in-flight promise
|
||||
when request identity must be single-flight.
|
||||
|
||||
For hooks writing shared workspace or global caches, test the ownership matrix:
|
||||
a second consumer preserves valid cached data; an initiator unmount before a
|
||||
deferred response still reaches terminal state; the latest same-key response
|
||||
wins over stale completion; and workspace/key switches remain isolated.
|
||||
|
||||
During cancellation or recovery, do not broadly suppress stream frames. Suppress
|
||||
only allowlisted cancellation acknowledgements with immutable operation identity;
|
||||
same-identity message, thinking, and tool frames remain authoritative activity.
|
||||
Add a negative regression for each activity class and a transport-boundary test
|
||||
when ordering depends on subscriber processing.
|
||||
|
||||
When delayed state has a lifecycle owner, pair the boundary tests: disposal
|
||||
before the threshold must cancel timers and emit nothing later; disposal or
|
||||
replacement after publication must immediately clear externally observable
|
||||
state; and stale callbacks must not mutate the replacement.
|
||||
|
||||
When setup can finish before hydration or restoration, test both readiness
|
||||
states. Cover state that is ready at setup and state that becomes ready after
|
||||
listener registration. Drive the readiness transition explicitly and dispose
|
||||
each added subscription.
|
||||
|
||||
When logic rejects ambiguous candidates, filter invalid candidates before the
|
||||
cardinality check. Cover zero valid candidates, one valid candidate mixed with
|
||||
invalid candidates, and multiple valid candidates.
|
||||
|
||||
For behavior described as remaining reactive, updating, or responding after
|
||||
initialization, test both the initial state and a deterministic post-initialization
|
||||
transition. An initial snapshot test cannot prove that a subscription, watcher,
|
||||
or reconciliation path remains active; trigger a state, event, or input change
|
||||
and assert the updated outcome. If no public API exposes publication or render
|
||||
counts, a controlled boundary fake may instrument those events, but retain an
|
||||
observable state assertion as the proof of behavior.
|
||||
|
||||
## Steps
|
||||
|
||||
### 1. RED — Write a failing test
|
||||
@@ -132,6 +199,9 @@ a later patch.
|
||||
**Don't test implementation details:**
|
||||
- Assert behavior, state, API response, DB row, emitted event, or UI outcome. Avoid assertions that only prove a helper was called or an internal query string happened to be built a certain way.
|
||||
- Custom hooks and async controller helpers are behavior-bearing logic, not pure React markup: add focused tests for success, failure, cancellation/no-op, and busy/loading-state cleanup.
|
||||
- When a change adds a bulk or action entry point, test that entry point directly,
|
||||
including its empty, partial, and success paths; coverage of a shared helper or
|
||||
neighboring single-item flow does not prove the new dispatch path works.
|
||||
|
||||
**Don't test mock behavior:**
|
||||
- If your assertion checks a mock element (`*-mock` test ID, mock return value), you're testing the mock, not the code. Test real behavior or don't mock it.
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
# Backend test patterns
|
||||
|
||||
Load this reference when writing or changing Go tests. Paths are relative to the repository root.
|
||||
|
||||
- **Testing:** Prefer `testing/synctest` (Go 1.24+) over `time.Sleep` for time-dependent tests. Use `synctest.Test` to wrap tests with tickers or timeouts — it advances fake time instantly when all goroutines are idle. When `synctest` is not feasible (e.g., tests spawning external processes like `git`), use channel-based synchronization (`<-started`, non-blocking `select`) instead of sleep-based waits. Reserve `time.Sleep` only for integration tests that need real subprocess execution time. For filesystem tests that inject a fixed time, set every time source in the fixture, including logical day-marker contents and active/segment file mtimes; do not rely on the host wall clock, because daily rotation can invalidate a fixture only on CI or merge-group runners.
|
||||
- **Test cleanup:** Register `t.Cleanup` immediately after creating resources that need teardown (adapters, `io.Pipe` writers, background goroutines, and every successful `sqlx.Open`) — before any `t.Fatal`/`t.Fatalf` path. Retain an explicit database `Close` before reading database files when required; late cleanup registration leaks resources on early failure.
|
||||
- **Joining production goroutines in tests:** When code spawns untracked goroutines (e.g. `fireWakeup`), don't rely on arbitrary sleeps. Join via an observable side effect — e.g. block on `EventTypeComplete` from `a.updatesCh` after unblocking the fake agent. For an API that must return before a gated or slow operation, use a goroutine plus observable channels/barriers: await the API result and fetch-start marker while the gate remains closed, then release or cancel it; use only a generous timeout as a deadlock guard, never an elapsed-time threshold such as under 100ms. Reserve multi-second waits for subprocess/integration tests only.
|
||||
- **Path/security tests:** Avoid using the real filesystem root as a fixture root. Build fake absolute roots under `t.TempDir()` with `filepath.Join`; this keeps tests portable across Windows, POSIX, and privileged cloud executors. For error assertions, do not compare raw `%q`/escaped strings with native `filepath` values; assert typed fields or normalize both sides with `filepath.Clean`/`filepath.FromSlash`. When multiple filesystem or database candidates compete, seed distinct sentinel records in each and assert the selected record plus preservation of the non-selected source; identical fixtures can let an incorrect selection pass.
|
||||
- **Filesystem permission tests:** Assert permission-denied behavior only after probing that the current executor enforces the permission bit change. Root-like Sprite executors may bypass `chmod` restrictions.
|
||||
- **Filesystem safety checks:** Carry the original `os.Lstat` `FileInfo` (or opened handle) through every decision; do not re-stat a validated path, which reopens a TOCTOU window before side effects. Test mismatches before writes or manager commands.
|
||||
- **Git indexed-environment tests:** When a test supplies `GIT_CONFIG_COUNT`, `GIT_CONFIG_KEY_n`, and `GIT_CONFIG_VALUE_n`, clear every inherited indexed key first and restore it with `t.Cleanup`. `internal/gitconfigenv` ignores indexes past the count; uncleared parent indexes silently alter the block instead of failing loudly.
|
||||
- **Derived environment contracts:** Drive derived values through the production producer/wiring seam instead of manually seeding keys; pair consumer/process coverage with a producer-boundary assertion so missing publication fails. For isolated config-discovery tests, run `env -u KANDEV_INTERNAL_CONFIG_FILE -u KANDEV_INTERNAL_CONFIG_HOME_FILE -u KANDEV_INTERNAL_AGENTCTL_STARTUP_CONFIG make -C apps/backend test`; retain the ambient integration-env job separately.
|
||||
- **GitLab host-trust tests:** `internal/gitlab`'s `TestMain` in `goleak_test.go` calls the scrub in `env_hermetic_test.go`; `internal/agentctl/server/process`'s `TestMain` clears ambient `KANDEV_GITLAB_HOST`/`GITLAB_HOST`/`GITLAB_TOKEN`. `test_ambient_env` runs both poisoned to catch a regression neither package's clean-env CI shard could catch.
|
||||
- **Full test output:** For local full-suite pass/fail validation, prefer plain `go test -race ./...`. `go test -json ./...` can emit very large JSONL streams; if a wrapper or tracing tool truncates the stream mid-record, downstream JSON parsing may fail even when Go tests passed. Use JSON output mainly for CI artifacts or test-report tooling that explicitly requires it.
|
||||
- **Private executable helpers:** When library code starts `os.Executable()` in a private helper mode, do not rely only on a command package's dispatch or package-specific `TestMain`: an importing package's test binary can be the executable and recursively run tests.
|
||||
Use either an explicitly marked private env-and-argument trampoline before normal test entry, or inject a dedicated helper executable. Cover it from a different importing package, not only the helper package.
|
||||
@@ -12,7 +12,9 @@ Use this as the routing map for Kandev's local skills. Prefer the repo's existin
|
||||
```text
|
||||
Task arrives
|
||||
|
|
||||
|-- Need to clarify intent first? ----------> /interview-me
|
||||
|-- Standalone interview or stress test? --> /interview-me
|
||||
|-- Feature/fix planning assumptions? -----> /interview-me (question only material unknowns)
|
||||
|-- Large initiative with dependent unknowns? -> /interview-me decision-mapping reference
|
||||
|-- Create/change/fix/publish Kandev plugin? -> /create-kandev-plugin plus /fix or /tdd as needed
|
||||
|-- New feature or behavior-changing fix? --> /spec-driven-development
|
||||
|-- Bug regression? ------------------------> /fix -> requirement/design check -> fix plan/work orders -> /tdd
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
name = "pr-poller"
|
||||
description = "Read-only GPT-5.3 Codex Spark monitor for a user-authorized wait on PR CI or review updates."
|
||||
model = "gpt-5.3-codex-spark"
|
||||
model_reasoning_effort = "low"
|
||||
description = "Read-only GPT-5.6 Luna monitor for a user-authorized wait on PR CI or review updates."
|
||||
model = "gpt-5.6-luna"
|
||||
model_reasoning_effort = "medium"
|
||||
sandbox_mode = "read-only"
|
||||
developer_instructions = """
|
||||
Poll one named GitHub PR and return a compact status report to the primary
|
||||
|
||||
@@ -17,11 +17,29 @@ Apply this guidance whenever editing `.github/**`.
|
||||
- PR label/metadata cleanup jobs that operate on pull requests must declare
|
||||
`pull-requests: write`, not `issues: write`; mirror the permission shape used
|
||||
by `preview-env.yml`.
|
||||
- **Workflow and checkout identity:** A `workflow_dispatch` uses the workflow
|
||||
definition at the selected dispatch ref. An explicit `checkout` of `main`
|
||||
selects the files that subsequent steps consume. It does not replace the
|
||||
running workflow definition. Record `github.workflow_ref`,
|
||||
`github.workflow_sha`, and the checkout SHA separately. Attribute failures
|
||||
to the definition or consumed files before deciding which branch needs a fix.
|
||||
- Release workflow changes must trace prepare/summary outputs and conditions
|
||||
through every build and publication job: skip decisions must propagate
|
||||
without accidental publishing, while backfill must reuse and validate the
|
||||
existing tag and still run its required build/publication path. Verify each
|
||||
actual publication job and artifact, not only the aggregate workflow result.
|
||||
- When validating a public artifact through redirects, inspect the final HTTP
|
||||
status, content type, effective URL, and non-empty body. Intermediate
|
||||
redirect headers do not prove that the delivered artifact is valid.
|
||||
- For workflow security changes, run the relevant raw workflow-contract tests,
|
||||
`python3 .github/scripts/lint-action-pinning_test.py`, `zizmor .github/workflows`,
|
||||
and `git diff --check`.
|
||||
- The `pr-docs.yml` workflow is a base-controlled, metadata-only check. It reads
|
||||
pull-request files through the bounded `.github/scripts/pr-docs.cjs` adapter;
|
||||
it must never check out or execute a pull-request head.
|
||||
- Its `PR documentation coverage` status is revision-specific. The exact
|
||||
`no-docs-allow` label is the only policy override, and merge-group evaluation
|
||||
must resolve and validate every member independently against the group's
|
||||
entry boundaries. All event types share a non-cancelling target-branch lock
|
||||
with `queue: max` so queued label reevaluations cannot race with merge-group
|
||||
status writes or replace the single pending run.
|
||||
|
||||
@@ -32,6 +32,7 @@ FROM mcr.microsoft.com/playwright:v1.61.1-noble AS runtime
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive \
|
||||
PNPM_HOME=/pnpm \
|
||||
COREPACK_HOME=/root/.cache/node/corepack \
|
||||
PATH=/pnpm:$PATH
|
||||
|
||||
# --- System packages -------------------------------------------------------
|
||||
|
||||
@@ -35,6 +35,13 @@ DIAGRAM (optional)
|
||||
POSSIBLE IMPROVEMENTS (optional)
|
||||
One line: risk level + what could go wrong or be improved. Skip if negligible.
|
||||
|
||||
DOCUMENTATION COVERAGE
|
||||
The `PR documentation coverage` status uses a deterministic path and artifact
|
||||
check. Runtime, workflow, configuration, and other non-exempt changes need a
|
||||
changed work order linked to its plan, requirements, acceptance criteria, and
|
||||
system design. The exact `no-docs-allow` label is a maintainer exception. The
|
||||
check does not claim semantic completeness or require planning before coding.
|
||||
|
||||
RELATED ISSUES
|
||||
Use "Closes #N" if this resolves an issue. Remove the line if there is no related issue.
|
||||
|
||||
|
||||
@@ -31,6 +31,16 @@ def workflow_step(workflow: str, name: str) -> str:
|
||||
return remainder.partition("\n - name:")[0]
|
||||
|
||||
|
||||
def job_block(workflow: str, job: str, next_job: str | None) -> str:
|
||||
marker = f" {job}:\n"
|
||||
_, separator, remainder = workflow.partition(marker)
|
||||
if not separator:
|
||||
raise AssertionError(f"Workflow has no {job} job")
|
||||
if next_job is None:
|
||||
return remainder
|
||||
return remainder.partition(f"\n {next_job}:\n")[0]
|
||||
|
||||
|
||||
class ClaudeCodeReviewWorkflowContractTest(unittest.TestCase):
|
||||
def test_review_workflow_ignores_pr_updates(self) -> None:
|
||||
workflow = WORKFLOW.read_text(encoding="utf-8")
|
||||
@@ -67,6 +77,27 @@ class ClaudeCodeReviewWorkflowContractTest(unittest.TestCase):
|
||||
workflow,
|
||||
)
|
||||
|
||||
def test_claude_execution_jobs_have_a_thirty_minute_budget(self) -> None:
|
||||
review_workflow = WORKFLOW.read_text(encoding="utf-8")
|
||||
for job, next_job in (
|
||||
("claude-review-same-repo", "label-allowlisted-fork"),
|
||||
("claude-review-fork", None),
|
||||
):
|
||||
block = job_block(review_workflow, job, next_job)
|
||||
self.assertRegex(
|
||||
block,
|
||||
r"runs-on: ubuntu-latest\n timeout-minutes: 30\n",
|
||||
f"{job} must bound Claude execution at 30 minutes",
|
||||
)
|
||||
|
||||
mention_workflow = MENTION_WORKFLOW.read_text(encoding="utf-8")
|
||||
block = job_block(mention_workflow, "claude", None)
|
||||
self.assertRegex(
|
||||
block,
|
||||
r"runs-on: ubuntu-latest\n timeout-minutes: 30\n",
|
||||
"the shared interactive Claude job must bound execution at 30 minutes",
|
||||
)
|
||||
|
||||
def test_manual_pr_review_does_not_checkout_untrusted_content(self) -> None:
|
||||
workflow = MENTION_WORKFLOW.read_text(encoding="utf-8")
|
||||
|
||||
|
||||
@@ -13,6 +13,8 @@ REPO_ROOT = Path(__file__).resolve().parents[2]
|
||||
DOCKERFILE = REPO_ROOT / ".github" / "docker" / "ci-base" / "Dockerfile"
|
||||
IMAGE_WORKFLOW = REPO_ROOT / ".github" / "workflows" / "ci-base-image.yml"
|
||||
E2E_WORKFLOW = REPO_ROOT / ".github" / "workflows" / "e2e-tests.yml"
|
||||
BACKEND_WORKFLOW = REPO_ROOT / ".github" / "workflows" / "backend-tests.yml"
|
||||
FRONTEND_WORKFLOW = REPO_ROOT / ".github" / "workflows" / "frontend-tests.yml"
|
||||
LINT_WORKFLOW = REPO_ROOT / ".github" / "workflows" / "lint-action-pinning.yml"
|
||||
IMAGE_DIGEST_RESOLVER = REPO_ROOT / ".github" / "scripts" / "resolve-image-digest.sh"
|
||||
VALID_IMAGE_INDEX = (
|
||||
@@ -154,6 +156,18 @@ cat "${FAKE_DOCKER_MANIFEST}"
|
||||
self.assertIn("type=gha,scope=runtime", workflow)
|
||||
self.assertIn("desktop-latest", workflow)
|
||||
|
||||
def test_container_jobs_use_the_baked_corepack_cache(self) -> None:
|
||||
dockerfile = DOCKERFILE.read_text(encoding="utf-8")
|
||||
|
||||
self.assertIn("COREPACK_HOME=/root/.cache/node/corepack", dockerfile)
|
||||
for workflow_path in (E2E_WORKFLOW, BACKEND_WORKFLOW, FRONTEND_WORKFLOW):
|
||||
workflow = workflow_path.read_text(encoding="utf-8")
|
||||
self.assertIn(
|
||||
"COREPACK_HOME: /root/.cache/node/corepack",
|
||||
workflow,
|
||||
workflow_path.name,
|
||||
)
|
||||
|
||||
def test_desktop_job_uses_image_without_live_bootstrap_downloads(self) -> None:
|
||||
workflow = E2E_WORKFLOW.read_text(encoding="utf-8")
|
||||
desktop_job = job_block(workflow, "desktop-e2e", "e2e-report")
|
||||
|
||||
@@ -107,7 +107,7 @@ class ExternalRunnerWorkflowContractTest(unittest.TestCase):
|
||||
self.assertIn("runs-on: ubuntu-latest" if job == "postgres-boot" else "runs-on: windows-latest", protected)
|
||||
self.assertNotIn("runner_plan", protected)
|
||||
|
||||
def test_frontend_uses_planner_for_test_and_gate(self) -> None:
|
||||
def test_frontend_uses_planner_for_unsharded_tests_and_gate(self) -> None:
|
||||
workflow = self.assert_planner(
|
||||
"frontend-tests.yml",
|
||||
"frontend",
|
||||
@@ -121,6 +121,9 @@ class ExternalRunnerWorkflowContractTest(unittest.TestCase):
|
||||
"runs-on: ${{ fromJSON(needs.runner_plan.outputs.plan).frontend_runner }}",
|
||||
job_block(workflow, "frontend", "frontend-gate"),
|
||||
)
|
||||
frontend = job_block(workflow, "frontend", "frontend-gate")
|
||||
self.assertNotIn("matrix:", frontend)
|
||||
self.assertNotIn("frontend_tests", workflow)
|
||||
self.assertIn(
|
||||
"runs-on: ${{ fromJSON(needs.runner_plan.outputs.plan).frontend_gate_runner }}",
|
||||
job_block(workflow, "frontend-gate", None),
|
||||
|
||||
@@ -40,6 +40,22 @@ def run_tests_step(workflow: str) -> str:
|
||||
return remainder.partition(NEXT_STEP_MARKER)[0]
|
||||
|
||||
|
||||
def frontend_job(workflow: str) -> str:
|
||||
"""Return the frontend job body, up to the required gate."""
|
||||
_, separator, remainder = workflow.partition(" frontend:\n")
|
||||
if not separator:
|
||||
raise AssertionError("frontend-tests.yml has no frontend job")
|
||||
return remainder.partition("\n frontend-gate:")[0]
|
||||
|
||||
|
||||
def frontend_gate_job(workflow: str) -> str:
|
||||
"""Return the required frontend gate body."""
|
||||
_, separator, remainder = workflow.partition(" frontend-gate:\n")
|
||||
if not separator:
|
||||
raise AssertionError("frontend-tests.yml has no frontend-gate job")
|
||||
return remainder
|
||||
|
||||
|
||||
def trigger_block(workflow: str, trigger: str) -> str:
|
||||
"""Return `trigger`'s block under `on:`, up to the next top-level key."""
|
||||
block = re.search(rf"(?m)^ {trigger}:\n(?:^ {{4}}.*\n|^\n)*", workflow)
|
||||
@@ -49,6 +65,78 @@ def trigger_block(workflow: str, trigger: str) -> str:
|
||||
|
||||
|
||||
class FrontendTestsWorkflowContractTest(unittest.TestCase):
|
||||
def test_cache_resolves_the_container_pnpm_store_before_restore(self) -> None:
|
||||
workflow = WORKFLOW.read_text(encoding="utf-8")
|
||||
job = frontend_job(workflow)
|
||||
resolve_index = job.find("- name: Resolve pnpm store path")
|
||||
cache_index = job.find("- name: Cache pnpm store")
|
||||
install_index = job.find("- name: Install dependencies")
|
||||
|
||||
self.assertGreaterEqual(resolve_index, 0, "store resolution step is missing")
|
||||
self.assertGreaterEqual(cache_index, 0, "cache step is missing")
|
||||
self.assertGreaterEqual(install_index, 0, "dependency installation step is missing")
|
||||
self.assertLess(resolve_index, cache_index)
|
||||
self.assertLess(cache_index, install_index)
|
||||
resolve_step = job[resolve_index:cache_index]
|
||||
cache_step = job[cache_index:install_index]
|
||||
|
||||
self.assertIn("id: pnpm-store", resolve_step)
|
||||
self.assertIn("pnpm store path --silent", resolve_step)
|
||||
self.assertIn('pnpm --version', resolve_step)
|
||||
self.assertIn('mkdir -p "${STORE_PATH}"', resolve_step)
|
||||
self.assertIn('printf \'path=%s\\n\' "${STORE_PATH}"', resolve_step)
|
||||
self.assertIn("path: ${{ steps.pnpm-store.outputs.path }}", cache_step)
|
||||
self.assertIn("runner.os", cache_step)
|
||||
self.assertIn("runner.arch", cache_step)
|
||||
self.assertIn("steps.pnpm-store.outputs.version", cache_step)
|
||||
self.assertIn("hashFiles('apps/pnpm-lock.yaml')", cache_step)
|
||||
self.assertIn(
|
||||
"restore-keys: pnpm-${{ runner.os }}-${{ runner.arch }}-${{ steps.pnpm-store.outputs.version }}-",
|
||||
cache_step,
|
||||
)
|
||||
self.assertIn("continue-on-error: true", cache_step)
|
||||
|
||||
def test_cache_failure_does_not_replace_frozen_installation(self) -> None:
|
||||
workflow = WORKFLOW.read_text(encoding="utf-8")
|
||||
job = frontend_job(workflow)
|
||||
cache_index = job.index("- name: Cache pnpm store")
|
||||
install_index = job.index("- name: Install dependencies")
|
||||
install_step = job[install_index:].partition(NEXT_STEP_MARKER)[0]
|
||||
|
||||
self.assertLess(cache_index, install_index)
|
||||
self.assertIn("pnpm install --frozen-lockfile", install_step)
|
||||
|
||||
def test_frontend_job_keeps_unsharded_unit_tests_and_static_checks(self) -> None:
|
||||
workflow = WORKFLOW.read_text(encoding="utf-8")
|
||||
job = frontend_job(workflow)
|
||||
|
||||
self.assertIn("- name: Run tests\n", job)
|
||||
self.assertIn("NODE_ENV: production", job)
|
||||
self.assertIn("pnpm --filter @kandev/web test", job)
|
||||
self.assertIn("- name: Run lint\n", job)
|
||||
self.assertIn("- name: Run typecheck\n", job)
|
||||
self.assertIn("- name: Build\n", job)
|
||||
self.assertNotIn("FRONTEND_TEST_SHARD", job)
|
||||
self.assertNotIn("matrix:", job)
|
||||
|
||||
def test_frontend_tests_remain_outside_the_planner_until_adoption_evidence(self) -> None:
|
||||
workflow = WORKFLOW.read_text(encoding="utf-8")
|
||||
planner = workflow.partition(" changes:\n")[0]
|
||||
|
||||
self.assertNotIn('"name":"frontend_tests"', planner)
|
||||
self.assertNotIn(" frontend_tests:\n", workflow)
|
||||
|
||||
def test_required_gate_requires_every_frontend_verification_result(self) -> None:
|
||||
workflow = WORKFLOW.read_text(encoding="utf-8")
|
||||
gate = frontend_gate_job(workflow)
|
||||
|
||||
self.assertIn(
|
||||
"needs: [changes, frontend, runner_plan]",
|
||||
gate,
|
||||
)
|
||||
self.assertNotIn("FRONTEND_TESTS_RESULT:", gate)
|
||||
self.assertNotIn("Frontend test matrix finished with result:", gate)
|
||||
|
||||
def test_test_step_reproduces_the_production_environment(self) -> None:
|
||||
step = run_tests_step(WORKFLOW.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
@@ -6,6 +6,8 @@ Usage:
|
||||
python3 .github/scripts/lint-harness-files.py AGENTS.md .agents/skills/fix/SKILL.md
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import re
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Contract tests for the pull request documentation coverage workflow."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
import re
|
||||
import unittest
|
||||
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[2]
|
||||
WORKFLOW = REPO_ROOT / ".github" / "workflows" / "pr-docs.yml"
|
||||
LINT_WORKFLOW = REPO_ROOT / ".github" / "workflows" / "lint-action-pinning.yml"
|
||||
|
||||
|
||||
class PullRequestDocumentationWorkflowContractTest(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.assertTrue(WORKFLOW.is_file(), "Documentation coverage workflow is missing")
|
||||
self.workflow = WORKFLOW.read_text(encoding="utf-8")
|
||||
|
||||
# @covers AC-CI-PR-DOCS-001.1, AC-CI-PR-DOCS-002.2
|
||||
def test_runs_for_all_pr_and_manual_retry_events(self) -> None:
|
||||
trigger = self.workflow.partition("on:\n")[2].partition("\nconcurrency:")[0]
|
||||
self.assertIn("pull_request_target:", trigger)
|
||||
for event in (
|
||||
"opened",
|
||||
"reopened",
|
||||
"synchronize",
|
||||
"edited",
|
||||
"labeled",
|
||||
"unlabeled",
|
||||
"ready_for_review",
|
||||
):
|
||||
self.assertIn(event, trigger)
|
||||
self.assertIn("merge_group:", trigger)
|
||||
self.assertIn("checks_requested", trigger)
|
||||
self.assertIn("workflow_dispatch:", trigger)
|
||||
self.assertIn("pr_number:", trigger)
|
||||
self.assertNotIn("paths:", trigger)
|
||||
|
||||
# @covers AC-CI-PR-DOCS-003.1, AC-CI-PR-DOCS-003.4
|
||||
def test_serializes_prs_without_cancelling_merge_group_runs(self) -> None:
|
||||
self.assertIn("group: >-", self.workflow)
|
||||
self.assertIn("github.event.merge_group.base_ref", self.workflow)
|
||||
self.assertIn("github.event.pull_request.base.ref", self.workflow)
|
||||
self.assertIn("queue: max", self.workflow)
|
||||
self.assertIn("cancel-in-progress: false", self.workflow)
|
||||
|
||||
# @covers AC-CI-PR-DOCS-003.3
|
||||
def test_uses_trusted_least_privilege_checkout(self) -> None:
|
||||
permissions = self.workflow.partition("permissions:\n")[2].partition("\njobs:")[0]
|
||||
self.assertEqual(
|
||||
permissions.strip(),
|
||||
"contents: read\n pull-requests: read\n statuses: write",
|
||||
)
|
||||
self.assertRegex(
|
||||
self.workflow,
|
||||
r"uses: actions/checkout@[0-9a-f]{40} # v[0-9]+",
|
||||
)
|
||||
self.assertIn("persist-credentials: false", self.workflow)
|
||||
self.assertIn("github.event.merge_group.base_sha", self.workflow)
|
||||
self.assertNotIn("github.event.pull_request.head.sha", self.workflow)
|
||||
self.assertIn("ref: ${{ env.CHECKOUT_REF }}", self.workflow)
|
||||
self.assertNotIn("pull_request:\n", self.workflow)
|
||||
self.assertNotIn("pnpm install", self.workflow)
|
||||
self.assertNotIn("npm install", self.workflow)
|
||||
self.assertIn("node .github/scripts/pr-docs.cjs", self.workflow)
|
||||
self.assertIn("name: Publish PR documentation coverage status", self.workflow)
|
||||
self.assertNotIn("name: PR documentation coverage\n", self.workflow)
|
||||
|
||||
# @covers AC-CI-PR-DOCS-001.1, AC-CI-PR-DOCS-002.1, AC-CI-PR-DOCS-003.2
|
||||
def test_dispatch_is_restricted_to_the_default_branch_and_script_owns_statuses(self) -> None:
|
||||
self.assertRegex(
|
||||
self.workflow,
|
||||
re.compile(
|
||||
r"github\.event_name != 'workflow_dispatch'.*github\.ref.*default_branch",
|
||||
re.DOTALL,
|
||||
),
|
||||
)
|
||||
self.assertIn("PR documentation coverage", (REPO_ROOT / ".github" / "scripts" / "pr-docs.cjs").read_text(encoding="utf-8"))
|
||||
self.assertIn("statuses: write", self.workflow)
|
||||
|
||||
def test_contract_is_registered_in_the_required_lint_workflow(self) -> None:
|
||||
lint_workflow = LINT_WORKFLOW.read_text(encoding="utf-8")
|
||||
self.assertRegex(
|
||||
lint_workflow,
|
||||
r"(?m)^ - name: Test pull request documentation workflow contract$\n"
|
||||
r"^ run: python3 .github/scripts/pr-docs-workflow-contract_test.py$",
|
||||
)
|
||||
self.assertRegex(
|
||||
lint_workflow,
|
||||
r"(?m)^ - name: Test pull request documentation validator$\n"
|
||||
r"^ run: node --test .github/scripts/pr-docs.test.cjs$",
|
||||
)
|
||||
makefile = (REPO_ROOT / "Makefile").read_text(encoding="utf-8")
|
||||
self.assertIn("@node --test .github/scripts/pr-docs.test.cjs", makefile)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,407 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Contract tests for the pull request size label workflow."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[2]
|
||||
WORKFLOW = REPO_ROOT / ".github" / "workflows" / "pr-size-label.yml"
|
||||
LINT_WORKFLOW = REPO_ROOT / ".github" / "workflows" / "lint-action-pinning.yml"
|
||||
|
||||
|
||||
def extract_function(source: str, name: str) -> str:
|
||||
"""Return one complete JavaScript function from the trusted workflow."""
|
||||
|
||||
marker = f"function {name}("
|
||||
start = source.find(marker)
|
||||
if start < 0:
|
||||
raise AssertionError(f"{name} is missing")
|
||||
|
||||
body_start = source.find("{", start)
|
||||
if body_start < 0:
|
||||
raise AssertionError(f"{name} has no body")
|
||||
|
||||
depth = 0
|
||||
quote: str | None = None
|
||||
escaped = False
|
||||
for index in range(body_start, len(source)):
|
||||
character = source[index]
|
||||
if quote is not None:
|
||||
if escaped:
|
||||
escaped = False
|
||||
elif character == "\\":
|
||||
escaped = True
|
||||
elif character == quote:
|
||||
quote = None
|
||||
continue
|
||||
|
||||
if character in {"'", '"', "`"}:
|
||||
quote = character
|
||||
elif character == "{":
|
||||
depth += 1
|
||||
elif character == "}":
|
||||
depth -= 1
|
||||
if depth == 0:
|
||||
return source[start : index + 1]
|
||||
|
||||
raise AssertionError(f"{name} has an incomplete body")
|
||||
|
||||
|
||||
def extract_call_argument(source: str, call: str) -> str:
|
||||
"""Return one balanced array/object expression passed to a call."""
|
||||
|
||||
call_start = source.find(call)
|
||||
if call_start < 0:
|
||||
raise AssertionError(f"{call} is missing")
|
||||
|
||||
expression_start = call_start + len(call)
|
||||
while expression_start < len(source) and source[expression_start].isspace():
|
||||
expression_start += 1
|
||||
|
||||
opening = source[expression_start]
|
||||
closing = {"[": "]", "{": "}"}.get(opening)
|
||||
if closing is None:
|
||||
raise AssertionError(f"{call} has no array/object argument")
|
||||
|
||||
depth = 0
|
||||
quote: str | None = None
|
||||
escaped = False
|
||||
for index in range(expression_start, len(source)):
|
||||
character = source[index]
|
||||
if quote is not None:
|
||||
if escaped:
|
||||
escaped = False
|
||||
elif character == "\\":
|
||||
escaped = True
|
||||
elif character == quote:
|
||||
quote = None
|
||||
continue
|
||||
|
||||
if character in {"'", '"', "`"}:
|
||||
quote = character
|
||||
elif character == opening:
|
||||
depth += 1
|
||||
elif character == closing:
|
||||
depth -= 1
|
||||
if depth == 0:
|
||||
return source[expression_start : index + 1]
|
||||
|
||||
raise AssertionError(f"{call} has an incomplete argument")
|
||||
|
||||
|
||||
def run_javascript(function_source: str, function_name: str, values: list[object]) -> object:
|
||||
"""Execute a pure workflow helper without loading the GitHub runtime."""
|
||||
|
||||
script = (
|
||||
f"{function_source}\n"
|
||||
f"process.stdout.write(JSON.stringify({function_name}(...JSON.parse(process.argv[1]))));"
|
||||
)
|
||||
result = subprocess.run(
|
||||
["node", "-e", script, json.dumps(values)],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
raise AssertionError(result.stderr or result.stdout)
|
||||
return json.loads(result.stdout)
|
||||
|
||||
|
||||
def run_javascript_for_each(
|
||||
function_source: str, function_name: str, values: list[object]
|
||||
) -> object:
|
||||
"""Execute a pure workflow helper once for every value."""
|
||||
|
||||
script = (
|
||||
f"{function_source}\n"
|
||||
"const values = JSON.parse(process.argv[1]);"
|
||||
f"process.stdout.write(JSON.stringify(values.map(value => {function_name}(value))));"
|
||||
)
|
||||
result = subprocess.run(
|
||||
["node", "-e", script, json.dumps(values)],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
raise AssertionError(result.stderr or result.stdout)
|
||||
return json.loads(result.stdout)
|
||||
|
||||
|
||||
class PullRequestSizeLabelWorkflowContractTest(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.assertTrue(WORKFLOW.is_file(), "Pull request size workflow is missing")
|
||||
self.workflow = WORKFLOW.read_text(encoding="utf-8")
|
||||
|
||||
# @covers AC-CI-PR-SIZE-001.1, AC-CI-PR-SIZE-001.9
|
||||
def test_recalculates_on_current_pull_request_events_and_serializes_by_number(self) -> None:
|
||||
trigger = self.workflow.partition("on:\n")[2].partition("\nconcurrency:")[0]
|
||||
self.assertEqual(
|
||||
trigger,
|
||||
" pull_request_target: # zizmor: ignore[dangerous-triggers] "
|
||||
"base-controlled metadata-only workflow\n"
|
||||
" types: [opened, reopened, synchronize]\n",
|
||||
)
|
||||
self.assertRegex(
|
||||
self.workflow,
|
||||
r"group: pr-size-label-\$\{\{ github\.event\.pull_request\.number \}\}",
|
||||
)
|
||||
self.assertIn("cancel-in-progress: false", self.workflow)
|
||||
|
||||
# @covers AC-CI-PR-SIZE-001.2
|
||||
def test_classifier_assigns_boundaries_and_confirmed_examples(self) -> None:
|
||||
classifier = extract_function(self.workflow, "classifySize")
|
||||
expected_boundaries = {
|
||||
0: "small",
|
||||
10: "small",
|
||||
11: "medium",
|
||||
50: "medium",
|
||||
51: "big",
|
||||
}
|
||||
for count, expected in expected_boundaries.items():
|
||||
with self.subTest(count=count):
|
||||
self.assertEqual(run_javascript(classifier, "classifySize", [count]), expected)
|
||||
|
||||
examples = {
|
||||
"small": (2, 7),
|
||||
"medium": (15, 17, 41),
|
||||
"big": (80, 96),
|
||||
}
|
||||
for expected, counts in examples.items():
|
||||
for count in counts:
|
||||
with self.subTest(expected=expected, count=count):
|
||||
self.assertEqual(run_javascript(classifier, "classifySize", [count]), expected)
|
||||
|
||||
# @covers AC-CI-PR-SIZE-001.3, AC-CI-PR-SIZE-001.4
|
||||
def test_file_classifier_counts_application_and_translation_paths_only(self) -> None:
|
||||
classifier = extract_function(self.workflow, "isCountedFile")
|
||||
counted = [
|
||||
"apps/backend/internal/workflow/workflow.go",
|
||||
"apps/backend/internal/workflow/workflow_test.go",
|
||||
"apps/backend/internal/common/scripts/scripts.go",
|
||||
"apps/web/src/components/task/task.tsx",
|
||||
"apps/web/src/vite-preload-recovery.ts",
|
||||
"apps/web/src/vite-preload-recovery.test.ts",
|
||||
"apps/web/vitest-environment.test.tsx",
|
||||
"apps/web/vitest-monaco-editor.test.ts",
|
||||
"apps/web/src/locales/en/common.json",
|
||||
"apps/web/src/locales/zh-cn/common.json",
|
||||
"apps/desktop/src-tauri/src/main.rs",
|
||||
]
|
||||
excluded = [
|
||||
"docs/specs/ci/requirements/pull-request-size-labels.md",
|
||||
".github/workflows/pr-size-label.yml",
|
||||
"scripts/pr-size-label.py",
|
||||
"apps/web/src/assets/icon.svg",
|
||||
"apps/web/src/assets/compiled.ts",
|
||||
"apps/web/lib/assets/icons.ts",
|
||||
"apps/web/public/style.css",
|
||||
"apps/backend/internal/notifications/providers/assets/icon.ts",
|
||||
"apps/desktop/src-tauri/icons/generated.ts",
|
||||
"apps/web/src/locales/en/common.yaml",
|
||||
"apps/web/node_modules/example/index.ts",
|
||||
"apps/backend/internal/webapp/embedded/generated/model.go",
|
||||
"apps/web/generated/model.ts",
|
||||
"apps/backend/scripts/check.go",
|
||||
"apps/backend/internal/agentctl/server/api/scripts/inspector.js",
|
||||
"apps/web/e2e/scripts/check.ts",
|
||||
"apps/web/scripts/check.ts",
|
||||
"apps/backend/cmd/sqlguard/check.go",
|
||||
"apps/backend/internal/db/sqlguard/check.go",
|
||||
"apps/web/eslint-rules/no-literal.ts",
|
||||
"apps/commitlint.config.mjs",
|
||||
"apps/prettier.config.mjs",
|
||||
"apps/web/eslint.config.mjs",
|
||||
"apps/web/e2e/playwright.config.ts",
|
||||
"apps/web/postcss.config.mjs",
|
||||
"apps/web/tailwind.config.ts",
|
||||
]
|
||||
|
||||
self.assertEqual(
|
||||
run_javascript_for_each(classifier, "isCountedFile", counted),
|
||||
[True] * len(counted),
|
||||
)
|
||||
self.assertEqual(
|
||||
run_javascript_for_each(classifier, "isCountedFile", excluded),
|
||||
[False] * len(excluded),
|
||||
)
|
||||
|
||||
# @covers AC-CI-PR-SIZE-001.3, AC-CI-PR-SIZE-001.4
|
||||
def test_file_filter_is_explicit_and_normalizes_filenames(self) -> None:
|
||||
self.assertIn("typeof filename !== 'string'", self.workflow)
|
||||
self.assertIn("filename.startsWith('apps/')", self.workflow)
|
||||
self.assertIn("filename.split('/')", self.workflow)
|
||||
self.assertIn("pathSegments.includes('node_modules')", self.workflow)
|
||||
for directory in (
|
||||
"apps/backend/scripts/",
|
||||
"apps/backend/internal/agentctl/server/api/scripts/",
|
||||
"apps/backend/internal/webapp/embedded/generated/",
|
||||
"apps/web/e2e/scripts/",
|
||||
"apps/web/generated/",
|
||||
"apps/web/scripts/",
|
||||
):
|
||||
self.assertIn(directory, self.workflow)
|
||||
for directory in (
|
||||
"apps/backend/internal/notifications/providers/assets/",
|
||||
"apps/desktop/src-tauri/icons/",
|
||||
"apps/web/lib/assets/",
|
||||
"apps/web/public/",
|
||||
"apps/web/src/assets/",
|
||||
):
|
||||
self.assertIn(directory, self.workflow)
|
||||
for extension in (
|
||||
"c",
|
||||
"cc",
|
||||
"cpp",
|
||||
"css",
|
||||
"go",
|
||||
"graphql",
|
||||
"gql",
|
||||
"h",
|
||||
"html",
|
||||
"js",
|
||||
"jsx",
|
||||
"mjs",
|
||||
"mts",
|
||||
"proto",
|
||||
"rs",
|
||||
"scss",
|
||||
"sql",
|
||||
"ts",
|
||||
"tsx",
|
||||
):
|
||||
self.assertRegex(self.workflow, rf"['\"]{extension}['\"]")
|
||||
self.assertIn(r"apps\/web\/src\/locales\/[^/]+\/[^/]+\.json", self.workflow)
|
||||
|
||||
# @covers AC-CI-PR-SIZE-001.5, AC-CI-PR-SIZE-001.6
|
||||
def test_label_definitions_and_convergence_preserve_unrelated_labels(self) -> None:
|
||||
self.assertIn("const sizeLabelDefinitions =", self.workflow)
|
||||
for name, color in (
|
||||
("small", "0E8A16"),
|
||||
("medium", "FBCA04"),
|
||||
("big", "D93F0B"),
|
||||
):
|
||||
self.assertRegex(self.workflow, rf"{name}: \{{")
|
||||
self.assertIn(f"color: '{color}'", self.workflow)
|
||||
self.assertIn("github.rest.issues.getLabel", self.workflow)
|
||||
self.assertIn("github.rest.issues.createLabel", self.workflow)
|
||||
self.assertIn("github.rest.issues.listLabelsOnIssue", self.workflow)
|
||||
self.assertIn("github.rest.issues.addLabels", self.workflow)
|
||||
self.assertIn("github.rest.issues.removeLabel", self.workflow)
|
||||
|
||||
stale_labels = extract_function(self.workflow, "staleSizeLabels")
|
||||
self.assertEqual(
|
||||
run_javascript(stale_labels, "staleSizeLabels", [["bug", "medium", "big"], "small"]),
|
||||
["medium", "big"],
|
||||
)
|
||||
self.assertEqual(
|
||||
run_javascript(stale_labels, "staleSizeLabels", [["bug", "small"], "small"]),
|
||||
[],
|
||||
)
|
||||
|
||||
add_index = self.workflow.index("github.rest.issues.addLabels")
|
||||
remove_index = self.workflow.index("github.rest.issues.removeLabel")
|
||||
self.assertLess(add_index, remove_index)
|
||||
|
||||
already_exists = extract_function(self.workflow, "isAlreadyExistingLabelError")
|
||||
self.assertTrue(
|
||||
run_javascript(
|
||||
already_exists,
|
||||
"isAlreadyExistingLabelError",
|
||||
[
|
||||
{
|
||||
"status": 422,
|
||||
"response": {
|
||||
"data": {"errors": [{"code": "already_exists"}]}
|
||||
},
|
||||
}
|
||||
],
|
||||
)
|
||||
)
|
||||
self.assertFalse(
|
||||
run_javascript(
|
||||
already_exists,
|
||||
"isAlreadyExistingLabelError",
|
||||
[{"status": 422, "response": {"data": {"errors": [{"code": "invalid"}]}}}],
|
||||
)
|
||||
)
|
||||
create_index = self.workflow.index("github.rest.issues.createLabel")
|
||||
retry_read_index = self.workflow.index(
|
||||
"await github.rest.issues.getLabel", create_index
|
||||
)
|
||||
self.assertLess(create_index, retry_read_index)
|
||||
|
||||
# @covers AC-CI-PR-SIZE-001.5
|
||||
def test_summary_row_contains_two_string_cells(self) -> None:
|
||||
summary_row = extract_function(self.workflow, "summaryTableRow")
|
||||
self.assertEqual(
|
||||
run_javascript(summary_row, "summaryTableRow", [4, "small"]),
|
||||
["4", "small"],
|
||||
)
|
||||
table_expression = extract_call_argument(self.workflow, "core.summary.addTable(")
|
||||
script = (
|
||||
f"{summary_row}\n"
|
||||
"const countedFileCount = 4;\n"
|
||||
"const targetLabel = 'small';\n"
|
||||
f"const table = {table_expression};\n"
|
||||
"process.stdout.write(JSON.stringify(table));"
|
||||
)
|
||||
result = subprocess.run(
|
||||
["node", "-e", script],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
raise AssertionError(result.stderr or result.stdout)
|
||||
table = json.loads(result.stdout)
|
||||
self.assertEqual(table[1], ["4", "small"])
|
||||
self.assertTrue(all(isinstance(cell, str) for cell in table[1]))
|
||||
|
||||
# @covers AC-CI-PR-SIZE-001.7
|
||||
def test_incomplete_file_results_fail_before_label_mutations(self) -> None:
|
||||
self.assertIn("github.paginate(", self.workflow)
|
||||
self.assertIn("github.rest.pulls.listFiles", self.workflow)
|
||||
self.assertIn("per_page: 100", self.workflow)
|
||||
self.assertIn("pull_number: context.payload.pull_request.number", self.workflow)
|
||||
limit_index = self.workflow.index("changedFiles.length >= 3000")
|
||||
self.assertIn("throw new Error", self.workflow[limit_index : limit_index + 240])
|
||||
for mutation in (
|
||||
"github.rest.issues.getLabel",
|
||||
"github.rest.issues.createLabel",
|
||||
"github.rest.issues.addLabels",
|
||||
"github.rest.issues.removeLabel",
|
||||
):
|
||||
self.assertLess(limit_index, self.workflow.index(mutation))
|
||||
|
||||
# @covers AC-CI-PR-SIZE-001.8
|
||||
def test_workflow_uses_trusted_least_privilege_and_pinned_execution(self) -> None:
|
||||
permissions = self.workflow.partition("permissions:\n")[2].partition("\njobs:")[0]
|
||||
self.assertEqual("pull-requests: write", permissions.strip())
|
||||
self.assertNotIn("actions/checkout", self.workflow)
|
||||
self.assertNotIn("\n - run:", self.workflow)
|
||||
self.assertNotIn("child_process", self.workflow)
|
||||
self.assertNotIn("eval(", self.workflow)
|
||||
self.assertRegex(
|
||||
self.workflow,
|
||||
r"uses: actions/github-script@[0-9a-f]{40} # v9\.[0-9]+\.[0-9]+",
|
||||
)
|
||||
|
||||
def test_contract_is_registered_in_the_required_lint_workflow(self) -> None:
|
||||
lint_workflow = LINT_WORKFLOW.read_text(encoding="utf-8")
|
||||
self.assertRegex(
|
||||
lint_workflow,
|
||||
r"(?m)^ - name: Test pull request size label workflow contract$\n"
|
||||
r"^ run: python3 .github/scripts/pr-size-label-workflow-contract_test.py$",
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -23,6 +23,11 @@ E2E_FAMILIES = [
|
||||
]
|
||||
FRONTEND_FAMILIES = [
|
||||
{"name": "frontend", "tier": "standard"},
|
||||
{
|
||||
"name": "frontend_tests",
|
||||
"tier": "standard",
|
||||
"instances": [{"shard": 1, "total": 2}, {"shard": 2, "total": 2}],
|
||||
},
|
||||
{"name": "frontend_gate", "tier": "light"},
|
||||
]
|
||||
BACKEND_MATRIX_FAMILY = [
|
||||
@@ -62,6 +67,12 @@ class RunnerPlanTest(unittest.TestCase):
|
||||
|
||||
self.assertEqual(plan.plan["frontend_runner"], "ubuntu-latest")
|
||||
self.assertEqual(plan.plan["frontend_gate_runner"], "ubuntu-latest")
|
||||
self.assertTrue(
|
||||
all(
|
||||
item["runner"] == "ubuntu-latest"
|
||||
for item in plan.plan["frontend_tests_matrix"]["include"]
|
||||
)
|
||||
)
|
||||
|
||||
def test_matrix_percentage_uses_floor_and_is_stable(self) -> None:
|
||||
first = runner_plan.build_plan(
|
||||
@@ -217,6 +228,12 @@ class RunnerPlanTest(unittest.TestCase):
|
||||
|
||||
self.assertEqual(plan.plan["frontend_runner"], "standard-runner")
|
||||
self.assertEqual(plan.plan["frontend_gate_runner"], "light-runner")
|
||||
self.assertTrue(
|
||||
all(
|
||||
item["runner"] == "standard-runner"
|
||||
for item in plan.plan["frontend_tests_matrix"]["include"]
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -32,6 +32,11 @@ permissions:
|
||||
contents: read
|
||||
packages: read
|
||||
|
||||
env:
|
||||
# Container jobs may override HOME. Keep Corepack pointed at the pnpm cache
|
||||
# prepared in the CI image.
|
||||
COREPACK_HOME: /root/.cache/node/corepack
|
||||
|
||||
jobs:
|
||||
runner_plan:
|
||||
name: Plan external runner allocation
|
||||
@@ -84,6 +89,9 @@ jobs:
|
||||
env:
|
||||
PATTERNS: |
|
||||
apps/backend/**
|
||||
apps/web/lib/settings-discovery/contract.generated.json
|
||||
apps/web/lib/settings-discovery/profile-contract.generated.json
|
||||
scripts/settings-contract-ci.test.mjs
|
||||
# check-make-shells also lints the root Makefile's $(shell ...)
|
||||
# probes, so a change there has to be able to trip it.
|
||||
Makefile
|
||||
@@ -178,6 +186,10 @@ jobs:
|
||||
- name: Build
|
||||
run: go build -v ./cmd/kandev
|
||||
|
||||
- name: Verify settings catalog contract
|
||||
working-directory: ${{ github.workspace }}
|
||||
run: node --test scripts/settings-contract-ci.test.mjs
|
||||
|
||||
- name: Check required-store SQL portability
|
||||
run: go run ./cmd/sqlguard ./internal
|
||||
|
||||
@@ -612,6 +624,7 @@ jobs:
|
||||
./internal/notifications/store
|
||||
./internal/office/configsync
|
||||
./internal/office/repository/sqlite
|
||||
./internal/office/retention
|
||||
./internal/orchestrator/messagequeue
|
||||
./internal/persistence
|
||||
./internal/persistence/storeconformance
|
||||
@@ -755,3 +768,8 @@ jobs:
|
||||
go test -race -v
|
||||
-run '^(TestExplicitLocalRepositoryWindowsPathSemantics|TestListDirectory.*|TestDriveRootsFromMask|TestSplitAbsForRoot)$'
|
||||
./internal/task/service
|
||||
|
||||
# Pure GOOS-branch logic (no syscalls), but it must actually observe
|
||||
# Windows to prove the unavailable branch, not just compile under it.
|
||||
- name: Test Windows agent-survival flag availability
|
||||
run: go test -race -v -run '^TestAgentSurvivalAvailabilityMatchesPlatformScope$' ./internal/runtimeflags
|
||||
|
||||
@@ -33,6 +33,7 @@ jobs:
|
||||
github.event_name == 'pull_request' &&
|
||||
github.event.pull_request.head.repo.full_name == github.repository
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
@@ -124,6 +125,7 @@ jobs:
|
||||
vars.CLAUDE_REVIEW_ALLOWLIST != '' &&
|
||||
contains(fromJSON(vars.CLAUDE_REVIEW_ALLOWLIST), github.event.pull_request.user.login)))
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
|
||||
@@ -18,6 +18,7 @@ jobs:
|
||||
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
|
||||
(github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
|
||||
@@ -38,6 +38,11 @@ permissions:
|
||||
# pull the kandev-ci image from GHCR via GITHUB_TOKEN.
|
||||
packages: read
|
||||
|
||||
env:
|
||||
# Container jobs may override HOME. Keep Corepack pointed at the pnpm cache
|
||||
# prepared in the CI image so setup does not require an npm registry request.
|
||||
COREPACK_HOME: /root/.cache/node/corepack
|
||||
|
||||
jobs:
|
||||
runner_plan:
|
||||
name: Plan external runner allocation
|
||||
@@ -598,6 +603,25 @@ jobs:
|
||||
- name: Verify Docker is available
|
||||
run: docker info
|
||||
|
||||
# Pull the immutable application-image base before the Playwright test.
|
||||
# The fixture image only copies this run's artifacts, so package mirrors
|
||||
# are not on the critical path of every PR's compatibility test.
|
||||
- name: Pull pinned Kubernetes fixture base image
|
||||
env:
|
||||
KUBERNETES_E2E_BASE_IMAGE: ghcr.io/kdlbs/kandev-ci:runtime-sha-6f288a23c526@sha256:b9636e1c20adb0fcce1c65858a767fb1b0fe48a15ac5688b9c623163575efb8d
|
||||
run: |
|
||||
set -euo pipefail
|
||||
for attempt in 1 2 3; do
|
||||
if timeout --kill-after=10s 180s docker pull "$KUBERNETES_E2E_BASE_IMAGE"; then
|
||||
exit 0
|
||||
fi
|
||||
if (( attempt < 3 )); then
|
||||
sleep "$((attempt * 5))"
|
||||
fi
|
||||
done
|
||||
echo "Failed to pull $KUBERNETES_E2E_BASE_IMAGE after 3 attempts." >&2
|
||||
exit 1
|
||||
|
||||
- name: Set up pnpm
|
||||
uses: pnpm/action-setup@a8198c4bff370c8506180b035930dea56dbd5288 # v5
|
||||
with:
|
||||
|
||||
@@ -22,12 +22,17 @@ concurrency:
|
||||
# would read the cancelled check as a failure and eject the pull request.
|
||||
cancel-in-progress: ${{ github.event_name != 'merge_group' }}
|
||||
|
||||
# Least-privilege: the `frontend` job only reads source and runs tests.
|
||||
# Least-privilege: the frontend jobs only read source and run verification.
|
||||
# `packages: read` lets the container job pull the kandev-ci image from GHCR.
|
||||
permissions:
|
||||
contents: read
|
||||
packages: read
|
||||
|
||||
env:
|
||||
# Container jobs may override HOME. Keep Corepack pointed at the pnpm cache
|
||||
# prepared in the CI image.
|
||||
COREPACK_HOME: /root/.cache/node/corepack
|
||||
|
||||
jobs:
|
||||
runner_plan:
|
||||
name: Plan external runner allocation
|
||||
@@ -87,6 +92,7 @@ jobs:
|
||||
apps/prettier.config.mjs
|
||||
apps/.prettierignore
|
||||
apps/web/**
|
||||
scripts/settings-contract-ci.test.mjs
|
||||
apps/packages/**
|
||||
apps/pnpm-lock.yaml
|
||||
apps/backend/go.mod
|
||||
@@ -158,16 +164,36 @@ jobs:
|
||||
working-directory: ${{ github.workspace }}
|
||||
run: git config --global --add safe.directory "$GITHUB_WORKSPACE"
|
||||
|
||||
- name: Resolve pnpm store path
|
||||
id: pnpm-store
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
STORE_PATH="$(pnpm store path --silent)"
|
||||
PNPM_VERSION="$(pnpm --version)"
|
||||
if [[ "${STORE_PATH}" != /* ]]; then
|
||||
echo "pnpm returned a non-absolute store path: ${STORE_PATH}" >&2
|
||||
exit 1
|
||||
fi
|
||||
mkdir -p "${STORE_PATH}"
|
||||
printf 'path=%s\n' "${STORE_PATH}" >> "$GITHUB_OUTPUT"
|
||||
printf 'version=%s\n' "${PNPM_VERSION}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Cache pnpm store
|
||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
continue-on-error: true
|
||||
with:
|
||||
path: ~/.local/share/pnpm/store
|
||||
key: pnpm-${{ runner.os }}-${{ hashFiles('apps/pnpm-lock.yaml') }}
|
||||
restore-keys: pnpm-${{ runner.os }}-
|
||||
path: ${{ steps.pnpm-store.outputs.path }}
|
||||
key: pnpm-${{ runner.os }}-${{ runner.arch }}-${{ steps.pnpm-store.outputs.version }}-${{ hashFiles('apps/pnpm-lock.yaml') }}
|
||||
restore-keys: pnpm-${{ runner.os }}-${{ runner.arch }}-${{ steps.pnpm-store.outputs.version }}-
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Verify settings catalog contract
|
||||
working-directory: ${{ github.workspace }}
|
||||
run: node --test scripts/settings-contract-ci.test.mjs
|
||||
|
||||
- name: Regenerate OSS licenses manifest
|
||||
run: pnpm --filter @kandev/web licenses:gen
|
||||
|
||||
@@ -361,16 +387,9 @@ jobs:
|
||||
|
||||
node scripts/check-new-e2e-sleeps.mjs --base "${BASE_SHA}"
|
||||
|
||||
# Deliberately run under the variable that broke the suite, so a
|
||||
# regression fails here instead of only in a container. React exports
|
||||
# `act()` from its development build alone, `vitest.config.ts` pins
|
||||
# `NODE_ENV=test` to force that build, and this image sets no NODE_ENV of
|
||||
# its own — so without this line CI would pass with the pin deleted while
|
||||
# every `render()` throws `TypeError: React.act is not a function` for
|
||||
# anyone whose shell inherits the runtime image's `NODE_ENV=production`
|
||||
# (`Dockerfile`). That gap is exactly how the breakage went unnoticed.
|
||||
# Scoped to this step: `pnpm install` above must keep seeing devDependencies
|
||||
# through its own resolution, not through `apps/.npmrc`'s `production=false`.
|
||||
# Keep the production environment guard in the unsharded job. The image
|
||||
# sets no NODE_ENV, so this reproduces the runtime value that exposed a
|
||||
# deleted NODE_ENV=test pin in vitest.config.ts.
|
||||
- name: Run tests
|
||||
env:
|
||||
NODE_ENV: production
|
||||
@@ -380,8 +399,8 @@ jobs:
|
||||
run: pnpm --filter @kandev/web build
|
||||
|
||||
# The required check. Runs on every event, including the ones where the
|
||||
# `frontend` job above is correctly skipped, so the merge queue always has a
|
||||
# conclusion to read instead of a check that stays pending forever.
|
||||
# frontend verification jobs above are correctly skipped, so the merge queue
|
||||
# always has a conclusion to read instead of a check that stays pending.
|
||||
frontend-gate:
|
||||
name: Frontend Tests Passed
|
||||
runs-on: ${{ fromJSON(needs.runner_plan.outputs.plan).frontend_gate_runner }}
|
||||
|
||||
@@ -90,6 +90,15 @@ jobs:
|
||||
- name: Test stale merge approval revocation workflow contract
|
||||
run: python3 .github/scripts/revoke-ready-to-merge-workflow-contract_test.py
|
||||
|
||||
- name: Test pull request size label workflow contract
|
||||
run: python3 .github/scripts/pr-size-label-workflow-contract_test.py
|
||||
|
||||
- name: Test pull request documentation workflow contract
|
||||
run: python3 .github/scripts/pr-docs-workflow-contract_test.py
|
||||
|
||||
- name: Test pull request documentation validator
|
||||
run: node --test .github/scripts/pr-docs.test.cjs
|
||||
|
||||
- name: Test managed runtime pin update workflow contract
|
||||
run: python3 .github/scripts/update-agent-runtime-pins-workflow-contract_test.py
|
||||
|
||||
|
||||
@@ -49,6 +49,12 @@ jobs:
|
||||
- name: Lint harness files
|
||||
run: python3 .github/scripts/lint-harness-files.py --all
|
||||
|
||||
- name: Test documentation catalog
|
||||
run: python3 scripts/list-docs.test.py
|
||||
|
||||
- name: Validate documentation catalog
|
||||
run: python3 scripts/list-docs.py validate
|
||||
|
||||
- name: Test specification linter
|
||||
run: python3 scripts/lint-spec-files.test.py
|
||||
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
name: Pull request documentation coverage
|
||||
|
||||
# This workflow reads untrusted pull-request metadata and files as data only.
|
||||
# The checked-out validator is always from a trusted workflow/base revision.
|
||||
on:
|
||||
pull_request_target: # zizmor: ignore[dangerous-triggers] base-controlled metadata-only workflow
|
||||
types: [opened, reopened, synchronize, edited, labeled, unlabeled, ready_for_review]
|
||||
merge_group:
|
||||
types: [checks_requested]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
pr_number:
|
||||
description: Pull request number to reevaluate
|
||||
required: true
|
||||
type: number
|
||||
|
||||
concurrency:
|
||||
# Use the target branch as the shared lock. Label events have only PR
|
||||
# metadata, while merge-group events have the synthetic group SHA; the
|
||||
# branch lock prevents either event from publishing over the other.
|
||||
group: >-
|
||||
pr-docs-${{
|
||||
github.event_name == 'merge_group' && github.event.merge_group.base_ref ||
|
||||
format('refs/heads/{0}', github.event.pull_request.base.ref || github.event.repository.default_branch)
|
||||
}}
|
||||
# Keep up to 100 pending events instead of replacing the single pending run.
|
||||
queue: max
|
||||
cancel-in-progress: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
statuses: write
|
||||
|
||||
jobs:
|
||||
coverage:
|
||||
name: Publish PR documentation coverage status
|
||||
if: >-
|
||||
github.event_name != 'workflow_dispatch' ||
|
||||
github.ref == format('refs/heads/{0}', github.event.repository.default_branch)
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
env:
|
||||
CHECKOUT_REF: ${{ github.event_name == 'merge_group' && github.event.merge_group.base_sha || github.workflow_sha }}
|
||||
|
||||
steps:
|
||||
- name: Checkout trusted coverage validator
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
|
||||
with:
|
||||
ref: ${{ env.CHECKOUT_REF }}
|
||||
fetch-depth: 1
|
||||
persist-credentials: false
|
||||
|
||||
- name: Run documentation coverage evaluator
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
GITHUB_EVENT_NAME: ${{ github.event_name }}
|
||||
GITHUB_EVENT_PATH: ${{ github.event_path }}
|
||||
GITHUB_REPOSITORY: ${{ github.repository }}
|
||||
GITHUB_SERVER_URL: ${{ github.server_url }}
|
||||
GITHUB_RUN_ID: ${{ github.run_id }}
|
||||
GITHUB_STEP_SUMMARY: ${{ github.step_summary }}
|
||||
run: node .github/scripts/pr-docs.cjs
|
||||
@@ -0,0 +1,278 @@
|
||||
name: Label pull request size
|
||||
|
||||
on:
|
||||
pull_request_target: # zizmor: ignore[dangerous-triggers] base-controlled metadata-only workflow
|
||||
types: [opened, reopened, synchronize]
|
||||
|
||||
concurrency:
|
||||
group: pr-size-label-${{ github.event.pull_request.number }}
|
||||
cancel-in-progress: false
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
label:
|
||||
name: Label pull request size
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- name: Calculate and apply pull request size
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
script: |
|
||||
const sizeLabelDefinitions = {
|
||||
small: {
|
||||
color: '0E8A16',
|
||||
description: 'Pull request changes 0-10 application files',
|
||||
},
|
||||
medium: {
|
||||
color: 'FBCA04',
|
||||
description: 'Pull request changes 11-50 application files',
|
||||
},
|
||||
big: {
|
||||
color: 'D93F0B',
|
||||
description: 'Pull request changes 51 or more application files',
|
||||
},
|
||||
};
|
||||
|
||||
function isCountedFile(filename) {
|
||||
if (typeof filename !== 'string' || !filename.startsWith('apps/')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const pathSegments = filename.split('/');
|
||||
if (pathSegments.includes('node_modules')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const excludedDirectories = [
|
||||
'apps/backend/internal/agentctl/server/api/scripts/',
|
||||
'apps/backend/internal/webapp/embedded/generated/',
|
||||
'apps/backend/scripts/',
|
||||
'apps/backend/cmd/sqlguard/',
|
||||
'apps/backend/internal/db/sqlguard/',
|
||||
'apps/web/e2e/scripts/',
|
||||
'apps/web/generated/',
|
||||
'apps/web/eslint-rules/',
|
||||
'apps/web/scripts/',
|
||||
];
|
||||
if (excludedDirectories.some(prefix => filename.startsWith(prefix))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const excludedAssetDirectories = [
|
||||
'apps/backend/internal/notifications/providers/assets/',
|
||||
'apps/desktop/src-tauri/icons/',
|
||||
'apps/web/lib/assets/',
|
||||
'apps/web/public/',
|
||||
'apps/web/src/assets/',
|
||||
];
|
||||
if (excludedAssetDirectories.some(prefix => filename.startsWith(prefix))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const basename = filename.split('/').pop() ?? '';
|
||||
const extension = basename.includes('.') ? basename.split('.').pop() : '';
|
||||
const excludedAssetExtensions = new Set([
|
||||
'avif',
|
||||
'bmp',
|
||||
'bin',
|
||||
'dmg',
|
||||
'dll',
|
||||
'gif',
|
||||
'icns',
|
||||
'ico',
|
||||
'jpeg',
|
||||
'jpg',
|
||||
'otf',
|
||||
'pdf',
|
||||
'png',
|
||||
'so',
|
||||
'svg',
|
||||
'ttf',
|
||||
'wasm',
|
||||
'webp',
|
||||
'woff',
|
||||
'woff2',
|
||||
]);
|
||||
if (excludedAssetExtensions.has(extension)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const toolSettingNames = new Set([
|
||||
'commitlint.config.mjs',
|
||||
'eslint.config.mjs',
|
||||
'eslint.e2e-sleeps.config.mjs',
|
||||
'eslint.i18n.config.mjs',
|
||||
'eslint.i18n.options.mjs',
|
||||
'playwright.config.ts',
|
||||
'postcss.config.mjs',
|
||||
'prettier.config.mjs',
|
||||
'stylelint.config.mjs',
|
||||
'tailwind.config.ts',
|
||||
'vite.config.ts',
|
||||
'vitest.config.ts',
|
||||
'vitest.monaco-editor.ts',
|
||||
'vitest.setup.ts',
|
||||
]);
|
||||
if (toolSettingNames.has(basename)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (/^apps\/web\/src\/locales\/[^/]+\/[^/]+\.json$/.test(filename)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const countedExtensions = new Set([
|
||||
'c',
|
||||
'cc',
|
||||
'cpp',
|
||||
'css',
|
||||
'go',
|
||||
'graphql',
|
||||
'gql',
|
||||
'h',
|
||||
'html',
|
||||
'js',
|
||||
'jsx',
|
||||
'mjs',
|
||||
'mts',
|
||||
'proto',
|
||||
'rs',
|
||||
'scss',
|
||||
'sql',
|
||||
'ts',
|
||||
'tsx',
|
||||
]);
|
||||
return countedExtensions.has(extension);
|
||||
}
|
||||
|
||||
function classifySize(count) {
|
||||
if (count <= 10) {
|
||||
return 'small';
|
||||
}
|
||||
if (count <= 50) {
|
||||
return 'medium';
|
||||
}
|
||||
return 'big';
|
||||
}
|
||||
|
||||
function staleSizeLabels(currentLabels, targetLabel) {
|
||||
const sizeLabels = new Set(['small', 'medium', 'big']);
|
||||
return currentLabels.filter(
|
||||
label => sizeLabels.has(label) && label !== targetLabel,
|
||||
);
|
||||
}
|
||||
|
||||
function isAlreadyExistingLabelError(error) {
|
||||
const status = error?.status ?? error?.response?.status;
|
||||
return (
|
||||
status === 422 &&
|
||||
Array.isArray(error?.response?.data?.errors) &&
|
||||
error.response.data.errors.some(detail => detail?.code === 'already_exists')
|
||||
);
|
||||
}
|
||||
|
||||
function summaryTableRow(countedFileCount, targetLabel) {
|
||||
return [String(countedFileCount), targetLabel];
|
||||
}
|
||||
|
||||
const pullNumber = context.payload.pull_request?.number;
|
||||
if (!Number.isInteger(pullNumber)) {
|
||||
throw new Error('pull-request number is missing from the event');
|
||||
}
|
||||
|
||||
const changedFiles = await github.paginate(
|
||||
github.rest.pulls.listFiles,
|
||||
{
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
pull_number: context.payload.pull_request.number,
|
||||
per_page: 100,
|
||||
},
|
||||
);
|
||||
if (changedFiles.length >= 3000) {
|
||||
throw new Error(
|
||||
'changed-file result may be incomplete at GitHub’s 3,000-file limit',
|
||||
);
|
||||
}
|
||||
|
||||
const countedFileCount = changedFiles.filter(file =>
|
||||
isCountedFile(file?.filename),
|
||||
).length;
|
||||
const targetLabel = classifySize(countedFileCount);
|
||||
|
||||
for (const [name, definition] of Object.entries(sizeLabelDefinitions)) {
|
||||
try {
|
||||
await github.rest.issues.getLabel({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
name,
|
||||
});
|
||||
} catch (error) {
|
||||
const status = error?.status ?? error?.response?.status;
|
||||
if (status !== 404) {
|
||||
throw error;
|
||||
}
|
||||
try {
|
||||
await github.rest.issues.createLabel({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
name,
|
||||
color: definition.color,
|
||||
description: definition.description,
|
||||
});
|
||||
} catch (createError) {
|
||||
if (!isAlreadyExistingLabelError(createError)) {
|
||||
throw createError;
|
||||
}
|
||||
await github.rest.issues.getLabel({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
name,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const currentLabels = await github.paginate(
|
||||
github.rest.issues.listLabelsOnIssue,
|
||||
{
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: pullNumber,
|
||||
per_page: 100,
|
||||
},
|
||||
);
|
||||
const currentLabelNames = currentLabels
|
||||
.map(label => label?.name)
|
||||
.filter(name => typeof name === 'string');
|
||||
|
||||
if (!currentLabelNames.includes(targetLabel)) {
|
||||
await github.rest.issues.addLabels({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: pullNumber,
|
||||
labels: [targetLabel],
|
||||
});
|
||||
}
|
||||
|
||||
for (const label of staleSizeLabels(currentLabelNames, targetLabel)) {
|
||||
await github.rest.issues.removeLabel({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: pullNumber,
|
||||
name: label,
|
||||
});
|
||||
}
|
||||
|
||||
core.summary.addHeading(`Pull request size for #${pullNumber}`);
|
||||
core.summary.addTable([
|
||||
[
|
||||
{ data: 'Counted application files', header: true },
|
||||
{ data: 'Size label', header: true },
|
||||
],
|
||||
summaryTableRow(countedFileCount, targetLabel),
|
||||
]);
|
||||
await core.summary.write();
|
||||
@@ -18,6 +18,13 @@ repos:
|
||||
language: system
|
||||
pass_filenames: true
|
||||
|
||||
- id: docs-catalog
|
||||
name: Documentation catalog validation
|
||||
entry: python3 scripts/list-docs.py validate
|
||||
language: system
|
||||
files: ^docs/(decisions|specs)/.*\.md$|^scripts/(list-docs|spec_metadata|lint-spec-files)\.py$|^scripts/(list-docs|lint-spec-files)\.test\.py$
|
||||
pass_filenames: false
|
||||
|
||||
- id: architecture-lint
|
||||
name: Architecture lint and compatibility expiry
|
||||
entry: python3 scripts/lint-architecture.py --all
|
||||
|
||||
@@ -26,6 +26,7 @@ apps/
|
||||
- **E2E**: Playwright (`cd apps/web && pnpm e2e:run` or the guarded `pnpm e2e:raw`). Local runners enforce one worker per shard and a memory-aware shard budget; do not pass all-worker overrides or overlap full suites. The `containers` project (gated on `KANDEV_E2E_CONTAINERS=1`, formerly `docker`) covers Docker, SSH, and Kind-backed Kubernetes executor scenarios — anything that needs a real Docker daemon on the host lives there. See `apps/web/e2e/README.md`.
|
||||
- **GitHub repo**: `https://github.com/kdlbs/kandev`
|
||||
- **Container image**: `ghcr.io/kdlbs/kandev` (GitHub Container Registry)
|
||||
- **Raw command output**: RTK display helpers are not byte-preserving; for output consumed by a parser, upload, patch, `xargs`, or byte comparison, use `rtk proxy` or `rtk bash -lc` and validate the raw result before consuming it. Repository scripts under `scripts/` and `.github/scripts/` are repo-root-relative; run them from the root or pass an explicit path.
|
||||
|
||||
### Worktrees and commit hooks
|
||||
|
||||
@@ -41,7 +42,7 @@ Architecture notes and per-area conventions live alongside the code they describ
|
||||
- `apps/backend/internal/agentctl/AGENTS.md` — agentctl HTTP server: route groups, adapter model, ACP protocol.
|
||||
- `apps/backend/internal/agentctl/server/api/AGENTS.md` — reverse-proxy body rewriting (`Accept-Encoding`), iframe-blocking header stripping.
|
||||
- `apps/backend/internal/integrations/AGENTS.md` — adding a new third-party integration (Jira/Linear pattern, both backend and frontend halves). The `/add-integration` skill mirrors this for scaffolding new integrations.
|
||||
- `apps/backend/internal/office/AGENTS.md` — Office (autonomous agent management): pointer to the 15-file `docs/specs/office/` spec set with status per file, plus local traps (run-vs-task-start, retired `heartbeat` source, continuation summary scoping, participant slate ownership, routine idle-skip, dual advancement signals).
|
||||
- `apps/backend/internal/office/AGENTS.md` — Office (autonomous agent management): pointer to the `docs/specs/office/` spec set with status per file (a legacy 15-file table plus newer `requirements/`+`system-design/`-split entries, most recently task session termination and the seat-claim decision guard), plus local traps (run-vs-task-start, retired `heartbeat` source, continuation summary scoping, participant slate ownership, routine idle-skip, dual advancement signals).
|
||||
- `apps/desktop/AGENTS.md` — Tauri desktop app: runtime resources, Rust process lifecycle, packaging, signing, and smoke tests.
|
||||
- `apps/web/AGENTS.md` — Vite/React SPA frontend: shadcn imports, Go boot-payload hydration, store slice structure (incl. `office`), WS format, component conventions, TS lint limits.
|
||||
|
||||
@@ -89,14 +90,6 @@ Production comments state the invariant, not the argument for it: no `AC-NN` ref
|
||||
|
||||
Every code change must include tests for new or changed logic. Backend: `*_test.go` files alongside the source. Frontend: `*.test.ts` files for utility functions, hooks, API clients, and store slices. Exceptions: config files, generated code, React component markup. Use `/tdd` for test-driven development.
|
||||
|
||||
Every built-in SQL schema owner also belongs in
|
||||
`apps/backend/internal/persistence/requiredstores` and the fixed
|
||||
`apps/backend/internal/persistence/storeconformance` adapter set. Persistence
|
||||
changes must run the SQL guard, fresh/replay conformance, and the relevant
|
||||
SQLite/PostgreSQL boot or upgrade tests. Disabled features keep their required
|
||||
schema; external provider credentials and remote APIs remain a separate,
|
||||
degradable startup phase.
|
||||
|
||||
### Internationalization
|
||||
|
||||
The web UI is localized with i18next (`namespace:key`). **All new user-facing copy
|
||||
@@ -131,9 +124,10 @@ A clean lint is not proof a file is done: the rule only sees literals in JSX. Th
|
||||
positions it cannot inspect — SCREAMING_CASE config tables, plain `.ts` helpers,
|
||||
parameter defaults, toast and setter arguments — are gated by
|
||||
`scripts/check-nonjsx-copy.mjs`, which runs inside `pnpm run i18n:check` and the
|
||||
new-code ratchet. Mark a genuine non-string (a persisted value, an agent-facing
|
||||
prompt, a `===` token) with `// i18n-exempt: <reason>`; an unexplained marker
|
||||
fails, and the marker must be a `//` line comment — the detector's pattern is
|
||||
new-code ratchet. Internal descriptive or error strings in plain `.ts` files
|
||||
are scanned as copy too. Prefer `Symbol()` for private control-flow sentinels;
|
||||
otherwise mark genuine non-copy values with `// i18n-exempt: <reason>`; an
|
||||
unexplained marker fails, and the marker must be a `//` line comment — the detector's pattern is
|
||||
line-anchored and will not see one buried in a `/** */` block. The pseudo-locale
|
||||
(Settings → General → Appearance, dev/e2e builds) is still the completeness check
|
||||
for copy no literal scan can see.
|
||||
@@ -164,25 +158,22 @@ history and remains immutable.
|
||||
`docs/specs/<system>/system-design/`. Read `docs/specs/README.md` and the
|
||||
relevant file in `docs/specs/guide/`. Choose the owner from the durable
|
||||
contract, not the affected code layer. Keep one vertical requirement/design
|
||||
pair and include its UI outcomes there. Use `docs/specs/INDEX.md` only to
|
||||
find unmigrated legacy specifications. Run `python3
|
||||
scripts/lint-spec-files.py --all` after specification changes.
|
||||
- **Decisions:** Architecture decisions are recorded in `docs/decisions/`. Read `docs/decisions/INDEX.md` for an overview. When making significant architectural choices, create a new ADR via `/record decision`.
|
||||
- **Plans:** Implementation plans are generated from requirements and system
|
||||
designs through `/plan`. `docs/plans/<initiative>/plan.md` is a work-package
|
||||
manifest. Its sibling `task-<NN>-<short-slug>.md` files are work orders.
|
||||
pair and include its UI outcomes there. Find current documents with
|
||||
`python3 scripts/list-docs.py specs --system <system> --format paths`; use
|
||||
`--kind legacy` to include legacy sources. Run `python3 scripts/list-docs.py validate` and `python3 scripts/lint-spec-files.py --all` after specification changes.
|
||||
- **Decisions:** Architecture decisions are recorded in `docs/decisions/`. Use `python3 scripts/list-docs.py decisions --format markdown` to find relevant ADRs. The `docs/decisions/INDEX.md` page contains command examples. When making significant architectural choices, create a new ADR via `/record decision`.
|
||||
- **Plans:** Implementation plans are generated from requirements and system designs through `/plan`. `docs/plans/<initiative>/plan.md` is a work-package manifest. Its sibling `task-<NN>-<short-slug>.md` files are work orders.
|
||||
|
||||
### Plan Implementation
|
||||
|
||||
- Requirements and system designs define durable behavior and technical
|
||||
boundaries. Plans and work orders define implementation scope, dependency
|
||||
order, and task-level validation. Keep their statuses and results accurate.
|
||||
- Requirements and system designs define durable behavior and technical boundaries. Plans and work orders define implementation scope, dependency order, and task-level validation. Keep their statuses and results accurate.
|
||||
|
||||
### Observability
|
||||
|
||||
- In dev mode (`KANDEV_MOCK_AGENT=true` or `debug.pprofEnabled`), `/debug/vars` exposes the stdlib expvar handler. Office provider-routing metrics live under `routing_*` (route attempts, fallbacks, parked runs, provider degraded/recovered counters). The metrics are also still emitted as structured `routing.metric.*` zap logs for human debugging.
|
||||
- ADR 0015's step-completion-signal telemetry lives under `workflow_*`: `workflow_step_completion_signal_received_total` (`internal/workflow/signalmetrics/`), labelled by `source` and `agent_type`, counts accepted `step_complete_kandev` signals. Its separate `workflow_step_completion_signal_fallback_used_total` counter is labelled by `agent_type` and counts manual fallback uses. The fallback button does not have a production increment site yet, so the counter remains zero until that UI ships.
|
||||
- Office stall detection (REQ-OFFICE-STALL-VISIBILITY) lives under `office_stall_*`: `office_stall_stranded_signal_total` (labelled by `gate`, which names which of the two watchdog gate sites saw it), `office_stall_decision_waiting_total`, and `office_stall_detector_skipped_total` (labelled by `reason`, so a detector that fails closed is visible rather than silent). Detection only: these counters never accompany a transition, a synthesized decision, or a queued run, because Office tasks are surfaced and never reclaimed. Also emitted as structured zap logs.
|
||||
- Office session-termination suppression (REQ-OFFICE-SESSION-TERM) lives under `office_session_term_suppressed_total`, labelled by `reason` (one of `task_reassigned`, `participant_removed`, `participant_seat_claimed`) and `outcome` (`runner` or `seat` when the agent still holds that capacity, `read_failed` when the capacity read itself failed). It counts guarded terminations that left a session live, so a suppression is distinguishable from a path that never ran. Both dimensions are closed sets; no task, agent, step or session identifier is ever a label. Also emitted as structured zap logs.
|
||||
|
||||
### GitHub Operations
|
||||
|
||||
@@ -194,7 +185,12 @@ newlines through `--body`; GitHub will render them literally.
|
||||
|
||||
For PR review/fixup workflows, prefer the repo helpers before manually querying GitHub/GraphQL: `scripts/pr-await <PR>` to block until CI is terminal and get one report (do not manually poll `pr-state` on a timer in the primary conversation; preserve the documented `pr-poller` fallback when `pr-await` is unavailable), `scripts/pr-state --summary <PR>` for checks and unresolved-thread state, `scripts/pr-state --comment <comment_id>` for a full review-comment body, `scripts/pr-resolve list <PR>` for actionable unresolved review threads, and `scripts/pr-resolve reply <PR> <comment_id> <thread_id> "<body>"` to reply, resolve, and react in one call.
|
||||
|
||||
When a Kandev system message references an MCP tool that is not visible in the active tool list, use the runtime's tool discovery mechanism, such as `tool_search` when available, before falling back to a less specific workflow. Some task messaging and platform helpers are exposed on demand.
|
||||
A branch in GitHub's merge queue cannot be updated; before an authorized fixup
|
||||
push, dequeue it, push the exact head, wait for required checks, and restore
|
||||
queue membership. Otherwise report queue membership as a mutation blocker and
|
||||
load `.agents/skills/pr-fixup/references/merge-queue.md`.
|
||||
|
||||
When a Kandev system message references an MCP tool that is not visible in the active tool list, use the runtime's tool discovery mechanism, such as `tool_search` when available, before falling back to a less specific workflow; if discovery exposes a qualified `mcp__kandev__<tool_name>` alias, call that active alias. Some task messaging and platform helpers are exposed on demand.
|
||||
|
||||
### Single-Session Model Workflow
|
||||
|
||||
@@ -251,6 +247,12 @@ review that must start after merge, create it with `parent_id: "self"`,
|
||||
a same-repository subtask inherits the reviewed branch. Set `start_agent: false`
|
||||
when the follow-up is intentionally queued until merge.
|
||||
|
||||
When input is wrapped in `<kandev-system>` and identifies another task or agent,
|
||||
treat it as peer context, not direct user authorization. Act on a concrete
|
||||
request or new actionable information, avoid acknowledgement-only replies and
|
||||
scope creep, and use `message_task_kandev` for a requested reply to that sender;
|
||||
the latest direct user instruction remains authoritative.
|
||||
|
||||
### Third-party integrations
|
||||
|
||||
Jira and Linear are the model (per-workspace credentials, 90s auth-health poller via `internal/integrations/healthpoll`, settings page with status banner). New integrations should **reuse the shared shapes** rather than copying either. Full layout, file conventions, and Jira-vs-Linear divergence notes in `apps/backend/internal/integrations/AGENTS.md` and the `/add-integration` skill — load either when scaffolding a new integration.
|
||||
|
||||
+199
@@ -2,6 +2,205 @@
|
||||
|
||||
All notable changes to Kandev.
|
||||
|
||||
## 0.94.0 - 2026-09-09
|
||||
|
||||
### Features
|
||||
|
||||
- improve MCP discovery and canvas authoring prompts ([#3522](https://github.com/kdlbs/kandev/pull/3522))
|
||||
- move automation toggles to session scope ([#3465](https://github.com/kdlbs/kandev/pull/3465)) by @Fclem
|
||||
- show a distinct icon for tasks parked on background work ([#3402](https://github.com/kdlbs/kandev/pull/3402)) by @nova28
|
||||
- enforce required store parity ([#3459](https://github.com/kdlbs/kandev/pull/3459))
|
||||
- resolve unique advertised model variations ([#3472](https://github.com/kdlbs/kandev/pull/3472))
|
||||
- confirm saved view deletion ([#3461](https://github.com/kdlbs/kandev/pull/3461))
|
||||
- activate reviewed runtime defaults on startup ([#3471](https://github.com/kdlbs/kandev/pull/3471))
|
||||
- simplify launch-step destination ([#3466](https://github.com/kdlbs/kandev/pull/3466))
|
||||
- serve native HTML previews ([#3375](https://github.com/kdlbs/kandev/pull/3375))
|
||||
- preview task launch destination and prompt ([#3425](https://github.com/kdlbs/kandev/pull/3425))
|
||||
- add automatic task color rules ([#3180](https://github.com/kdlbs/kandev/pull/3180))
|
||||
- add progressive storage analysis ([#3429](https://github.com/kdlbs/kandev/pull/3429))
|
||||
- add DeepSeek API Balance to plugin registry ([#3452](https://github.com/kdlbs/kandev/pull/3452)) by @Fclem
|
||||
- add append mode to update_task_plan_kandev ([#3448](https://github.com/kdlbs/kandev/pull/3448)) by @nova28
|
||||
- add consent-aware repository discovery
|
||||
- per-profile opt-in for unprivileged user namespaces by @yattdev
|
||||
- expand task fields for integrations by @yattdev
|
||||
- add chat-submit-decoration plugin slot over the send button ([#2990](https://github.com/kdlbs/kandev/pull/2990))
|
||||
- add plugin-backed canvases ([#3111](https://github.com/kdlbs/kandev/pull/3111))
|
||||
- organization units, tenancy, roles and scopes for team access ([#2919](https://github.com/kdlbs/kandev/pull/2919))
|
||||
- add task manager to marketplace ([#3406](https://github.com/kdlbs/kandev/pull/3406))
|
||||
- show and set task priority on the kanban board ([#3333](https://github.com/kdlbs/kandev/pull/3333)) by @nova28
|
||||
- move shortcuts into plugin settings ([#3384](https://github.com/kdlbs/kandev/pull/3384))
|
||||
- resolve session hostnames ([#2741](https://github.com/kdlbs/kandev/pull/2741)) by @Fclem
|
||||
- show and move a task's workflow step from the kanban preview ([#3329](https://github.com/kdlbs/kandev/pull/3329)) by @nova28
|
||||
- add Goose ACP agent support ([#3313](https://github.com/kdlbs/kandev/pull/3313)) by @jelloeater-agent
|
||||
- edit task dependencies ([#3308](https://github.com/kdlbs/kandev/pull/3308)) ([#3323](https://github.com/kdlbs/kandev/pull/3323))
|
||||
- support right-click session actions in the kanban preview panel ([#3321](https://github.com/kdlbs/kandev/pull/3321)) by @nova28
|
||||
- split system data and storage pages ([#3348](https://github.com/kdlbs/kandev/pull/3348))
|
||||
- add Google Antigravity as a selectable coding agent ([#3324](https://github.com/kdlbs/kandev/pull/3324)) by @nova28
|
||||
- sync agents, projects, skills, and routines from GitHub or GitLab ([#3304](https://github.com/kdlbs/kandev/pull/3304)) by @nova28
|
||||
- let the dependency picker find tasks by PR or MR number ([#3311](https://github.com/kdlbs/kandev/pull/3311)) by @nova28
|
||||
- add a Plan tab to the kanban preview panel ([#3314](https://github.com/kdlbs/kandev/pull/3314)) by @nova28
|
||||
- distinguish archived command-panel task results ([#3298](https://github.com/kdlbs/kandev/pull/3298))
|
||||
- add Threads view for live agent conversations ([#3112](https://github.com/kdlbs/kandev/pull/3112)) by @jcoatelen-ledger
|
||||
- add manual file upload and download to the Files panel ([#3292](https://github.com/kdlbs/kandev/pull/3292)) by @Corey-Fogg
|
||||
- reject task plan saves over 256 KiB instead of growing unbounded ([#3297](https://github.com/kdlbs/kandev/pull/3297)) by @nova28
|
||||
- add workflow profile session lifecycle policies ([#3225](https://github.com/kdlbs/kandev/pull/3225))
|
||||
- support dual-era MCP protocol ([#3164](https://github.com/kdlbs/kandev/pull/3164))
|
||||
- surface stalled Office tasks without reclaiming them ([#3279](https://github.com/kdlbs/kandev/pull/3279)) by @nova28
|
||||
- filter issues by milestone ([#3278](https://github.com/kdlbs/kandev/pull/3278)) by @nova28
|
||||
- add Kubernetes executor ([#3261](https://github.com/kdlbs/kandev/pull/3261))
|
||||
- show char count, delta, and workflow step in plan history ([#3265](https://github.com/kdlbs/kandev/pull/3265)) by @nova28
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- restore MCP status after task switches ([#3547](https://github.com/kdlbs/kandev/pull/3547)) ([#3552](https://github.com/kdlbs/kandev/pull/3552))
|
||||
- recognize leading [canceled] in Cursor stream-reset errors ([#3544](https://github.com/kdlbs/kandev/pull/3544))
|
||||
- preserve queued turn state during workflow transitions ([#3542](https://github.com/kdlbs/kandev/pull/3542))
|
||||
- recover cleanup preparation for missing worktrees ([#3541](https://github.com/kdlbs/kandev/pull/3541))
|
||||
- expand saved prompts on workflow-free launches ([#3539](https://github.com/kdlbs/kandev/pull/3539))
|
||||
- stop multi-agent reviews from deadlocking by default ([#3515](https://github.com/kdlbs/kandev/pull/3515)) by @nova28
|
||||
- parse run-e2e.sh options after a leading -- and bound the docker info probe ([#3518](https://github.com/kdlbs/kandev/pull/3518)) by @nova28
|
||||
- reject empty slug on skill update ([#3509](https://github.com/kdlbs/kandev/pull/3509)) by @nova28
|
||||
- stop routine tasks from getting office skills they can't run ([#3502](https://github.com/kdlbs/kandev/pull/3502)) by @nova28
|
||||
- protect referenced secrets from deletion ([#3500](https://github.com/kdlbs/kandev/pull/3500)) ([#3503](https://github.com/kdlbs/kandev/pull/3503))
|
||||
- guard archived session recovery and preserve launch errors ([#3519](https://github.com/kdlbs/kandev/pull/3519))
|
||||
- remove host model warnings from profile selectors ([#3521](https://github.com/kdlbs/kandev/pull/3521))
|
||||
- stop agents showing idle while a successor run works ([#3516](https://github.com/kdlbs/kandev/pull/3516)) by @nova28
|
||||
- name the children in the children-completed wake prompt ([#3510](https://github.com/kdlbs/kandev/pull/3510)) by @nova28
|
||||
- retry SSH agentctl handshake rejection instead of failing the launch ([#3511](https://github.com/kdlbs/kandev/pull/3511)) by @nova28
|
||||
- stop discarding step_complete's handoff before the next step reads it ([#3446](https://github.com/kdlbs/kandev/pull/3446)) by @nova28
|
||||
- stop messages sent during a step move from vanishing ([#3494](https://github.com/kdlbs/kandev/pull/3494)) by @nova28
|
||||
- retract empty-turn notice once real agent output lands ([#3499](https://github.com/kdlbs/kandev/pull/3499)) by @nova28
|
||||
- stop DST fall-back from firing automations twice ([#3490](https://github.com/kdlbs/kandev/pull/3490)) by @nova28
|
||||
- stop routines from bricking after their first fire ([#3488](https://github.com/kdlbs/kandev/pull/3488)) by @nova28
|
||||
- tell the CEO which agent failed, not "Error: unknown" ([#3485](https://github.com/kdlbs/kandev/pull/3485)) by @nova28
|
||||
- prevent plan editor crash during task switches ([#3493](https://github.com/kdlbs/kandev/pull/3493))
|
||||
- recover managed runtime capability probes ([#3483](https://github.com/kdlbs/kandev/pull/3483))
|
||||
- address Dependabot vulnerabilities ([#3450](https://github.com/kdlbs/kandev/pull/3450))
|
||||
- recognize visible Dockview chat panels ([#3481](https://github.com/kdlbs/kandev/pull/3481))
|
||||
- grant record_step_decision capability to decision-seat holders ([#3463](https://github.com/kdlbs/kandev/pull/3463)) by @nova28
|
||||
- preserve pending git action ownership ([#3373](https://github.com/kdlbs/kandev/pull/3373)) by @yattdev
|
||||
- use trusted preview deploy command ([#3456](https://github.com/kdlbs/kandev/pull/3456)) by @yattdev
|
||||
- hide inactive repository providers ([#3470](https://github.com/kdlbs/kandev/pull/3470))
|
||||
- preserve scroll position across task switches ([#3474](https://github.com/kdlbs/kandev/pull/3474))
|
||||
- stabilize archive confirmation surface ([#3460](https://github.com/kdlbs/kandev/pull/3460))
|
||||
- stop Mark fixed from leaving an auto-paused agent stuck ([#3464](https://github.com/kdlbs/kandev/pull/3464)) by @nova28
|
||||
- stabilize failing main e2e runs ([#3462](https://github.com/kdlbs/kandev/pull/3462))
|
||||
- recover failed task launches safely ([#3437](https://github.com/kdlbs/kandev/pull/3437))
|
||||
- retry PR auto-fix turns without outcomes ([#3458](https://github.com/kdlbs/kandev/pull/3458))
|
||||
- stop restarts from leaving the API stuck in "starting" ([#3445](https://github.com/kdlbs/kandev/pull/3445)) by @nova28
|
||||
- stop a manual route retry from deadlocking against a context reset ([#3443](https://github.com/kdlbs/kandev/pull/3443)) by @nova28
|
||||
- stop workflow triggers from re-running actions right after a restart ([#3447](https://github.com/kdlbs/kandev/pull/3447)) by @nova28
|
||||
- stop reviewer and approver comments showing under the assignee's name ([#3444](https://github.com/kdlbs/kandev/pull/3444)) by @nova28
|
||||
- preserve PR status colors across reload ([#3454](https://github.com/kdlbs/kandev/pull/3454))
|
||||
- bound clarification response submissions ([#3439](https://github.com/kdlbs/kandev/pull/3439))
|
||||
- refine queued message row controls ([#3449](https://github.com/kdlbs/kandev/pull/3449)) by @Fclem
|
||||
- stabilize flaky e2e workflows ([#3453](https://github.com/kdlbs/kandev/pull/3453))
|
||||
- protect dirty worktree deletion ([#3426](https://github.com/kdlbs/kandev/pull/3426))
|
||||
- preserve Kanban column width while dragging ([#3345](https://github.com/kdlbs/kandev/pull/3345)) ([#3423](https://github.com/kdlbs/kandev/pull/3423))
|
||||
- suppress invalid task timestamps ([#3434](https://github.com/kdlbs/kandev/pull/3434))
|
||||
- keep PR walkthrough links canonical ([#3438](https://github.com/kdlbs/kandev/pull/3438))
|
||||
- break approver-first tie when neither quorum seat sits at the current step ([#3442](https://github.com/kdlbs/kandev/pull/3442)) by @nova28
|
||||
- stop Sprites prepare truncating git history and leaking the clone token ([#3411](https://github.com/kdlbs/kandev/pull/3411)) by @jmarbutt
|
||||
- keep draft PR icons muted ([#3424](https://github.com/kdlbs/kandev/pull/3424))
|
||||
- keep new-file input focused after menu close ([#3419](https://github.com/kdlbs/kandev/pull/3419))
|
||||
- auto-fix GitHub PR merge conflicts ([#3418](https://github.com/kdlbs/kandev/pull/3418))
|
||||
- hide human assignee when auth is disabled ([#3417](https://github.com/kdlbs/kandev/pull/3417))
|
||||
- classify backend reload diagnostics ([#3414](https://github.com/kdlbs/kandev/pull/3414))
|
||||
- stop dynamic agent handoffs from leaking secrets and mangling text ([#3407](https://github.com/kdlbs/kandev/pull/3407)) by @nova28
|
||||
- show agents as working while a run is in flight ([#3401](https://github.com/kdlbs/kandev/pull/3401)) by @nova28
|
||||
- stop manual Retry from starting a second agent when a route already resumed ([#3399](https://github.com/kdlbs/kandev/pull/3399)) by @nova28
|
||||
- read the stdin environment to EOF for bash on macOS ([#3385](https://github.com/kdlbs/kandev/pull/3385)) by @jmarbutt
|
||||
- brace refspec variables for zsh login shells ([#3383](https://github.com/kdlbs/kandev/pull/3383)) by @jmarbutt
|
||||
- harden prompt history and saved prompt aliases ([#3369](https://github.com/kdlbs/kandev/pull/3369)) by @Fclem
|
||||
- contain mobile Kanban archive confirmation ([#3430](https://github.com/kdlbs/kandev/pull/3430))
|
||||
- preserve remote OpenCode auth providers ([#3382](https://github.com/kdlbs/kandev/pull/3382)) ([#3421](https://github.com/kdlbs/kandev/pull/3421))
|
||||
- preserve latest task-create workflow ([#3413](https://github.com/kdlbs/kandev/pull/3413))
|
||||
- load and scroll to unloaded prompts ([#3412](https://github.com/kdlbs/kandev/pull/3412)) by @Fclem
|
||||
- preserve workflow move handoff prompts ([#3409](https://github.com/kdlbs/kandev/pull/3409)) ([#3415](https://github.com/kdlbs/kandev/pull/3415))
|
||||
- keep a usable agent selected when the executor changes ([#3388](https://github.com/kdlbs/kandev/pull/3388)) by @jmarbutt
|
||||
- stop a stale session snapshot from reverting a model switch ([#3379](https://github.com/kdlbs/kandev/pull/3379)) by @JnManso
|
||||
- preserve transcript position on task switch ([#3405](https://github.com/kdlbs/kandev/pull/3405))
|
||||
- stop a slow MCP server from stalling Claude for hours ([#3398](https://github.com/kdlbs/kandev/pull/3398)) by @nova28
|
||||
- stabilize immutable runtime image resolution ([#3397](https://github.com/kdlbs/kandev/pull/3397)) by @yattdev
|
||||
- align merge approval cleanup contract
|
||||
- repair stale approval merge-state query
|
||||
- restore PostgreSQL domain store parity ([#3372](https://github.com/kdlbs/kandev/pull/3372))
|
||||
- open registered repository links in file viewer ([#3400](https://github.com/kdlbs/kandev/pull/3400))
|
||||
- make passthrough composer touch-safe ([#2809](https://github.com/kdlbs/kandev/pull/2809)) ([#3303](https://github.com/kdlbs/kandev/pull/3303))
|
||||
- stop dynamic-route retries from double-launching the same prompt ([#3360](https://github.com/kdlbs/kandev/pull/3360)) by @nova28
|
||||
- support PostgreSQL task terminals ([#3371](https://github.com/kdlbs/kandev/pull/3371)) ([#3378](https://github.com/kdlbs/kandev/pull/3378))
|
||||
- preserve detached workspace ownership ([#3366](https://github.com/kdlbs/kandev/pull/3366)) ([#3374](https://github.com/kdlbs/kandev/pull/3374))
|
||||
- recover dynamic agent routes stuck in starting with no owner ([#3362](https://github.com/kdlbs/kandev/pull/3362)) by @nova28
|
||||
- use built-in token for runtime pin PRs ([#3365](https://github.com/kdlbs/kandev/pull/3365))
|
||||
- reject stale executor transition workspaces ([#3354](https://github.com/kdlbs/kandev/pull/3354)) by @WaleWangPW
|
||||
- stop disabled dynamic routing from resuming pending retries ([#3361](https://github.com/kdlbs/kandev/pull/3361)) by @nova28
|
||||
- backfill feeder pulls after task-tree lifecycle changes ([#3376](https://github.com/kdlbs/kandev/pull/3376))
|
||||
- make accordion menu the default ([#3368](https://github.com/kdlbs/kandev/pull/3368))
|
||||
- launch the dynamic fallback successor outside the agent.failed dispatch ([#3356](https://github.com/kdlbs/kandev/pull/3356)) by @iamapfelbaum
|
||||
- prevent silent internal bridge request loss ([#3364](https://github.com/kdlbs/kandev/pull/3364)) ([#3367](https://github.com/kdlbs/kandev/pull/3367))
|
||||
- compact TUI MCP strategy selector ([#3370](https://github.com/kdlbs/kandev/pull/3370))
|
||||
- make kanban boards recover from agent failures too ([#3315](https://github.com/kdlbs/kandev/pull/3315)) by @nova28
|
||||
- stop retries from hammering a failed credential after a restart ([#3357](https://github.com/kdlbs/kandev/pull/3357)) by @nova28
|
||||
- resolve Windows resume share-access violation ([#3353](https://github.com/kdlbs/kandev/pull/3353)) by @JnManso
|
||||
- keep change action spinners visible ([#3339](https://github.com/kdlbs/kandev/pull/3339))
|
||||
- detect backend restarts in open pages ([#3346](https://github.com/kdlbs/kandev/pull/3346))
|
||||
- quiet Claude passthrough by default ([#3305](https://github.com/kdlbs/kandev/pull/3305)) ([#3317](https://github.com/kdlbs/kandev/pull/3317))
|
||||
- classify OpenCode period usage limits so dynamic routing can advance ([#3320](https://github.com/kdlbs/kandev/pull/3320)) by @iamapfelbaum
|
||||
- restore compact coarse-pointer file tree rows ([#3326](https://github.com/kdlbs/kandev/pull/3326)) by @Fclem
|
||||
- keep model selector visible after model switches ([#3349](https://github.com/kdlbs/kandev/pull/3349))
|
||||
- recover pathless worktree failures ([#3335](https://github.com/kdlbs/kandev/pull/3335)) ([#3351](https://github.com/kdlbs/kandev/pull/3351))
|
||||
- recover SSH sessions after dead runtime ([#3330](https://github.com/kdlbs/kandev/pull/3330)) ([#3350](https://github.com/kdlbs/kandev/pull/3350))
|
||||
- stop superseded session worktrees claiming cutover slots ([#3328](https://github.com/kdlbs/kandev/pull/3328)) by @tito
|
||||
- registering a reviewer stops duplicating the auto-picked seat ([#3327](https://github.com/kdlbs/kandev/pull/3327)) by @nova28
|
||||
- stop workflow step transitions from leaking claude subprocesses ([#3338](https://github.com/kdlbs/kandev/pull/3338)) by @nova28
|
||||
- fixes MCP issue where auto_approve is silently ignored in agent profile create/update. ([#3331](https://github.com/kdlbs/kandev/pull/3331)) by @meanderix
|
||||
- stop clarification answers from timing out on large message tables ([#3322](https://github.com/kdlbs/kandev/pull/3322)) by @nova28
|
||||
- retain PR commit provenance across refreshes ([#3341](https://github.com/kdlbs/kandev/pull/3341))
|
||||
- restore chat scroll on env-changing task switch ([#3342](https://github.com/kdlbs/kandev/pull/3342))
|
||||
- enable coarse-pointer terminal scrolling ([#2808](https://github.com/kdlbs/kandev/pull/2808)) ([#3301](https://github.com/kdlbs/kandev/pull/3301))
|
||||
- stop chat from saying no utility agent is configured after you save one ([#3306](https://github.com/kdlbs/kandev/pull/3306)) by @nova28
|
||||
- deduplicate empty workflow prompts ([#3299](https://github.com/kdlbs/kandev/pull/3299))
|
||||
- stop the reject leg from waking the reviewer, not the assignee ([#3294](https://github.com/kdlbs/kandev/pull/3294)) by @nova28
|
||||
- guard session resumption across task navigation ([#3302](https://github.com/kdlbs/kandev/pull/3302))
|
||||
- contain a panicking subscriber so it stops crashing event delivery ([#3295](https://github.com/kdlbs/kandev/pull/3295)) by @nova28
|
||||
- stop a failing CEO agent from re-queuing itself forever ([#3296](https://github.com/kdlbs/kandev/pull/3296)) by @nova28
|
||||
- stop parent tasks waking twice when children finish ([#3271](https://github.com/kdlbs/kandev/pull/3271)) by @nova28
|
||||
- preserve qualified remote refs in watcher branches ([#3268](https://github.com/kdlbs/kandev/pull/3268))
|
||||
- preserve transcript scroll across session activation ([#3291](https://github.com/kdlbs/kandev/pull/3291))
|
||||
- cap the plan text pasted into every new agent session ([#3280](https://github.com/kdlbs/kandev/pull/3280)) by @nova28
|
||||
- recognize reviewer decisions from agents who also hold the approver seat ([#3270](https://github.com/kdlbs/kandev/pull/3270)) by @nova28
|
||||
- escalate reviewer and approver agent failures to the CEO ([#3275](https://github.com/kdlbs/kandev/pull/3275)) by @nova28
|
||||
- fire budget alerts when reassigning a task pushes a project over budget ([#3276](https://github.com/kdlbs/kandev/pull/3276)) by @nova28
|
||||
- materialize attachments sent into a generating turn ([#3277](https://github.com/kdlbs/kandev/pull/3277)) by @Corey-Fogg
|
||||
- allow typing while sessions start ([#3285](https://github.com/kdlbs/kandev/pull/3285))
|
||||
- preserve plan comments across session switches ([#3272](https://github.com/kdlbs/kandev/pull/3272))
|
||||
- stop idle-skip from swallowing manual wakeups queued behind a cron run ([#3281](https://github.com/kdlbs/kandev/pull/3281)) by @nova28
|
||||
- allow the stall watchdog to archive its own task ([#3260](https://github.com/kdlbs/kandev/pull/3260)) by @nova28
|
||||
- stop failed answer submits from vanishing silently ([#3263](https://github.com/kdlbs/kandev/pull/3263)) by @nova28
|
||||
- normalize skill slugs to canonical form on write and sync ([#3257](https://github.com/kdlbs/kandev/pull/3257)) by @nova28
|
||||
|
||||
### Performance
|
||||
|
||||
- stream frontend diagnostic captures ([#3540](https://github.com/kdlbs/kandev/pull/3540))
|
||||
- isolate task surface rendering ([#3226](https://github.com/kdlbs/kandev/pull/3226))
|
||||
- isolate sidebar task tree renders ([#3435](https://github.com/kdlbs/kandev/pull/3435))
|
||||
- isolate kanban task update renders ([#3436](https://github.com/kdlbs/kandev/pull/3436))
|
||||
|
||||
### Refactoring
|
||||
|
||||
- remove legacy websocket request API ([#3431](https://github.com/kdlbs/kandev/pull/3431))
|
||||
- remove unused recreator ([#3432](https://github.com/kdlbs/kandev/pull/3432))
|
||||
- remove deprecated model fetcher cache ([#3433](https://github.com/kdlbs/kandev/pull/3433))
|
||||
- stop hand-syncing the dispatcher's active-state list with the SQL filter ([#3274](https://github.com/kdlbs/kandev/pull/3274)) by @nova28
|
||||
|
||||
### Documentation
|
||||
|
||||
- correct stale unique-index precondition for office session identity ([#3513](https://github.com/kdlbs/kandev/pull/3513)) by @nova28
|
||||
- mark shared access features experimental ([#3428](https://github.com/kdlbs/kandev/pull/3428))
|
||||
- add branded diagram design guidance ([#3316](https://github.com/kdlbs/kandev/pull/3316))
|
||||
- document mobile remote access ([#2807](https://github.com/kdlbs/kandev/pull/2807)) ([#3300](https://github.com/kdlbs/kandev/pull/3300))
|
||||
- stop postgres and nats bugs from shipping green through verify ([#3273](https://github.com/kdlbs/kandev/pull/3273)) by @nova28
|
||||
|
||||
## 0.93.0 - 2026-09-02
|
||||
|
||||
### Features
|
||||
|
||||
@@ -30,6 +30,12 @@ discussed the direction before opening the PR. Link the issue from the PR. If
|
||||
an agent is preparing the change, it must stop and report missing discussion
|
||||
instead of opening the PR.
|
||||
|
||||
### Documentation coverage
|
||||
|
||||
When implementing a feature or fix, update the relevant requirements, system
|
||||
design, plan, and work-order files. CI runs a linter to check PR documentation
|
||||
coverage.
|
||||
|
||||
## How to Contribute
|
||||
|
||||
1. **Fork and branch.** Create a feature branch from `main`.
|
||||
|
||||
@@ -559,6 +559,7 @@ test-cli:
|
||||
test-scripts:
|
||||
@printf "$(CYAN)Running script tests...$(RESET)\n"
|
||||
@python3 .github/scripts/lint-action-pinning_test.py
|
||||
@node --test .github/scripts/pr-docs.test.cjs
|
||||
@bash scripts/pr-state.test.sh
|
||||
@bash scripts/pr-await.test.sh
|
||||
@bash scripts/run-quiet.test.sh
|
||||
|
||||
@@ -21,6 +21,18 @@ Run it locally or self-host it on your own infrastructure. Use the [mobile remot
|
||||
|
||||
Open source, multi-provider, no telemetry, not tied to any cloud.
|
||||
|
||||
## Distribution
|
||||
|
||||
Kandev is distributed as a native Go binary for each supported platform. The
|
||||
compiled web frontend is embedded in that binary. The binary serves the web UI
|
||||
and API, so the application server does not need Node.js, a separate web
|
||||
server, or a frontend build at runtime.
|
||||
|
||||
Homebrew, Scoop, release archives, and the desktop app run this native binary
|
||||
directly. The npm/npx package adds a small Node.js platform selector, so Node.js
|
||||
is required to launch Kandev through npm/npx but not by the application server.
|
||||
Release bundles also include `agentctl` helpers for task environments.
|
||||
|
||||
## Vision
|
||||
|
||||
> **Humans stay in control.** Define tasks, build agentic workflows with gates, review every change, decide what ships.
|
||||
@@ -236,7 +248,7 @@ We also want to add support for this remote runtime:
|
||||
apps/
|
||||
├── backend/ # Go backend (orchestrator, lifecycle, agentctl, WS gateway)
|
||||
├── web/ # Vite/React frontend (SPA, Zustand, real-time subscriptions)
|
||||
├── cli/ # CLI tool (npx kandev launcher)
|
||||
├── cli/ # npm shim for the native Go runtime
|
||||
├── desktop/ # Tauri desktop shell
|
||||
└── packages/ # Shared UI components & types
|
||||
```
|
||||
|
||||
+16
-17
@@ -97,6 +97,14 @@ apps/backend/
|
||||
│ └── worktree/ # Git worktree management for workspace isolation
|
||||
```
|
||||
|
||||
Canvas creation authority is recorded only by the trusted task authoring
|
||||
adapter. It binds the workspace owner, creating session, task scope, and
|
||||
policy version to a new draft. The first valid static web-app release may
|
||||
consume that single-use authority for exact task-scoped grants in the same
|
||||
transaction as release insertion and activation. Existing drafts, imports,
|
||||
later permission increases, and revoked grants remain on the human review
|
||||
path. Never use source metadata or a manifest trust field as authority.
|
||||
|
||||
## Key Concepts
|
||||
|
||||
**Orchestrator** coordinates task execution:
|
||||
@@ -144,7 +152,7 @@ replace state verification, installation association, or HMAC verification.
|
||||
- `streams.go` - WebSocket stream connections to agentctl
|
||||
- `process_runner.go` - agent process launch and management
|
||||
- `profile_resolver.go` - resolves agent profiles/settings
|
||||
**Lifecycle callback identity:** Process callbacks must carry the launched PID and generation; ignore delayed callbacks from replaced processes and duplicate callbacks, with tests for both old-generation and duplicate delivery.
|
||||
**Lifecycle callback identity:** Process callbacks must retain the launched PID and generation captured when scheduled, revalidate both before state/I/O, and ignore delayed callbacks from replaced processes and duplicates; test replacement before start and after waits. Prompt callbacks and asynchronous prompt errors must likewise carry immutable execution, session, prompt-generation, and turn evidence captured at the result boundary; never reread mutable execution snapshots later, because a successor prompt may already own them. Settle through the correlated terminal path and test both replacement-execution and same-execution successor-prompt races.
|
||||
|
||||
**agentctl client** (`internal/agent/runtime/agentctl/`) is the HTTP/WS client used by the lifecycle manager to talk to a running agentctl instance. It is a runtime-tier package and should not be imported outside `internal/agent/runtime/`.
|
||||
|
||||
@@ -218,29 +226,20 @@ Client (WS) ← Orchestrator ← Lifecycle Manager ←──── stream update
|
||||
- Pass context through chains; event bus for cross-component comm.
|
||||
- **Dependency direction:** Shared admission limits used by task models and orchestrator/messagequeue belong in a neutral internal package; task/models must not import the higher-level orchestrator/messagequeue package.
|
||||
- Production Git commands must use `subproc.NewGitCommand` with a classified `subproc.RunGit*` helper, or hold a classified admission slot across streaming `Start`/`Wait`. Do not construct raw Git commands outside `internal/common/subproc`; choose `interactive`, `lifecycle`, or `background`.
|
||||
- Managed Git final preparation runs immediately before `Start` and owns the full `Start`-to-`Wait` lifecycle. Network callers choose finite post-admission budgets; use `AfterAcquire` builders so queue wait is outside the execution budget. Shared lifecycle code belongs in `internal/common`, and agentctl keeps only compatibility wrappers.
|
||||
- **Cross-tier shared code belongs in `internal/common/`.** agentctl ships as a standalone binary uploaded into containers, so neither it nor the backend may own code the other imports — put shared logic in a neutral `internal/common/*` package instead of importing across the boundary or forking a copy (`ptyexec` is the worked example; `subproc`, `gitref`, `securityutil` are older ones). Duplicating to "avoid the dependency" is how the two PTY copies drifted.
|
||||
- **Startup configuration ownership:** `internal/common/config/catalog.go` and `source.go` own the stable operator catalog, discovery, precedence, provenance, and typed values; managed agentctl children receive a private subset contract. Do not reparse stable env in executors or copy YAML into public child environments.
|
||||
- **Pure computation that only matters on Windows should not carry `//go:build windows`.** One CI job runs on Windows and it tests a package allowlist, so tagged code is easily unverified — loginpty's copy of the Win32 quoting was compiled by no job at all. Keep platform API calls behind the tag, leave string/path helpers untagged, and add packages needing native coverage to the `test-windows` job in `.github/workflows/backend-tests.yml`.
|
||||
- **Event-bus wildcard parity:** New NATS wildcard subscriptions must verify equivalent `MemoryEventBus` semantics in `go test ./internal/events/bus`.
|
||||
- **Event-bus wildcard parity:** New NATS wildcard subscriptions must verify equivalent `MemoryEventBus` semantics in `go test ./internal/events/bus`. `EventBus.Publish` completion means transport/enqueue, not remote NATS subscriber processing; never use publication as a local ordering acknowledgement. If a waiter must not release until local state is armed, use a construction-supplied synchronous local notifier or durable state transition before publishing fan-out, and test with an async/NATS-like bus fake as well as `MemoryEventBus`.
|
||||
- **Repository provider identity:** Provider-backed repositories are keyed by workspace, provider, normalized `provider_host` origin, full owner/namespace, and name. Persist `provider_host` when importing or resolving a remote; do not infer self-managed GitLab rows from owner/name alone. Legacy rows with an empty host have unknown identity and must fail closed for provider write/link operations.
|
||||
- **Repository provider branches:** Native task branch pickers derive provider identity only from the persisted workspace repository. GitHub uses the first-party service; manifest-owned providers route through the owning active plugin's workspace-scoped `repositories.branches` action. Do not add provider-specific task-service branches or accept browser-supplied repository descriptors on this path.
|
||||
- **PR status sync coverage:** A `github_pr_watches` row is unique per (session, repository, branch). A watch that has **found a PR** is never re-pointed: it is that PR's only sync handle, and both the poller and the on-demand sync iterate watches, so re-pointing it froze the PR at its last-observed checks/review state — which the UI renders as an open, mergeable PR long after it merged, and which silently disabled CI auto-fix and auto-merge for every branch but the one currently checked out. Branch switches (`resetPRWatchForBranchSwitch`) and the git-status branch sync (`syncPRWatchBranch`) therefore only re-point a **searching** watch (`pr_number=0`) of the same repository, and otherwise add a watch for the new branch — one searching watch per (session, repository) plus one per PR actually opened. Watches are still not a complete index of a task's PRs (sessions get deleted, PRs get linked by URL), so the orphan sweep stays. `service_pr_unwatched.go` reconciles the `github_task_prs` rows no watch points at, and deliberately writes **lifecycle fields only** (`state` / `merged_at` / `closed_at`) — check and review aggregates belong to the watch-driven sync that actually fetches reviews and check runs, and the per-PR REST status marks its counts populated even with nothing to count, so widening this path to aggregates persists 0/0 over good data (it broke six PR-popover e2e specs exactly that way). Terminal and detached rows are excluded so the set stays bounded. **The backend is the only writer of `github_task_prs`:** the PR-feedback read makes the same upstream calls the status sync does (`GetPR` + `ListPRReviews` + `ListCheckRuns`, plus comments), so it persists through `SyncTaskPR` (`service_pr_feedback_sync.go`) and lets `github.task_pr.updated` converge every surface. The frontend must never patch PR lifecycle fields into its own store copy — that write cannot reach the DB, so it desyncs the panel from the kanban card and evaporates on reload (it also masked the sync hole above for months). Both derivations share `newPRStatus` so they cannot drift; the persist rides *inside* the TTL cache's fetch closure, so a cache hit costs no write; failures are logged and swallowed because this is a read endpoint; and the row lookup is workspace-scoped (`ListTaskPRsByPRNumber`) to match the credential scope the read was authorized under. Finally, `merged` is the one state `SyncTaskPR` refuses to leave (`resolveTerminalMergeState`) since GitHub cannot un-merge a PR — do **not** generalize that into a `merged > closed > open` rank, because `closed -> open` is a real transition (a PR can be reopened) and ranking it as a regression pins reopened PRs closed forever. **The batched branch query's empty answer is authoritative:** an alias resolving with **zero** nodes has definitively said "no open PR on this branch", so `branchBatchResult.ResolvedEmpty` carries that to `applyBatchedSearchingWatch`, which must not re-ask it through a per-watch `client.FindPRByBranch`. That re-ask was one `gh pr list` (GraphQL) plus one `gh api /repos/...` per searching watch per cycle *forever*, since a branch with no PR keeps that shape — measured at 5,200-9,200 GraphQL points/hour against a 5,000/hour limit with 42 searching watches, parking the poller on `github rate-limited` ~40 min into every reset window. On a definitive negative only the fork **parent** (a repository the batch never asked about) is worth a look, and `forkParentRepositoryForLookup` caches fork identity per (credential scope, owner, repo) since that answer is constant. Two-or-more open PRs is **not** definitive — `selectBatchedBranchPRNode` refuses to guess — so that case and an unresolved alias keep the full fork-network fallback, throttled on the watch's `last_checked_at` (`PRSyncFreshnessWindow`) exactly as `triggerPRDetection` is; the batched path is the production path, so a throttle guarding only the legacy per-watch path guards nothing. Changes here need a **call-count** test (`service_pr_watch_batched_budget_test.go`) — an assertion on the resulting `PRStatus` passes with the whole regression restored.
|
||||
- **PR status sync:** For watch identity, PR lifecycle persistence, or batched lookup changes, read [GitHub guidance](internal/github/AGENTS.md).
|
||||
- **Execution access:** Workspace-oriented handlers (files, shell, inference, ports, vscode, LSP) MUST use `GetOrEnsureExecution(ctx, sessionID)` — it recovers from backend restarts by creating executions on-demand. Only use `GetExecutionBySessionID` for operations that require a running agent process (prompt, cancel, mode).
|
||||
- **Generated-title ownership:** Claim generated-title ownership only after task, config, and Office eligibility is known. Config tasks, Office/External modes, and workspace-only preparation (`StartAgent: false`) must never claim it; only an eligible agent-start task may claim the generated title.
|
||||
- **Task lifecycle events:** Any code path that mutates a task row must publish via the event bus (`task.created` / `task.updated` / `task.deleted`) — either by going through `Service.CreateTask` / `UpdateTask` / `DeleteTask` / `ArchiveTask`, or by calling `publishTaskEvent` (or one of the `Publish*` helpers in `service_events.go`) directly. Walking `repository.TaskRepository` straight bypasses event publishing and breaks WS-driven UI like the All-Workflows kanban view. `HandoffService`'s cascade methods learned this the hard way — they now require a `TaskEventPublisher` wired via `SetTaskEventPublisher`. New cascade / bulk / cleanup paths must follow the same pattern. **Workflow steps** follow the same rule with their own publisher: step create/update/delete publish `workflow_step.created` / `.updated` / `.deleted` through `internal/workflow/stepevents.Publisher` (the WS gateway fans them out; the orchestrator re-evaluates queue admission off `.updated`). Both mutation surfaces — REST/WS in `internal/workflow/handlers`, MCP in `internal/mcp/handlers` — share that publisher and differ only in the source label they pass, so a new step-mutation path uses it rather than hand-rolling the payload; promoting a start step demotes the previous one, so also publish `.updated` for every entry in `DemotedStartSteps`.
|
||||
- **Workspace deletion side tables:** Any workspace-scoped integration side table without a database foreign key/cascade must be deleted explicitly from its `WorkspaceDeleted` handler. Cover the create → delete lifecycle in a test, and include the same cleanup in E2E reset fixtures; do not assume deleting the workspace row removes orphaned integration metadata.
|
||||
- **Testing:** Prefer `testing/synctest` (Go 1.24+) over `time.Sleep` for time-dependent tests. Use `synctest.Test` to wrap tests with tickers or timeouts — it advances fake time instantly when all goroutines are idle. When `synctest` is not feasible (e.g., tests spawning external processes like `git`), use channel-based synchronization (`<-started`, non-blocking `select`) instead of sleep-based waits. Reserve `time.Sleep` only for integration tests that need real subprocess execution time.
|
||||
- **Test cleanup:** Register `t.Cleanup` immediately after creating resources that need teardown (adapters, `io.Pipe` writers, background goroutines) — before any `t.Fatal`/`t.Fatalf` path. Late cleanup registration leaks pipes and goroutines on early failure.
|
||||
- **Joining production goroutines in tests:** When code spawns untracked goroutines (e.g. `fireWakeup`), don't rely on arbitrary sleeps. Join via an observable side effect — e.g. block on `EventTypeComplete` from `a.updatesCh` after unblocking the fake agent. Use short timeouts (~100ms) for in-process negative assertions; reserve multi-second waits for subprocess/integration tests only.
|
||||
- **Path/security tests:** Avoid using the real filesystem root as a fixture root. Build fake absolute roots under `t.TempDir()` with `filepath.Join`; this keeps tests portable across Windows, POSIX, and privileged cloud executors. For error assertions, do not compare raw `%q`/escaped strings with native `filepath` values; assert typed fields or normalize both sides with `filepath.Clean`/`filepath.FromSlash`.
|
||||
- **Filesystem permission tests:** Assert permission-denied behavior only after probing that the current executor enforces the permission bit change. Root-like Sprite executors may bypass `chmod` restrictions.
|
||||
- **Filesystem safety checks:** Carry the original `os.Lstat` `FileInfo` (or opened handle) through every decision; do not re-stat a validated path, which reopens a TOCTOU window before side effects. Test mismatches before writes or manager commands.
|
||||
- **Git indexed-environment tests:** When a test supplies `GIT_CONFIG_COUNT`, `GIT_CONFIG_KEY_n`, and `GIT_CONFIG_VALUE_n`, clear every inherited indexed key first and restore it with `t.Cleanup`. `internal/gitconfigenv` ignores indexes past the count; uncleared parent indexes silently alter the block instead of failing loudly.
|
||||
- **Derived environment contracts:** Drive derived values through the production producer/wiring seam instead of manually seeding keys; pair consumer/process coverage with a producer-boundary assertion so missing publication fails.
|
||||
- **GitLab host-trust tests:** `internal/gitlab`'s `TestMain` in `goleak_test.go` calls the scrub in `env_hermetic_test.go`; `internal/agentctl/server/process`'s `TestMain` clears ambient `KANDEV_GITLAB_HOST`/`GITLAB_HOST`/`GITLAB_TOKEN`. `test_ambient_env` runs both poisoned to catch a regression neither package's clean-env CI shard could catch.
|
||||
- **Full test output:** For local full-suite pass/fail validation, prefer plain `go test -race ./...`. `go test -json ./...` can emit very large JSONL streams; if a wrapper or tracing tool truncates the stream mid-record, downstream JSON parsing may fail even when Go tests passed. Use JSON output mainly for CI artifacts or test-report tooling that explicitly requires it.
|
||||
- **Private executable helpers:** When library code starts `os.Executable()` in a private helper mode, do not rely only on a command package's dispatch or package-specific `TestMain`: an importing package's test binary can be the executable and recursively run tests.
|
||||
Use either an explicitly marked private env-and-argument trampoline before normal test entry, or inject a dedicated helper executable. Cover it from a different importing package, not only the helper package.
|
||||
- **Testing:** For backend test changes, load [backend-tests.md](../../.agents/skills/tdd/references/backend-tests.md).
|
||||
It covers time, cleanup, filesystem fixtures, environment isolation, and subprocess helpers.
|
||||
|
||||
### Goroutine ownership and leak testing
|
||||
|
||||
@@ -253,7 +252,7 @@ Every long-running goroutine must have a single owner with explicit start and st
|
||||
- **Leak testing:** packages that spawn goroutines add `goleak.VerifyTestMain(m)` in a per-package `TestMain`. New packages of this kind must follow suit. When a third-party background goroutine genuinely can't be drained, suppress it with `goleak.IgnoreTopFunction(...)` and leave a comment explaining why. Currently instrumented: `internal/gateway/websocket/`, `internal/agent/runtime/lifecycle/`, `internal/agentctl/server/process/`, `internal/orchestrator/`, `internal/github/`, `internal/gitlab/`, `internal/jira/`, `internal/linear/`, `internal/integrations/healthpoll/`.
|
||||
|
||||
## Backups
|
||||
- On every SQLite boot, `persistence.Provide` reads `kandev_meta.kandev_version`. If the stored version differs from the binary version (or any user tables exist but no version is recorded), it takes a `VACUUM INTO` snapshot into `backups/` beside the configured SQLite database file before running migrations. The default path is `<home>/data/kandev.db`, with backups in `<home>/data/backups/`.
|
||||
- On every SQLite boot, `persistence.Provide` reads `kandev_meta.kandev_version`. If the stored version differs from the binary version (or any user tables exist but no version is recorded), it takes a `VACUUM INTO` snapshot into `backups/` beside the configured SQLite database file before running migrations. `VACUUM INTO` recreates its target, so create staged snapshots inside a same-filesystem private `0700` staging directory and chmod the file to `0600` before validation/install; removing a placeholder and chmodding afterward does not protect the creation window. The default path is `<home>/data/kandev.db`, with backups in `<home>/data/backups/`.
|
||||
- Retention: 2 backups kept (newest two by mtime); older ones are pruned after the snapshot succeeds.
|
||||
- Postgres: backup is skipped with a log line. Use `pg_dump` for Postgres backups.
|
||||
- Boot aborts if the backup fails — the pool is closed and `Provide` returns an error.
|
||||
@@ -264,7 +263,7 @@ Every long-running goroutine must have a single owner with explicit start and st
|
||||
|
||||
## Schema & migrations (SQLite repository)
|
||||
|
||||
`internal/task/repository/sqlite` also runs against PostgreSQL: avoid unguarded SQLite-only `rowid`, JSON, or date syntax and add an environment-gated PostgreSQL behavior test for every changed dialect-sensitive method (schema replay is insufficient).
|
||||
`internal/task/repository/sqlite` also runs against PostgreSQL: avoid unguarded SQLite-only `rowid`, JSON, or date syntax and add an environment-gated PostgreSQL behavior test for every changed dialect-sensitive method (schema replay is insufficient). For PostgreSQL read-modify-write invariants, acquire a transaction-scoped `pg_advisory_xact_lock(hashtextextended(namespace+identity, 0))` before the first read or update; SQLite's single-writer lock does not need this, and unique constraints/retries remain defense in depth. Verify with an env-gated real multi-connection test under `go test -race`.
|
||||
|
||||
`initSchema()` in `internal/task/repository/sqlite/base_schema.go` runs the `init*Schema` (CREATE TABLE) steps **before** `runMigrations()`. The table-creation DDL uses `CREATE TABLE IF NOT EXISTS`, so on an **existing** database it is a no-op and never adds columns to a table that is already present.
|
||||
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/kandev/kandev/internal/agentctl/server/config"
|
||||
"github.com/kandev/kandev/internal/common/logger"
|
||||
)
|
||||
|
||||
// F71 bound values for agentctl's own detached diagnostic log
|
||||
// (AC-EXECUTORS-SURVIVAL-001.3): fixed implementation constants, not
|
||||
// operator settings -- the sink adds no configuration key and no
|
||||
// environment variable of its own. Chosen to mirror the backend's own
|
||||
// logger's order of magnitude while staying a visibly distinct budget.
|
||||
const (
|
||||
diagnosticLogMaxSizeMB = 16
|
||||
diagnosticLogMaxBackups = 10
|
||||
diagnosticLogMaxAgeDays = 14
|
||||
)
|
||||
|
||||
// diagnosticLoggingConfig builds the file-backed, bounded logging
|
||||
// configuration for agentctl's own diagnostic output while it may be
|
||||
// running detached from any backend. Not yet wired into runMain's logger
|
||||
// construction: switching away from stdout is a decision that depends on
|
||||
// whether the agent-survival capability is engaged for this launch, which
|
||||
// Layer 5.9 threads into agentctl's config (kill-path #6, "inherited
|
||||
// stdout", is only a problem once that capability is active). This function
|
||||
// exists so 5.9 has a ready, already-tested LoggingConfig to switch to
|
||||
// instead of hand-assembling one at the call site.
|
||||
func diagnosticLoggingConfig(level, format, diagnosticLogPath string) logger.LoggingConfig {
|
||||
return logger.LoggingConfig{
|
||||
Level: level,
|
||||
Format: format,
|
||||
OutputPath: diagnosticLogPath,
|
||||
MaxSizeMB: diagnosticLogMaxSizeMB,
|
||||
MaxBackups: diagnosticLogMaxBackups,
|
||||
MaxAgeDays: diagnosticLogMaxAgeDays,
|
||||
}
|
||||
}
|
||||
|
||||
// resolveRunLoggingConfig is Layer 5.9's kill-path #6 gate ("inherited
|
||||
// stdout", design 01's kill paths): stdout is only a liveness hazard once
|
||||
// the agent-survival capability is actually engaged for this launch, so
|
||||
// every other case -- capability disabled, or a diagnostic log path that
|
||||
// failed to resolve for some reason -- keeps today's stdout logging
|
||||
// unchanged rather than risk an unwritable sink.
|
||||
func resolveRunLoggingConfig(cfg *config.Config) logger.LoggingConfig {
|
||||
if cfg.AgentSurvivalEnabled && cfg.DiagnosticLogPath != "" {
|
||||
return diagnosticLoggingConfig(cfg.LogLevel, cfg.LogFormat, cfg.DiagnosticLogPath)
|
||||
}
|
||||
return logger.LoggingConfig{
|
||||
Level: cfg.LogLevel,
|
||||
Format: cfg.LogFormat,
|
||||
OutputPath: "stdout",
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/kandev/kandev/internal/agentctl/server/config"
|
||||
"github.com/kandev/kandev/internal/common/logger"
|
||||
)
|
||||
|
||||
// TestDiagnosticLoggingConfigAppliesTheF71Bounds pins the fixed bound values
|
||||
// Build picked for AC-EXECUTORS-SURVIVAL-001.3 (MaxSizeMB=16, MaxBackups=10,
|
||||
// MaxAgeDays=14) and that the level/format/path passed through are used
|
||||
// verbatim, not silently overridden.
|
||||
func TestDiagnosticLoggingConfigAppliesTheF71Bounds(t *testing.T) {
|
||||
got := diagnosticLoggingConfig("debug", "json", "/home/kandev-test/.kandev/logs/agentctl-diagnostic.log")
|
||||
|
||||
want := logger.LoggingConfig{
|
||||
Level: "debug",
|
||||
Format: "json",
|
||||
OutputPath: "/home/kandev-test/.kandev/logs/agentctl-diagnostic.log",
|
||||
MaxSizeMB: 16,
|
||||
MaxBackups: 10,
|
||||
MaxAgeDays: 14,
|
||||
}
|
||||
if got != want {
|
||||
t.Fatalf("diagnosticLoggingConfig() = %+v, want %+v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
// TestDiagnosticLoggingConfigWritesToADurableFile proves the wire contract
|
||||
// end to end: constructing a real *logger.Logger from diagnosticLoggingConfig
|
||||
// and writing a line actually lands in the named file, rather than a
|
||||
// hand-fabricated fixture asserting only on the struct fields.
|
||||
func TestDiagnosticLoggingConfigWritesToADurableFile(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "logs", "agentctl-diagnostic.log")
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
||||
t.Fatalf("MkdirAll: %v", err)
|
||||
}
|
||||
|
||||
log, err := logger.NewLogger(diagnosticLoggingConfig("info", "json", path))
|
||||
if err != nil {
|
||||
t.Fatalf("NewLogger: %v", err)
|
||||
}
|
||||
log.Info("agentctl running detached")
|
||||
if err := log.Close(); err != nil {
|
||||
t.Fatalf("Close: %v", err)
|
||||
}
|
||||
|
||||
contents, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatalf("ReadFile(%q): %v", path, err)
|
||||
}
|
||||
if !strings.Contains(string(contents), "agentctl running detached") {
|
||||
t.Fatalf("diagnostic log file contents = %q, want it to contain the logged message", contents)
|
||||
}
|
||||
}
|
||||
|
||||
// TestResolveRunLoggingConfigStaysOnStdoutUnlessSurvivalIsEngaged pins the
|
||||
// Layer 5.9 gate: kill-path #6 ("inherited stdout", see design 01's kill
|
||||
// paths) is only a problem once the capability is actually active for this
|
||||
// launch, so every other case -- disabled, or a diagnostic log path that
|
||||
// somehow failed to resolve -- must keep today's stdout behavior byte-for-
|
||||
// byte rather than silently switching sinks.
|
||||
func TestResolveRunLoggingConfigStaysOnStdoutUnlessSurvivalIsEngaged(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
agentSurvivalEnabled bool
|
||||
diagnosticLogPath string
|
||||
wantStdout bool
|
||||
}{
|
||||
{"disabled with a resolved path", false, "/home/kandev-test/.kandev/logs/agentctl-diagnostic.log", true},
|
||||
{"enabled but path unresolved", true, "", true},
|
||||
{"enabled with a resolved path", true, "/home/kandev-test/.kandev/logs/agentctl-diagnostic.log", false},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
cfg := &config.Config{
|
||||
LogLevel: "debug",
|
||||
LogFormat: "json",
|
||||
AgentSurvivalEnabled: tc.agentSurvivalEnabled,
|
||||
DiagnosticLogPath: tc.diagnosticLogPath,
|
||||
}
|
||||
got := resolveRunLoggingConfig(cfg)
|
||||
if tc.wantStdout {
|
||||
want := logger.LoggingConfig{Level: "debug", Format: "json", OutputPath: "stdout"}
|
||||
if got != want {
|
||||
t.Fatalf("resolveRunLoggingConfig() = %+v, want %+v", got, want)
|
||||
}
|
||||
return
|
||||
}
|
||||
want := diagnosticLoggingConfig("debug", "json", tc.diagnosticLogPath)
|
||||
if got != want {
|
||||
t.Fatalf("resolveRunLoggingConfig() = %+v, want %+v", got, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -7,9 +7,14 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
decisionApproved = "approved"
|
||||
decisionRejected = "rejected"
|
||||
)
|
||||
|
||||
func runTaskCmd(args []string) int {
|
||||
if len(args) == 0 {
|
||||
cliError("usage: agentctl kandev task <get|update|create> [flags]")
|
||||
cliError("usage: agentctl kandev task <get|update|create|decision> [flags]")
|
||||
return 1
|
||||
}
|
||||
switch args[0] {
|
||||
@@ -19,12 +24,51 @@ func runTaskCmd(args []string) int {
|
||||
return taskUpdate(args[1:])
|
||||
case subcmdCreate:
|
||||
return taskCreate(args[1:])
|
||||
case "decision":
|
||||
return taskDecision(args[1:])
|
||||
default:
|
||||
cliError("unknown task subcommand: %s", args[0])
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
// taskDecision records the caller's workflow-step verdict through the signed
|
||||
// Office runtime API. The runtime derives every identity field from the token.
|
||||
func taskDecision(args []string) int {
|
||||
fs := flag.NewFlagSet("task decision", flag.ContinueOnError)
|
||||
decision := fs.String("decision", "", fmt.Sprintf("Verdict: %s or %s (required)", decisionApproved, decisionRejected))
|
||||
reason := fs.String("reason", "", "Reason for the verdict (required)")
|
||||
if err := fs.Parse(args); err != nil {
|
||||
cliError("parse flags: %v", err)
|
||||
return 1
|
||||
}
|
||||
if fs.NArg() > 0 {
|
||||
cliError("unexpected arguments: %s", strings.Join(fs.Args(), " "))
|
||||
return 1
|
||||
}
|
||||
switch *decision {
|
||||
case decisionApproved, decisionRejected:
|
||||
default:
|
||||
cliError("--decision must be %q or %q", decisionApproved, decisionRejected)
|
||||
return 1
|
||||
}
|
||||
if strings.TrimSpace(*reason) == "" {
|
||||
cliError("--reason is required")
|
||||
return 1
|
||||
}
|
||||
|
||||
client, err := newKandevClient()
|
||||
if err != nil {
|
||||
cliError("%v", err)
|
||||
return 1
|
||||
}
|
||||
body, status, err := client.do(http.MethodPost, "/api/v1/office/runtime/task/decision", map[string]string{
|
||||
"decision": *decision,
|
||||
"reason": *reason,
|
||||
})
|
||||
return handleResponse(body, status, err)
|
||||
}
|
||||
|
||||
// taskGet fetches a task by ID. Defaults to $KANDEV_TASK_ID when --id is omitted.
|
||||
func taskGet(args []string) int {
|
||||
fs := flag.NewFlagSet("task get", flag.ContinueOnError)
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestTaskDecision_PostsOnlyDecisionAndReasonToRuntimeEndpoint(t *testing.T) {
|
||||
captured := setupMockTransport(t, http.StatusOK, `{"decision":"approved"}`)
|
||||
t.Setenv("KANDEV_API_URL", "http://kandev.test")
|
||||
t.Setenv("KANDEV_API_KEY", "signed-office-run-token")
|
||||
t.Setenv("KANDEV_RUN_ID", "run-456")
|
||||
t.Setenv("KANDEV_TASK_ID", "task-from-environment")
|
||||
t.Setenv("KANDEV_AGENT_ID", "agent-from-environment")
|
||||
|
||||
code := runKandevCLI([]string{
|
||||
"task", "decision", "--decision", "approved", "--reason", "looks good",
|
||||
})
|
||||
if code != 0 {
|
||||
t.Fatalf("task decision exit = %d, want 0", code)
|
||||
}
|
||||
if captured.Method != http.MethodPost || captured.Path != "/api/v1/office/runtime/task/decision" {
|
||||
t.Fatalf("request = %s %s, want POST /api/v1/office/runtime/task/decision", captured.Method, captured.Path)
|
||||
}
|
||||
assertRunIDHeader(t, captured, "run-456")
|
||||
|
||||
var body map[string]any
|
||||
if err := json.Unmarshal([]byte(captured.Body), &body); err != nil {
|
||||
t.Fatalf("unmarshal request body: %v", err)
|
||||
}
|
||||
if len(body) != 2 || body["decision"] != "approved" || body["reason"] != "looks good" {
|
||||
t.Fatalf("request body = %#v, want only decision and reason", body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTaskDecision_RejectsInvalidInputBeforeRequest(t *testing.T) {
|
||||
captured := setupMockTransport(t, http.StatusOK, `{}`)
|
||||
t.Setenv("KANDEV_API_URL", "http://kandev.test")
|
||||
t.Setenv("KANDEV_API_KEY", "signed-office-run-token")
|
||||
|
||||
if code := runKandevCLI([]string{"task", "decision", "--decision", "approve", "--reason", "ok"}); code == 0 {
|
||||
t.Fatal("expected invalid decision to fail")
|
||||
}
|
||||
if captured.Method != "" {
|
||||
t.Fatalf("invalid decision contacted server: %s %s", captured.Method, captured.Path)
|
||||
}
|
||||
|
||||
if code := runKandevCLI([]string{"task", "decision", "--decision", "approved", "--reason", " "}); code == 0 {
|
||||
t.Fatal("expected blank reason to fail")
|
||||
}
|
||||
if captured.Method != "" {
|
||||
t.Fatalf("blank reason contacted server: %s %s", captured.Method, captured.Path)
|
||||
}
|
||||
}
|
||||
@@ -109,12 +109,10 @@ func runMain() int {
|
||||
cfg.Port = *portFlag
|
||||
}
|
||||
|
||||
applySIGPIPEDisposition(cfg, signal.Ignore)
|
||||
|
||||
// Initialize logger
|
||||
log, err := logger.NewLogger(logger.LoggingConfig{
|
||||
Level: cfg.LogLevel,
|
||||
Format: cfg.LogFormat,
|
||||
OutputPath: "stdout",
|
||||
})
|
||||
log, err := logger.NewLogger(resolveRunLoggingConfig(cfg))
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "failed to initialize logger: %v\n", err)
|
||||
return 1
|
||||
@@ -126,6 +124,9 @@ func runMain() int {
|
||||
log.Info("starting agentctl",
|
||||
zap.Int("port", cfg.Port),
|
||||
zap.String("log_level", cfg.LogLevel))
|
||||
log.Info("agentctl bootstrap nonce mode",
|
||||
zap.Bool("configured", cfg.BootstrapNonceConfigured()),
|
||||
zap.String("nonce_fingerprint", cfg.BootstrapNonceFingerprint()))
|
||||
|
||||
run(cfg, log)
|
||||
return 0
|
||||
@@ -203,6 +204,13 @@ func run(cfg *config.Config, log *logger.Logger) {
|
||||
// Create instance manager
|
||||
instMgr := instance.NewManager(cfg, log)
|
||||
|
||||
// Declared here and assigned below, before any instance can actually be
|
||||
// created: the server factory closure reads controlServer.CredentialSource()
|
||||
// lazily (at CreateInstance time, not at SetServerFactory time), so the
|
||||
// forward reference is safe as long as controlServer is assigned before
|
||||
// the first instance is created.
|
||||
var controlServer *api.ControlServer
|
||||
|
||||
// Set the server factory to create API servers for each instance
|
||||
instMgr.SetServerFactory(func(instCfg *config.InstanceConfig, procMgr *process.Manager, instLog *logger.Logger) http.Handler {
|
||||
// Create MCP backend client for bidirectional communication through agent stream
|
||||
@@ -222,11 +230,15 @@ func run(cfg *config.Config, log *logger.Logger) {
|
||||
instLog.Info("MCP server enabled (channel-based)",
|
||||
zap.String("session_id", instCfg.SessionID))
|
||||
|
||||
return api.NewServer(instCfg, procMgr, mcpSrv, mcpBackendClient, instLog).Router()
|
||||
srv := api.NewServer(instCfg, procMgr, mcpSrv, mcpBackendClient, instLog)
|
||||
srv.SetCredentialSource(controlServer.CredentialSource())
|
||||
return srv.Router()
|
||||
})
|
||||
|
||||
// Create control server
|
||||
controlServer := api.NewControlServer(cfg, instMgr, log)
|
||||
controlServer = api.NewControlServer(cfg, instMgr, log)
|
||||
stopUnownedReaper := startUnownedReaperIfEnabled(cfg, controlServer)
|
||||
defer stopUnownedReaper()
|
||||
|
||||
// Start HTTP server. When no auth token is configured (auth disabled),
|
||||
// ListenHost binds to loopback only so the unauthenticated command/shell/
|
||||
@@ -258,8 +270,9 @@ func run(cfg *config.Config, log *logger.Logger) {
|
||||
// Returns a channel that closes when the parent dies.
|
||||
parentDied := monitorParentLiveness(log)
|
||||
|
||||
// Wait for shutdown signal (OS signal or parent death)
|
||||
waitForShutdown(log, parentDied, func(ctx context.Context) {
|
||||
// Wait for shutdown signal (OS signal, parent death, or an authenticated
|
||||
// ownership-shutdown operation from an adopting/owning backend).
|
||||
waitForShutdown(log, parentDied, controlServer.ShutdownRequested(), func(ctx context.Context) {
|
||||
// Flush pending traces before stopping instances
|
||||
if err := shared.ShutdownTracing(ctx); err != nil {
|
||||
log.Error("error shutting down tracing", zap.Error(err))
|
||||
@@ -277,23 +290,24 @@ func run(cfg *config.Config, log *logger.Logger) {
|
||||
})
|
||||
}
|
||||
|
||||
// waitForShutdown waits for a shutdown trigger (OS signal or parent death) and
|
||||
// runs the cleanup function. parentDied may be nil when no parent monitor is active.
|
||||
func waitForShutdown(log *logger.Logger, parentDied <-chan struct{}, cleanup func(ctx context.Context)) {
|
||||
// waitForShutdown waits for a shutdown trigger (OS signal, parent death, or
|
||||
// an ownership-shutdown operation) and runs the cleanup function. parentDied
|
||||
// may be nil when no parent monitor is active. ownershipShutdown may be nil
|
||||
// in tests that don't exercise a control server.
|
||||
func waitForShutdown(log *logger.Logger, parentDied <-chan struct{}, ownershipShutdown <-chan struct{}, cleanup func(ctx context.Context)) {
|
||||
sigCh := make(chan os.Signal, 1)
|
||||
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
|
||||
|
||||
if parentDied == nil {
|
||||
// No parent monitor — wait for OS signal only.
|
||||
sig := <-sigCh
|
||||
// A nil channel (no parent monitor, no control server) blocks forever in
|
||||
// a select, so that case is simply never chosen -- no separate branching
|
||||
// needed for the "not active" cases.
|
||||
select {
|
||||
case sig := <-sigCh:
|
||||
log.Info("received signal", zap.String("signal", sig.String()))
|
||||
} else {
|
||||
select {
|
||||
case sig := <-sigCh:
|
||||
log.Info("received signal", zap.String("signal", sig.String()))
|
||||
case <-parentDied:
|
||||
log.Warn("parent process died, initiating shutdown")
|
||||
}
|
||||
case <-parentDied:
|
||||
log.Warn("parent process died, initiating shutdown")
|
||||
case <-ownershipShutdown:
|
||||
log.Warn("ownership-shutdown operation invoked, initiating shutdown")
|
||||
}
|
||||
|
||||
log.Info("shutting down agentctl...")
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"syscall"
|
||||
|
||||
"github.com/kandev/kandev/internal/agentctl/server/config"
|
||||
)
|
||||
|
||||
// applySIGPIPEDisposition decides whether this process keeps running after
|
||||
// the backend that spawned it goes away.
|
||||
//
|
||||
// The launcher always pipes this process's stdout/stderr back to the
|
||||
// spawning backend for logging (internal/agent/runtime/agentctl/launcher's
|
||||
// pipeOutput). When that backend exits, the kernel closes its end of those
|
||||
// pipes, and Go's default SIGPIPE disposition kills this process the next
|
||||
// time it writes to fd 1 or 2, which every subsequent log line does. An
|
||||
// inherited pipe is therefore one of the ways a departing backend takes its
|
||||
// agentctl with it.
|
||||
//
|
||||
// Which behavior is correct depends on the capability. With agent survival
|
||||
// enabled the process is meant to outlive a graceful restart, so the write
|
||||
// must degrade to an ordinary error, which the logger already tolerates.
|
||||
// With the capability disabled nothing may outlive the backend, so this kill
|
||||
// path stays intact alongside the others.
|
||||
//
|
||||
// Called after configuration is resolved: a process that dies before then
|
||||
// dies on the default disposition, and the short-lived CLI and
|
||||
// credential-helper invocations dispatched before configuration keep the
|
||||
// ordinary disposition a command-line tool should have.
|
||||
func applySIGPIPEDisposition(cfg *config.Config, ignore func(...os.Signal)) {
|
||||
if !cfg.AgentSurvivalEnabled {
|
||||
return
|
||||
}
|
||||
ignore(syscall.SIGPIPE)
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"syscall"
|
||||
"testing"
|
||||
|
||||
"github.com/kandev/kandev/internal/agentctl/server/config"
|
||||
)
|
||||
|
||||
// TestSIGPIPEDispositionFollowsTheSurvivalCapability pins that ignoring
|
||||
// SIGPIPE is part of the survival capability rather than an unconditional
|
||||
// property of the binary. With the capability off, an inherited stdout whose
|
||||
// reader has gone away must still be able to end this process, because that
|
||||
// is one of the paths by which a departing backend takes its agentctl with
|
||||
// it; removing it for every launch would leave a detached agentctl behind on
|
||||
// installations that never enabled survival at all.
|
||||
func TestSIGPIPEDispositionFollowsTheSurvivalCapability(t *testing.T) {
|
||||
t.Run("disabled leaves the default disposition intact", func(t *testing.T) {
|
||||
var ignored []os.Signal
|
||||
applySIGPIPEDisposition(&config.Config{AgentSurvivalEnabled: false}, func(s ...os.Signal) {
|
||||
ignored = append(ignored, s...)
|
||||
})
|
||||
if len(ignored) != 0 {
|
||||
t.Fatalf("signals ignored = %v, want none with the capability disabled", ignored)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("enabled ignores SIGPIPE", func(t *testing.T) {
|
||||
var ignored []os.Signal
|
||||
applySIGPIPEDisposition(&config.Config{AgentSurvivalEnabled: true}, func(s ...os.Signal) {
|
||||
ignored = append(ignored, s...)
|
||||
})
|
||||
if len(ignored) != 1 || ignored[0] != syscall.SIGPIPE {
|
||||
t.Fatalf("signals ignored = %v, want exactly [SIGPIPE]", ignored)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package main
|
||||
|
||||
import "github.com/kandev/kandev/internal/agentctl/server/config"
|
||||
|
||||
// unownedReaper is satisfied by *api.ControlServer, narrowed here so the
|
||||
// gate decision below is testable without a real control server.
|
||||
type unownedReaper interface {
|
||||
StartUnownedReaper()
|
||||
StopUnownedReaper()
|
||||
}
|
||||
|
||||
// startUnownedReaperIfEnabled starts the unowned-period reaper only when the
|
||||
// agent-survival capability is engaged for this launch, per
|
||||
// StartUnownedReaper's own caller contract: starting it unconditionally
|
||||
// would self-terminate a healthy attached instance once the period elapses,
|
||||
// since nothing renews ownership without that capability's backend-side
|
||||
// claim loop. StopUnownedReaper is always safe to call even when Start was
|
||||
// never called, so the returned stop func needs no second branch.
|
||||
func startUnownedReaperIfEnabled(cfg *config.Config, reaper unownedReaper) (stop func()) {
|
||||
if cfg.AgentSurvivalEnabled {
|
||||
reaper.StartUnownedReaper()
|
||||
}
|
||||
return reaper.StopUnownedReaper
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/kandev/kandev/internal/agentctl/server/config"
|
||||
)
|
||||
|
||||
// fakeUnownedReaper is a test double for unownedReaper, letting the gate
|
||||
// decision be asserted without exercising the real reaper goroutine's
|
||||
// lifecycle (already covered directly by the api package's own tests).
|
||||
type fakeUnownedReaper struct {
|
||||
started bool
|
||||
stopped bool
|
||||
}
|
||||
|
||||
func (f *fakeUnownedReaper) StartUnownedReaper() { f.started = true }
|
||||
func (f *fakeUnownedReaper) StopUnownedReaper() { f.stopped = true }
|
||||
|
||||
// TestStartUnownedReaperIfEnabled pins Layer 5.9's gate on
|
||||
// StartUnownedReaper's own caller contract: unconditionally starting it on
|
||||
// every agentctl launch would self-terminate a healthy attached instance
|
||||
// once the period elapses, since nothing renews ownership without the
|
||||
// agent-survival capability and its backend-side claim loop.
|
||||
func TestStartUnownedReaperIfEnabled(t *testing.T) {
|
||||
t.Run("enabled starts the reaper", func(t *testing.T) {
|
||||
reaper := &fakeUnownedReaper{}
|
||||
stop := startUnownedReaperIfEnabled(&config.Config{AgentSurvivalEnabled: true}, reaper)
|
||||
if !reaper.started {
|
||||
t.Fatal("StartUnownedReaper was not called with the capability enabled")
|
||||
}
|
||||
stop()
|
||||
if !reaper.stopped {
|
||||
t.Fatal("the returned stop function did not call StopUnownedReaper")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("disabled never starts the reaper but stop stays safe", func(t *testing.T) {
|
||||
reaper := &fakeUnownedReaper{}
|
||||
stop := startUnownedReaperIfEnabled(&config.Config{AgentSurvivalEnabled: false}, reaper)
|
||||
if reaper.started {
|
||||
t.Fatal("StartUnownedReaper was called with the capability disabled")
|
||||
}
|
||||
stop()
|
||||
if !reaper.stopped {
|
||||
t.Fatal("the returned stop function did not call StopUnownedReaper even when disabled")
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -42,7 +42,7 @@ Scenarios in `scenarioRegistry` can only emit `SessionUpdate` notifications —
|
||||
|
||||
`/overloaded[:N]` is the reference example (`handler.go: handleOverloaded`): it returns the production `529 Overloaded` error for the first `N` prompts of a session (default 1), then recovers with a normal text response. The orchestrator's backoff retry tears the agent process down and relaunches it between attempts, so the fail-count is persisted in a **temp file** keyed by the session id (`overloadedCounterPath`) rather than an in-memory map — it survives the relaunch (and is cleaned up on recovery and in `CloseSession`). Use `/overloaded` to demo the yellow retry status, or a large `N` like `/overloaded:9` to keep failing so the retry loop stays visible / exhausts to the red recovery banner.
|
||||
|
||||
`/transport-lost[:N]` (`handler.go: handleTransportLost`) follows the same shape for the ACP wire-level transport-death signature (`peer disconnected before response` — the acp-go-sdk's own `ErrPeerDisconnected` payload shape), which `routingerr`'s `acp.transport_lost.v1` rule classifies as `CodeAgentTransportLost` and admits to the short-retry ladder. Use `/transport-lost` or a large `N` like `/transport-lost:9` the same way as `/overloaded`.
|
||||
`/transport-lost[:N]` (`handler.go: handleTransportLost`) follows the same shape for the ACP wire-level transport-death signature (`peer disconnected before response` — the acp-go-sdk's own `ErrPeerDisconnected` payload shape), carried only in the error's `Data` the same way the SDK itself constructs it. The generic prompt-error projection classifies on `Message` alone, so `routingerr`'s `acp.transport_lost.v1` rule never sees it here: the failure presents as terminal and exposes manual recovery on the first occurrence, not the short-retry ladder. `N` still controls how many consecutive prompts fail before the mock recovers, but no automatic retry drives through them on this command.
|
||||
|
||||
## Emitter helpers worth knowing
|
||||
|
||||
|
||||
@@ -145,8 +145,10 @@ var transportLostErrorData = map[string]any{"error": "peer disconnected before r
|
||||
// transportLostCmdRe matches `/transport-lost` or `/e2e:transport-lost`,
|
||||
// optionally followed by `:N` — the number of consecutive prompts to fail
|
||||
// with the ACP peer-disconnected signature before recovering (default 1).
|
||||
// Use a large N (e.g. `/transport-lost:9`) to exhaust the retry budget and
|
||||
// fall through to the red recovery banner.
|
||||
// The signature lives only in the error's Data, which the generic
|
||||
// prompt-error projection never reads, so every failure presents as
|
||||
// terminal and exposes manual recovery; N does not drive an automatic
|
||||
// retry ladder here the way it does for `/overloaded`.
|
||||
var transportLostCmdRe = regexp.MustCompile(`(?i)^/(?:e2e:)?transport-lost(?::(\d+))?$`)
|
||||
|
||||
// parseTransportLostCmd reports whether the prompt is the /transport-lost
|
||||
|
||||
@@ -288,19 +288,38 @@ func ptr(s string) *string {
|
||||
// LoadSession restores a previous session for resume.
|
||||
// When --fail-on-resume is set, exit before completing the load — LoadSession
|
||||
// is only reached on resume, so no resumed-guard is needed here (unlike TUI).
|
||||
func (a *mockAgent) LoadSession(_ context.Context, req acp.LoadSessionRequest) (acp.LoadSessionResponse, error) {
|
||||
func (a *mockAgent) LoadSession(ctx context.Context, req acp.LoadSessionRequest) (acp.LoadSessionResponse, error) {
|
||||
if parseFailOnResumeFlag() {
|
||||
_, _ = fmt.Fprintf(logOutput, "mock-agent[%d]: refusing resume for session %s (--fail-on-resume), exiting 1\n", os.Getpid(), req.SessionId)
|
||||
os.Exit(1)
|
||||
}
|
||||
if delay := parseResumeDelayFlag(); delay > 0 {
|
||||
_, _ = fmt.Fprintf(logOutput, "mock-agent[%d]: delaying resume for session %s by %s\n", os.Getpid(), req.SessionId, delay)
|
||||
timer := time.NewTimer(delay)
|
||||
defer timer.Stop()
|
||||
select {
|
||||
case <-timer.C:
|
||||
case <-ctx.Done():
|
||||
return acp.LoadSessionResponse{}, ctx.Err()
|
||||
}
|
||||
}
|
||||
a.mu.Lock()
|
||||
a.sessions[req.SessionId] = true
|
||||
if a.sessions == nil {
|
||||
a.sessions = make(map[acp.SessionId]bool)
|
||||
}
|
||||
if a.sessionConfig == nil {
|
||||
a.sessionConfig = make(map[acp.SessionId][]acp.SessionConfigOption)
|
||||
}
|
||||
if _, ok := a.sessionConfig[req.SessionId]; !ok {
|
||||
a.sessionConfig[req.SessionId] = mockSessionConfigOptions()
|
||||
if a.commandsEmitted == nil {
|
||||
a.commandsEmitted = make(map[acp.SessionId]bool)
|
||||
}
|
||||
a.sessions[req.SessionId] = true
|
||||
configOptions, ok := a.sessionConfig[req.SessionId]
|
||||
if !ok {
|
||||
a.sessionConfig[req.SessionId] = mockSessionConfigOptions()
|
||||
configOptions = a.sessionConfig[req.SessionId]
|
||||
}
|
||||
responseConfigOptions := cloneSessionConfigOptions(configOptions)
|
||||
// Reset emit state so the resume re-advertises commands (matches real
|
||||
// agents which re-emit on session/load).
|
||||
delete(a.commandsEmitted, req.SessionId)
|
||||
@@ -310,7 +329,10 @@ func (a *mockAgent) LoadSession(_ context.Context, req acp.LoadSessionRequest) (
|
||||
// Re-emit available commands after the session/load response flushes.
|
||||
go a.emitAvailableCommandsAfterDelay(req.SessionId)
|
||||
|
||||
return acp.LoadSessionResponse{}, nil
|
||||
return acp.LoadSessionResponse{
|
||||
ConfigOptions: responseConfigOptions,
|
||||
Modes: mockSessionModes(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Prompt processes a user message and streams responses via SessionUpdate.
|
||||
@@ -616,6 +638,41 @@ func parseFailOnResumeFromArgs(args []string) bool {
|
||||
return slices.Contains(args[1:], "--fail-on-resume")
|
||||
}
|
||||
|
||||
// parseResumeDelayFlag returns the positive duration from --delay-resume. This
|
||||
// is a mock-agent-only readiness fixture used by E2E resume tests.
|
||||
func parseResumeDelayFlag() time.Duration {
|
||||
if delay := parseResumeDelayFromArgs(os.Args); delay > 0 {
|
||||
return delay
|
||||
}
|
||||
return parseResumeDelayFromValue(os.Getenv("E2E_MOCK_AGENT_RESUME_DELAY"))
|
||||
}
|
||||
|
||||
func parseResumeDelayFromArgs(args []string) time.Duration {
|
||||
for i, arg := range args[1:] {
|
||||
var raw string
|
||||
if arg == "--delay-resume" && i+1 < len(args)-1 {
|
||||
raw = args[i+2]
|
||||
} else if value, ok := strings.CutPrefix(arg, "--delay-resume="); ok {
|
||||
raw = value
|
||||
}
|
||||
if raw == "" {
|
||||
continue
|
||||
}
|
||||
if delay := parseResumeDelayFromValue(raw); delay > 0 {
|
||||
return delay
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func parseResumeDelayFromValue(raw string) time.Duration {
|
||||
delay, err := time.ParseDuration(strings.TrimSpace(raw))
|
||||
if err == nil && delay > 0 {
|
||||
return delay
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// mcpConfigPayload is the JSON structure for --mcp-config.
|
||||
type mcpConfigPayload struct {
|
||||
MCPServers map[string]mcpServerDef `json:"mcpServers"`
|
||||
|
||||
@@ -89,6 +89,35 @@ func TestInitializePromptQueueingCanBeDisabled(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseResumeDelayFromArgs(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
args []string
|
||||
want time.Duration
|
||||
}{
|
||||
{name: "separate value", args: []string{"mock-agent", "--delay-resume", "2s"}, want: 2 * time.Second},
|
||||
{name: "equals value", args: []string{"mock-agent", "--delay-resume=1500ms"}, want: 1500 * time.Millisecond},
|
||||
{name: "missing value", args: []string{"mock-agent", "--delay-resume"}},
|
||||
{name: "invalid value", args: []string{"mock-agent", "--delay-resume", "later"}},
|
||||
{name: "negative value", args: []string{"mock-agent", "--delay-resume=-1s"}},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := parseResumeDelayFromArgs(tt.args); got != tt.want {
|
||||
t.Fatalf("parseResumeDelayFromArgs() = %s, want %s", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseResumeDelayFlagUsesEnvironment(t *testing.T) {
|
||||
t.Setenv("E2E_MOCK_AGENT_RESUME_DELAY", "3s")
|
||||
if got := parseResumeDelayFlag(); got != 3*time.Second {
|
||||
t.Fatalf("parseResumeDelayFlag() = %s, want 3s", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseSavedPromptDeliveryScenarioRequiresTrustedExpansionShape(t *testing.T) {
|
||||
const directive = savedPromptDeliveryDirective
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package main
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
acp "github.com/coder/acp-go-sdk"
|
||||
)
|
||||
@@ -37,6 +38,41 @@ func TestSetSessionConfigOptionReturnsAuthoritativeState(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadSessionReturnsCapabilitiesForResumedSession(t *testing.T) {
|
||||
sessionID := acp.SessionId("session-load-test")
|
||||
agent := &mockAgent{
|
||||
conn: &promptCancelUpdater{started: make(chan struct{})},
|
||||
sessions: make(map[acp.SessionId]bool),
|
||||
sessionConfig: make(map[acp.SessionId][]acp.SessionConfigOption),
|
||||
commandsEmitted: make(map[acp.SessionId]bool),
|
||||
}
|
||||
|
||||
response, err := agent.LoadSession(context.Background(), acp.LoadSessionRequest{SessionId: sessionID})
|
||||
if err != nil {
|
||||
t.Fatalf("LoadSession() error = %v", err)
|
||||
}
|
||||
select {
|
||||
case <-agent.conn.(*promptCancelUpdater).started:
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("timed out waiting for resumed-session commands")
|
||||
}
|
||||
if len(response.ConfigOptions) == 0 {
|
||||
t.Fatal("LoadSession() returned no config options")
|
||||
}
|
||||
if response.Modes == nil || len(response.Modes.AvailableModes) == 0 {
|
||||
t.Fatal("LoadSession() returned no session modes")
|
||||
}
|
||||
values := make(map[string]string, len(response.ConfigOptions))
|
||||
for _, option := range response.ConfigOptions {
|
||||
if option.Select != nil {
|
||||
values[string(option.Select.Id)] = string(option.Select.CurrentValue)
|
||||
}
|
||||
}
|
||||
if values["model"] != modelFast {
|
||||
t.Fatalf("loaded model = %q, want %q", values["model"], modelFast)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetSessionConfigOptionRejectsUnknownValue(t *testing.T) {
|
||||
sessionID := acp.SessionId("session-config-test")
|
||||
agent := &mockAgent{
|
||||
|
||||
@@ -10,6 +10,46 @@ import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRunDeployAcceptsSkipDescription(t *testing.T) {
|
||||
t.Setenv("SPRITES_API_TOKEN", "")
|
||||
t.Setenv("GH_TOKEN", "")
|
||||
t.Setenv("GITHUB_REPOSITORY", "owner/repo")
|
||||
|
||||
oldStderr := os.Stderr
|
||||
readPipe, writePipe, err := os.Pipe()
|
||||
if err != nil {
|
||||
t.Fatalf("create stderr pipe: %v", err)
|
||||
}
|
||||
os.Stderr = writePipe
|
||||
t.Cleanup(func() {
|
||||
os.Stderr = oldStderr
|
||||
_ = readPipe.Close()
|
||||
_ = writePipe.Close()
|
||||
})
|
||||
|
||||
code := runDeploy(context.Background(), []string{
|
||||
"--pr", "123",
|
||||
"--repo", "owner/repo",
|
||||
"--skip-description",
|
||||
})
|
||||
if err := writePipe.Close(); err != nil {
|
||||
t.Fatalf("close stderr pipe: %v", err)
|
||||
}
|
||||
output, err := io.ReadAll(readPipe)
|
||||
if err != nil {
|
||||
t.Fatalf("read stderr: %v", err)
|
||||
}
|
||||
|
||||
if code != 2 {
|
||||
t.Fatalf("runDeploy exit code = %d, want missing token validation (2)", code)
|
||||
}
|
||||
if strings.Contains(string(output), "flag provided but not defined") {
|
||||
t.Fatalf("skip-description was rejected as an unknown flag: %s", output)
|
||||
}
|
||||
if !strings.Contains(string(output), "SPRITES_API_TOKEN is required") {
|
||||
t.Fatalf("runDeploy output = %q, want missing token validation", output)
|
||||
}
|
||||
}
|
||||
func TestRunDeployRejectsMissingExplicitArtifactBeforeCredentials(t *testing.T) {
|
||||
t.Setenv("SPRITES_API_TOKEN", "")
|
||||
t.Setenv("GH_TOKEN", "")
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
agentcatalog "github.com/kandev/kandev/internal/agent/settings/catalog"
|
||||
agentsettingsdto "github.com/kandev/kandev/internal/agent/settings/dto"
|
||||
"github.com/kandev/kandev/internal/settingscatalog"
|
||||
userdto "github.com/kandev/kandev/internal/user/dto"
|
||||
)
|
||||
|
||||
const schemaVersion = "settings-catalog.v1"
|
||||
|
||||
type snapshot struct {
|
||||
SchemaVersion string `json:"schema_version"`
|
||||
GeneratedBy string `json:"generated_by"`
|
||||
Domains []settingscatalog.DomainDescriptor `json:"domains"`
|
||||
MutableFields map[string][]string `json:"mutable_fields"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
check := flag.Bool("check", false, "check generated snapshots without writing them")
|
||||
flag.Parse()
|
||||
|
||||
root, err := moduleRoot()
|
||||
if err != nil {
|
||||
fatal(err)
|
||||
}
|
||||
all, profile, err := snapshots()
|
||||
if err != nil {
|
||||
fatal(err)
|
||||
}
|
||||
outputs := map[string][]byte{
|
||||
filepath.Join(root, "../web/lib/settings-discovery/contract.generated.json"): all,
|
||||
filepath.Join(root, "../web/lib/settings-discovery/profile-contract.generated.json"): profile,
|
||||
}
|
||||
for path, expected := range outputs {
|
||||
if *check {
|
||||
actual, readErr := os.ReadFile(path)
|
||||
if readErr != nil {
|
||||
fatal(fmt.Errorf("read %s: %w", path, readErr))
|
||||
}
|
||||
if !bytes.Equal(actual, expected) {
|
||||
fatal(fmt.Errorf("generated snapshot is stale: %s", path))
|
||||
}
|
||||
continue
|
||||
}
|
||||
if err := os.WriteFile(path, expected, 0o644); err != nil {
|
||||
fatal(fmt.Errorf("write %s: %w", path, err))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func snapshots() ([]byte, []byte, error) {
|
||||
profileDomains := agentcatalog.Descriptors()
|
||||
profileTypes := make(map[string]struct{}, len(profileDomains))
|
||||
for _, domain := range profileDomains {
|
||||
profileTypes[domain.ResourceType] = struct{}{}
|
||||
}
|
||||
allDomains := make([]settingscatalog.DomainDescriptor, 0)
|
||||
for _, domain := range settingscatalog.DefaultDomainDescriptors() {
|
||||
if _, exists := profileTypes[domain.ResourceType]; exists {
|
||||
continue
|
||||
}
|
||||
allDomains = append(allDomains, domain)
|
||||
}
|
||||
allDomains = append(allDomains, profileDomains...)
|
||||
allRegistry, err := settingscatalog.NewRegistry(allDomains)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
profileRegistry, err := settingscatalog.NewRegistry(profileDomains)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
mutableFields := mutableFieldInventory()
|
||||
all, err := marshalSnapshot(allRegistry, mutableFields)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
profile, err := marshalSnapshot(profileRegistry, mutableFields)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return all, profile, nil
|
||||
}
|
||||
|
||||
func marshalSnapshot(registry *settingscatalog.Registry, mutableFields map[string][]string) ([]byte, error) {
|
||||
domains := registry.Domains()
|
||||
sort.Slice(domains, func(i, j int) bool { return domains[i].ResourceType < domains[j].ResourceType })
|
||||
payload, err := json.MarshalIndent(snapshot{
|
||||
SchemaVersion: schemaVersion,
|
||||
GeneratedBy: "cmd/settings-catalog",
|
||||
Domains: domains,
|
||||
MutableFields: mutableFields,
|
||||
}, "", " ")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return append(payload, '\n'), nil
|
||||
}
|
||||
|
||||
func mutableFieldInventory() map[string][]string {
|
||||
userFields := jsonFieldPaths(reflect.TypeFor[userdto.UpdateUserSettingsRequest](), nil)
|
||||
// sidebar_task_colors is the public patch alias. The DTO stores its
|
||||
// normalized form as sidebar_task_color_patch before calling the service.
|
||||
userFields = append(userFields, "sidebar_task_colors")
|
||||
sort.Strings(userFields)
|
||||
return map[string][]string{
|
||||
"agent_profile": jsonFieldPaths(reflect.TypeFor[agentsettingsdto.ProfileUpdateRequest](), map[string]struct{}{
|
||||
"id": {}, "force": {},
|
||||
}),
|
||||
"user_settings": userFields,
|
||||
}
|
||||
}
|
||||
|
||||
func jsonFieldPaths(valueType reflect.Type, excluded map[string]struct{}) []string {
|
||||
if valueType.Kind() == reflect.Pointer {
|
||||
valueType = valueType.Elem()
|
||||
}
|
||||
paths := make([]string, 0, valueType.NumField())
|
||||
for index := 0; index < valueType.NumField(); index++ {
|
||||
field := valueType.Field(index)
|
||||
jsonName := strings.Split(field.Tag.Get("json"), ",")[0]
|
||||
if jsonName == "" || jsonName == "-" {
|
||||
continue
|
||||
}
|
||||
if _, skip := excluded[jsonName]; skip {
|
||||
continue
|
||||
}
|
||||
paths = append(paths, jsonName)
|
||||
}
|
||||
sort.Strings(paths)
|
||||
return paths
|
||||
}
|
||||
|
||||
func moduleRoot() (string, error) {
|
||||
workingDirectory, err := os.Getwd()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
for current := workingDirectory; ; current = filepath.Dir(current) {
|
||||
if _, err := os.Stat(filepath.Join(current, "go.mod")); err == nil {
|
||||
return current, nil
|
||||
} else if !errors.Is(err, os.ErrNotExist) {
|
||||
return "", err
|
||||
}
|
||||
parent := filepath.Dir(current)
|
||||
if parent == current {
|
||||
return "", fmt.Errorf("go.mod not found above %s", workingDirectory)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func fatal(err error) {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
@@ -11,10 +11,10 @@ WORKFLOW TOOLS:
|
||||
- update_workflow_kandev: Update a workflow. Required: workflow_id. Optional: name, description.
|
||||
- delete_workflow_kandev: Delete a workflow and all its steps (destructive). Required: workflow_id.
|
||||
- import_workflow_kandev: Import one or more workflows into a workspace from a portable document (the kandev_workflow YAML/JSON export envelope). Workflows whose name already exists are skipped. Required: workspace_id, document. Returns the created and skipped workflow names.
|
||||
- export_workflow_kandev: Export one workflow as a portable version 1 kandev_workflow JSON document. Required: workflow_id. Pass the returned JSON unchanged as document to import_workflow_kandev.
|
||||
- export_workflow_kandev: Export one workflow as a portable version 2 kandev_workflow JSON document. Required: workflow_id. Pass the returned JSON unchanged as document to import_workflow_kandev. Version 1 documents remain accepted for compatibility.
|
||||
- list_workflow_steps_kandev: List workflow steps (columns) in a workflow. Required: workflow_id.
|
||||
- create_workflow_step_kandev: Create a new workflow step. Required: workflow_id, name. Optional: position, color, prompt, is_start_step, allow_manual_move, show_in_command_panel, auto_advance_requires_signal, cancel_triggers_turn_complete, events.
|
||||
- update_workflow_step_kandev: Update a workflow step. Required: step_id. Optional: name, color, prompt, is_start_step, allow_manual_move, show_in_command_panel, auto_archive_after_hours, auto_advance_requires_signal, cancel_triggers_turn_complete, events.
|
||||
- create_workflow_step_kandev: Create a new workflow step. Required: workflow_id, name. Optional: position, color, prompt, is_start_step, allow_manual_move, show_in_command_panel, complete_task_on_enter, auto_advance_requires_signal, cancel_triggers_turn_complete, events.
|
||||
- update_workflow_step_kandev: Update a workflow step. Required: step_id. Optional: name, color, prompt, is_start_step, allow_manual_move, show_in_command_panel, auto_archive_after_hours, complete_task_on_enter, auto_advance_requires_signal, cancel_triggers_turn_complete, events.
|
||||
- cancel_triggers_turn_complete: When true, an explicit user cancellation runs the step's normal on_turn_complete actions after the cancelled turn settles. It does not apply to internal or agent cancellations.
|
||||
- delete_workflow_step_kandev: Delete a workflow step (destructive). Required: step_id.
|
||||
- reorder_workflow_steps_kandev: Reorder workflow steps. Required: workflow_id, step_ids (ordered array of step IDs).
|
||||
@@ -22,7 +22,7 @@ WORKFLOW TOOLS:
|
||||
AGENT TOOLS:
|
||||
- list_agents_kandev: List all configured agents and their profiles.
|
||||
- update_agent_kandev: Update agent settings. Required: agent_id. Optional: supports_mcp, mcp_config_path.
|
||||
- create_agent_profile_kandev: Create a new agent profile. Required: agent_id, name, model. Optional: auto_approve.
|
||||
- create_agent_profile_kandev: Create a new agent profile. Required: agent_id, name. Optional: model, auto_approve, settings. Use describe_setting_kandev for the current profile schema before supplying settings.
|
||||
- delete_agent_profile_kandev: Delete an agent profile. Required: profile_id.
|
||||
|
||||
EXECUTOR PROFILE TOOLS:
|
||||
@@ -46,6 +46,27 @@ Saved prompt names are case-sensitive. Surrounding whitespace is ignored. Use
|
||||
the list result to discover names before reading a prompt. These tools are
|
||||
read-only and do not create, update, delete, or expand saved prompts.
|
||||
|
||||
SETTINGS TOOLS:
|
||||
- search_settings_kandev: Search setting definitions by metadata. This does not read saved values.
|
||||
- describe_setting_kandev: Describe one setting's schema, target rules, authority, and choices.
|
||||
- list_settings_resources_kandev: List authorized exact targets for a resource type.
|
||||
- get_settings_kandev: Read saved values for one exact target. Sensitive values are redacted or returned as references.
|
||||
- update_settings_kandev: Update declared settings for one exact target. Changes are validated by the owning domain.
|
||||
|
||||
When a user asks to inspect or change a setting, use this sequence:
|
||||
1. Search with search_settings_kandev when you need to find the setting key.
|
||||
2. Describe the setting with describe_setting_kandev before reading or writing it.
|
||||
3. List exact targets with list_settings_resources_kandev when the setting needs a resource ID or workspace ID.
|
||||
4. Read current values with get_settings_kandev before changing a saved value.
|
||||
5. Update one exact target with update_settings_kandev using the declared field paths and schema.
|
||||
|
||||
Do not guess resource IDs, workspace IDs, setting keys, or field paths. A target
|
||||
must match the setting's declared resource type. Use the separate
|
||||
agent_profile_mcp target and MCP config tools for profile MCP documents. Use
|
||||
existing lifecycle tools for resource creation, deletion, reordering, and
|
||||
other explicit actions. Do not request or write credential values through
|
||||
settings tools; use the existing interactive integration flow.
|
||||
|
||||
TASK TOOLS:
|
||||
- list_tasks_kandev: List all tasks in a workflow. Required: workflow_id.
|
||||
- move_task_kandev: Move a task to a different workflow step. Required: task_id, workflow_step_id.
|
||||
@@ -61,6 +82,8 @@ EXAMPLE REQUESTS the user might ask:
|
||||
- "Add a 'Code Review' step to my workflow"
|
||||
- "Create a new agent profile for Claude with auto-approve enabled"
|
||||
- "Show me the current workflow steps"
|
||||
- "Show me the current terminal font size"
|
||||
- "Change the default agent profile for this workspace"
|
||||
- "Update the MCP servers for the default agent profile"
|
||||
- "Create a new executor profile for Docker with a prepare script"
|
||||
- "Move all completed tasks to the 'Done' column"
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
Create a Kandev canvas for the goal described in the user's request.
|
||||
|
||||
If the application goal is missing, ask what the canvas must show or do.
|
||||
|
||||
Find the Kandev canvas MCP tools before writing application files. If they are
|
||||
not callable, use native tool search for `kandev canvas` or inspect the
|
||||
available MCP catalog.
|
||||
|
||||
Call `create_canvas_kandev` to create the draft and obtain its source directory.
|
||||
Read `read_canvas_authoring_skill_kandev` once without a path.
|
||||
Build inside the returned source directory and use authorized live Kandev data
|
||||
for domain views.
|
||||
|
||||
Call `publish_canvas_kandev` and address any validation errors. Report the
|
||||
canvas identity and whether its release is active, awaits permission review,
|
||||
or was unsuccessful.
|
||||
|
||||
If publication is unsuccessful, report the failure and do not claim that the
|
||||
canvas is published. If workspace access requires promotion, explain the user
|
||||
action that is still required.
|
||||
|
||||
A local build alone does not publish a canvas inside Kandev. If the tools remain
|
||||
unavailable, report the limitation instead of claiming that workspace files
|
||||
are a Kandev canvas.
|
||||
@@ -1,33 +1,32 @@
|
||||
KANDEV MCP TOOLS — You have access to the following MCP tools from the "kandev" server.
|
||||
The exact `_kandev` names below are the canonical MCP protocol names. Agent clients may show a server-qualified form in their display or tool registry; treat that form as a client-specific alias for the same tool, not a separate capability, and use the exact form exposed by the active client.
|
||||
KANDEV MCP TOOLS — Selected tools from the "kandev" server are available here.
|
||||
Names ending in `_kandev` are canonical MCP protocol names. A client-specific alias may be server-qualified; use the exact callable name and schema exposed by the client.
|
||||
|
||||
Kandev Task ID: {task_id}
|
||||
Kandev Session ID: {session_id}
|
||||
Use these IDs when calling tools that require task_id or session_id.
|
||||
|
||||
DELEGATION POLICY:
|
||||
For ordinary coding, research, review, or parallel work, use your host agent's native subagent mechanism (for example Codex's native subagent tool, Claude Code's Agent tool, Cursor custom subagents, or OpenCode's Task tool) only when the user has explicitly authorized delegation for this task. Otherwise continue in this session or ask the user. Do NOT use Kandev task or session tools as a generic worker mechanism.
|
||||
Use create_task_kandev only when the user explicitly wants a persistent Kandev task or subtask, workflow tracking, or Kandev task lifecycle. Use spawn_session_kandev only when the user explicitly wants another Kandev session/tab. If no native subagent tool is available, continue in this session or ask the user; do not silently create a Kandev task or session.
|
||||
|
||||
For ordinary coding, research, review, or parallel work, use your host agent's native subagent mechanism only when the user has explicitly authorized delegation; otherwise continue in this session or ask the user. Never use Kandev task or session tools as generic workers.
|
||||
Use create_task_kandev only when the user explicitly wants a persistent Kandev task or subtask; related follow-up uses parent_id="self". Use spawn_session_kandev only when the user explicitly wants another Kandev session/tab; otherwise do not silently create a Kandev task or session.
|
||||
{autopilot_section}
|
||||
|
||||
MCP DISCOVERY:
|
||||
These instructions list selected Kandev tools, not the complete MCP catalog.
|
||||
For Kandev operations, use tools exposed by the "kandev" server. If the needed tool is already callable, use its current schema; otherwise use your client's native tool search or discovery when available.
|
||||
Search for "kandev" plus the operation or known canonical tool name. If search is unavailable, inspect the MCP tools available in your client.
|
||||
Use the exact callable name and schema exposed by the client. An omitted entry here does not mean that the tool is unavailable. If discovery cannot find a required tool, report that limitation before substituting another result.
|
||||
|
||||
ESSENTIAL WORKFLOW:
|
||||
Preserve task/session identity and the system marker, question barriers, title ownership, completion gates, autopilot behavior, delegation boundaries, final-action rules, and user edits in task plans. Use create/get/update plan tools. For data requests that need a chart, preview, or metric, call show_rich_output_kandev with the schema returned by discovery.
|
||||
|
||||
Available tools:
|
||||
{question_tool_section}
|
||||
{step_complete_section}{task_title_section}- create_task_plan_kandev: Save an implementation plan for a task.
|
||||
- get_task_plan_kandev: Read a task plan, including user edits.
|
||||
- update_task_plan_kandev: Update an existing task plan.
|
||||
- delete_task_plan_kandev: Delete a task plan.
|
||||
- show_rich_output_kandev: When user asks for chart/graph/plot/file preview/KPI/metrics with data: call now. Do not implement the display as ASCII/SVG/HTML or with another app. Else prose; small text table: Markdown. Send version=1,title,blocks (1-4). Inline: {"type":"chart","chart_type":"bar","title":"T","summary":"S","labels":["A","B"],"series":[{"label":"Count","values":[42,27]}]}. CSV line: {"type":"chart","chart_type":"line","title":"T","summary":"S","csv":{"path":"reports/latency.csv","x_column":"recorded_at","series":[{"column":"p95_ms","label":"p95 (ms)"}]}}. Metrics: {"type":"metrics","items":[{"label":"Passed","value":"38"}]}. Paths workspace-relative. Kandev owns axes/legends/tooltips/layout. Label series with units.
|
||||
- show_walkthrough_kandev: Store an ordered, file-anchored code walkthrough.
|
||||
- get_walkthrough_kandev: Read a task's stored walkthrough.
|
||||
- delete_walkthrough_kandev: Delete a task's walkthrough.
|
||||
- list_workspaces_kandev: List all workspaces.
|
||||
- list_workflows_kandev: List workflows in a workspace.
|
||||
- list_tasks_kandev: List tasks in a workflow.
|
||||
- create_task_kandev: Create user-requested persistent Kandev-tracked work. Use parent_id="self" for a current-task subtask; its context is inherited unless explicitly overridden.
|
||||
- update_task_kandev: Update a task, including a deferred launch prompt for blocked work that has not started.
|
||||
- spawn_session_kandev: Start an additional Kandev session/tab without creating a task. Use only when the user explicitly requests one.
|
||||
- message_task_kandev: Send a prompt to an existing Kandev task session.{coordinator_task_control_section}
|
||||
- list_task_sessions_kandev: List a task's sessions and their IDs.
|
||||
{step_complete_section}{task_title_section}{canvas_guidance_section}- create_task_plan_kandev, get_task_plan_kandev, update_task_plan_kandev: preserve user edits.
|
||||
{rich_output_section}
|
||||
- show_walkthrough_kandev, get_walkthrough_kandev, delete_walkthrough_kandev.
|
||||
- create_task_kandev: Create explicitly requested persistent work; related follow-up uses parent_id="self".
|
||||
- spawn_session_kandev: Start an explicitly requested additional session/tab.
|
||||
- message_task_kandev: Send a prompt to an existing task session.{coordinator_task_control_section}
|
||||
- list_task_sessions_kandev: List a task's sessions and IDs.
|
||||
|
||||
IMPORTANT: You MUST use these MCP tools when instructed to create plans, ask questions, or interact with the Kandev platform. Do not skip them.
|
||||
IMPORTANT: Use these tools when instructed; do not skip them.
|
||||
|
||||
@@ -16,7 +16,6 @@ Available tools:
|
||||
- list_task_documents_kandev: List documents on an accessible related task. Required: task_id.
|
||||
- get_task_document_kandev: Read one document on an accessible related task. Required: task_id, document_key.
|
||||
- write_task_document_kandev: Create or replace a document on the current task or an ancestor. Required: task_id, document_key, content. Optional: title, type.
|
||||
- record_step_decision_kandev: Record an approve/reject decision for the current workflow step, as a reviewer or approver. Required: decision ("approved" or "rejected"), reason. Not idempotent: a second call supersedes the first.
|
||||
- step_complete_kandev: Signal that every requirement for the current workflow step is satisfied when the step requires an explicit signal. Required: summary.
|
||||
{step_complete_instruction}
|
||||
|
||||
|
||||
@@ -75,4 +75,5 @@ steps:
|
||||
name: Approved
|
||||
position: 3
|
||||
color: bg-green-500
|
||||
complete_task_on_enter: true
|
||||
allow_manual_move: true
|
||||
|
||||
@@ -349,4 +349,5 @@ steps:
|
||||
name: Done
|
||||
position: 7
|
||||
color: bg-green-500
|
||||
complete_task_on_enter: true
|
||||
allow_manual_move: true
|
||||
|
||||
@@ -54,6 +54,7 @@ steps:
|
||||
name: Done
|
||||
position: 3
|
||||
color: bg-green-500
|
||||
complete_task_on_enter: true
|
||||
allow_manual_move: true
|
||||
events:
|
||||
on_turn_start:
|
||||
|
||||
@@ -37,8 +37,10 @@ type stepDefYAML struct {
|
||||
AgentProfileID string `yaml:"agent_profile_id,omitempty"`
|
||||
ProfileSessionStartPolicy taskmodels.WorkflowProfileSessionStartPolicy `yaml:"profile_session_start_policy,omitempty"`
|
||||
ProfileSessionEndPolicy taskmodels.WorkflowProfileSessionEndPolicy `yaml:"profile_session_end_policy,omitempty"`
|
||||
SessionTarget *models.WorkflowSessionTarget `yaml:"session_target,omitempty"`
|
||||
AutoAdvanceRequiresSignal bool `yaml:"auto_advance_requires_signal,omitempty"`
|
||||
CancelTriggersTurnComplete bool `yaml:"cancel_triggers_turn_complete,omitempty"`
|
||||
CompleteTaskOnEnter bool `yaml:"complete_task_on_enter,omitempty"`
|
||||
Events stepEventsYAML `yaml:"events,omitempty"`
|
||||
}
|
||||
|
||||
@@ -167,6 +169,11 @@ func convertStep(s stepDefYAML) (models.StepDefinition, error) {
|
||||
if err != nil {
|
||||
return models.StepDefinition{}, fmt.Errorf("step %q: %w", s.ID, err)
|
||||
}
|
||||
if s.SessionTarget != nil {
|
||||
if err := models.ValidateWorkflowSessionTarget(s.SessionTarget); err != nil {
|
||||
return models.StepDefinition{}, fmt.Errorf("step %q: %w", s.ID, err)
|
||||
}
|
||||
}
|
||||
return models.StepDefinition{
|
||||
ID: s.ID,
|
||||
Name: s.Name,
|
||||
@@ -181,8 +188,10 @@ func convertStep(s stepDefYAML) (models.StepDefinition, error) {
|
||||
AgentProfileID: s.AgentProfileID,
|
||||
ProfileSessionStartPolicy: taskmodels.NormalizeWorkflowProfileSessionStartPolicy(string(s.ProfileSessionStartPolicy)),
|
||||
ProfileSessionEndPolicy: taskmodels.NormalizeWorkflowProfileSessionEndPolicy(string(s.ProfileSessionEndPolicy)),
|
||||
SessionTarget: models.CloneWorkflowSessionTarget(s.SessionTarget),
|
||||
AutoAdvanceRequiresSignal: s.AutoAdvanceRequiresSignal,
|
||||
CancelTriggersTurnComplete: s.CancelTriggersTurnComplete,
|
||||
CompleteTaskOnEnter: s.CompleteTaskOnEnter,
|
||||
StageType: stage,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -61,6 +61,32 @@ func TestLoadTemplates_CancelTriggersTurnCompleteDefaults(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadTemplates_CompletionPolicyUsesExplicitFinalStepSetting(t *testing.T) {
|
||||
templates, err := LoadTemplates()
|
||||
if err != nil {
|
||||
t.Fatalf("LoadTemplates() returned error: %v", err)
|
||||
}
|
||||
for _, template := range templates {
|
||||
for _, step := range template.Steps {
|
||||
want := template.ID != "improve-kandev" && template.ID != "report-kandev-issue" &&
|
||||
step.Position == lastTemplatePosition(template.Steps)
|
||||
if step.CompleteTaskOnEnter != want {
|
||||
t.Errorf("template %q step %q completion policy = %t, want %t", template.ID, step.Name, step.CompleteTaskOnEnter, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func lastTemplatePosition(steps []models.StepDefinition) int {
|
||||
last := 0
|
||||
for i, step := range steps {
|
||||
if i == 0 || step.Position > last {
|
||||
last = step.Position
|
||||
}
|
||||
}
|
||||
return last
|
||||
}
|
||||
|
||||
func TestLoadTemplates_AllValid(t *testing.T) {
|
||||
templates, err := LoadTemplates()
|
||||
if err != nil {
|
||||
|
||||
@@ -144,6 +144,7 @@ steps:
|
||||
position: 4
|
||||
color: bg-green-500
|
||||
stage_type: custom
|
||||
complete_task_on_enter: true
|
||||
allow_manual_move: true
|
||||
show_in_command_panel: true
|
||||
events:
|
||||
|
||||
@@ -117,4 +117,5 @@ steps:
|
||||
name: Done
|
||||
position: 3
|
||||
color: bg-green-500
|
||||
complete_task_on_enter: true
|
||||
allow_manual_move: true
|
||||
|
||||
@@ -169,4 +169,5 @@ steps:
|
||||
name: Done
|
||||
position: 2
|
||||
color: bg-green-500
|
||||
complete_task_on_enter: true
|
||||
allow_manual_move: true
|
||||
|
||||
@@ -27,5 +27,6 @@ steps:
|
||||
name: Done
|
||||
position: 1
|
||||
color: bg-green-500
|
||||
complete_task_on_enter: true
|
||||
show_in_command_panel: false
|
||||
allow_manual_move: false
|
||||
|
||||
+6
-6
@@ -39,11 +39,11 @@ require (
|
||||
go.opentelemetry.io/otel/trace v1.44.0
|
||||
go.uber.org/goleak v1.3.0
|
||||
go.uber.org/zap v1.26.0
|
||||
golang.org/x/crypto v0.52.0
|
||||
golang.org/x/net v0.55.0
|
||||
golang.org/x/sync v0.20.0
|
||||
golang.org/x/sys v0.45.0
|
||||
golang.org/x/text v0.37.0
|
||||
golang.org/x/crypto v0.57.0
|
||||
golang.org/x/net v0.58.0
|
||||
golang.org/x/sync v0.23.0
|
||||
golang.org/x/sys v0.48.0
|
||||
golang.org/x/text v0.42.0
|
||||
google.golang.org/grpc v1.83.1
|
||||
google.golang.org/protobuf v1.36.11
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
||||
@@ -133,7 +133,7 @@ require (
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/arch v0.3.0 // indirect
|
||||
golang.org/x/oauth2 v0.36.0 // indirect
|
||||
golang.org/x/term v0.43.0 // indirect
|
||||
golang.org/x/term v0.46.0 // indirect
|
||||
golang.org/x/time v0.11.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user