docs: migrate legacy specs and add product context (#2964)

This commit is contained in:
Carlos Florêncio
2026-08-24 11:09:03 +00:00
committed by GitHub
parent c071f6a5e3
commit 832734bb67
2136 changed files with 33965 additions and 23035 deletions
+1 -1
View File
@@ -178,7 +178,7 @@ Standalone agentctl is launched in its own process group so terminal Ctrl+C is h
- **Two session paths bypass the task service and must guard themselves.** (1) Handlers that resolve an execution by a bare in-memory lookup (`GetExecutionBySessionID`, `*BySessionID`) skip the `GetOrEnsure*` chokepoint where the lifecycle check runs — call `service.AuthorizeSessionAccess` (see `ProcessHandlers.denySessionAccess`) or `lifecycleMgr.CheckSessionAccess` first. (2) The orchestrator reads sessions through its own repo handle, so it inherits nothing from the task service; session-keyed entry points there call `authorizeSession` / `authorizeTask`, wired by `SetSessionAccessChecker` / `SetTaskAccessChecker`. An entry point that accepts **both** a task and a session ID must use `authorizeTaskSessionPair`, never `authorizeSession` alone: the caller can satisfy a session-only check with one of their own sessions while pointing `taskID` at someone else's task, which the method then uses for its task-scoped work. Put the guard **first** in the method, before any repo/executor/agent-manager use — `TestSessionKeyedEntryPointsGuardBeforeDependencies` runs each entry point with nil dependencies and fails on a panic if a guard is placed too late.
- **Dispatched WS actions have a gateway backstop.** `Client.authorizeAction` (`internal/gateway/websocket/dispatch_scope.go`) checks any payload carrying `task_id` or `session_id` against the client's identity *before* dispatch, so a newly added action is scoped by default rather than only when its author remembers. Handler-level scoping is still required (defense in depth, and it covers non-WS callers) — do not delete a service-layer check because the backstop exists. The backstop reads `task_id`, `session_id` and `task_environment_id`, plus `id` on **top-level `task.<verb>` actions only** (`task.state`, `task.move`, `task.get`, … — `task.` and exactly one more segment). Keep using those names for task-scoped actions, because an action that invents a new name for the same kind of resource silently opts out. (`user_shell.stop` was exactly that: it keys off `task_environment_id` with `task_id` optional, so a `task_id`-only backstop missed it. `task.state` and `task.move` were the second instance: they name the task `id`, so the backstop parsed no refs and let one user mutate any other user's task — hence the `id` rule.) Deeper `task.*` namespaces are deliberately excluded: in `task.plan.revision.get` and `task.review.finding.update` the `id` names a revision or a finding, so those actions must carry `task_id` when they are task-scoped. Also avoid permissive types (`any`, `json.RawMessage`) for those fields: a payload that fails to unmarshal into the ref struct is treated as naming nothing.
- **WS**: clients carry their identity; dispatched actions and subscriptions are scoped; workspace-carrying events route via `Hub.BroadcastToWorkspace`. A new `hub.Broadcast` (global) call site needs a `//ws:global` justification comment. Subscription actions (`*.subscribe`) `return` before reaching the dispatch backstop (`Client.authorizeAction`), so scoping a new subscribe topic means adding a hook to `SubscriptionAccessPolicy` (`internal/gateway/websocket/access.go`) *and* calling it explicitly inside that action's own handler — the backstop's `scopedActionRefs` cannot reach it. `run.subscribe`'s `Subscriptions.Run` hook (added for WO-02, resolving the run's workspace via `runs.GetRunWorkspaceID`) is the worked example.
- **Task overview vs. session detail:** Keep cross-task state in the bounded, persisted `TaskStatusSummary` projection and `task.status_summary.updated`; keep unbounded transcript and execution data session-scoped. The [bounded task-status spec](../../docs/specs/platform/bounded-task-status-delivery.md) and [accepted ADR](../../docs/decisions/2026-08-01-separate-task-summary-session-stream-traffic.md) define the shared contract.
- **Task overview vs. session detail:** Keep cross-task state in the bounded, persisted `TaskStatusSummary` projection and `task.status_summary.updated`; keep unbounded transcript and execution data session-scoped. The [bounded task-status spec](../../docs/specs/platform/requirements/bounded-task-status-delivery.md) and [accepted ADR](../../docs/decisions/2026-08-01-separate-task-summary-session-stream-traffic.md) define the shared contract.
- **Port-proxy subtree capability:** with auth enforced, `/port-proxy/:sessionId/:port/*` needs a credential on *every* request, but browser subresource fetches cannot always carry one (the `<link rel="manifest">` fetch never sends cookies; sandboxed iframes and `?token=`-opened previews drop them elsewhere). `PortProxyHandler` mints a short-lived (15 min, sliding), path-scoped, HMAC-signed capability after the document authenticates and propagates it as a cookie scoped to the proxy subtree plus a `kandev_cap` query parameter appended to rewritten asset URLs. `requireConnectionAuth` (`internal/gateway/websocket/access.go`) accepts either form only for the exact session:port it was minted for and restores the issuing identity, so `CheckSessionAccess` still gates on the real owner. Synthetic identities (auth disabled) skip minting — proxied bodies stay byte-identical to the pre-auth behavior.
- **Self-authenticating webhooks** (automation and office channels) and `/health`, `/api/v1/features`, `/api/v1/app-state` stay public — the allowlist lives in `auth/httpmw/middleware.go` with a pinning test. Plugin webhooks are not allowlisted: `auth/httpmw` structurally defers only GET/POST relay paths because it cannot read manifests, and `internal/plugins.Controller.webhook` enforces each manifest's `access` policy.
+1 -1
View File
@@ -59,7 +59,7 @@ const steerSetupHoldMillis = 30_000
// text of its own, so a mid-turn steer delivered while it runs can only be
// answered by the steer's own successor turn. This reproduces the "folded"
// outcome from the mid-turn steering spec's outcome taxonomy
// (docs/specs/platform/mid-turn-steering.md): the predecessor settles without
// (docs/specs/platform/requirements/mid-turn-steering.md): the predecessor settles without
// having produced an answer, and the operator sees a single, combined reply.
func scenarioSteerFoldSetup(e *emitter) {
waitForDelay(e.ctx, steerSetupHoldMillis)
@@ -59,7 +59,7 @@ type sshSessionState struct {
// Each session owns its own *ssh.Client (no shared pool). One SSH connection
// per session keeps teardown simple — closing the executor instance closes
// the client — at the cost of an extra TCP+handshake per session on the same
// host. See docs/specs/ssh-executor/spec.md for the full design.
// host. See docs/specs/executors/requirements/ssh-executor.md for the full design.
type SSHExecutor struct {
agentctlResolver *AgentctlResolver
secretStore secrets.SecretStore
@@ -349,7 +349,7 @@ func (c *Config) ToSharedConfig() *shared.Config {
//
// Delivery is opportunistic: whether the agent folds the prompt into the running
// turn or runs it as the next turn is the agent's decision and is not advertised
// over the protocol. See docs/specs/platform/mid-turn-steering.md.
// over the protocol. See docs/specs/platform/requirements/mid-turn-steering.md.
type SteerablePrompter interface {
SupportsSteering() bool
PromptSteer(
@@ -51,7 +51,7 @@ func (a *Adapter) SupportsSteering() bool {
// Delivery is opportunistic. Whether the agent folds this prompt into the
// running turn or runs it as the next turn is the agent's decision and is not
// advertised over the protocol, so both outcomes must be correct. See
// docs/specs/platform/mid-turn-steering.md.
// docs/specs/platform/requirements/mid-turn-steering.md.
func (a *Adapter) PromptSteer(
ctx context.Context,
message string,
@@ -27,7 +27,7 @@ const promptQueueingMetaKey = "promptQueueing"
// the bridge, is not advertised over ACP, and its version is not observable
// here. Mid-turn delivery is therefore opportunistic, and a caller must be
// correct whether the agent folds the prompt or runs it as the next turn. See
// docs/specs/platform/mid-turn-steering.md.
// docs/specs/platform/requirements/mid-turn-steering.md.
//
// Every malformed shape fails closed to false: absent `_meta`, absent
// namespace, a namespace that is not an object, a missing key, or a value that
@@ -294,7 +294,7 @@ type AgentEvent struct {
// prompt while another prompt for the same session is still in flight. It is
// the negotiated precondition for prompt handoff and mid-turn steering, and
// does not by itself promise the agent will fold that prompt into the running
// turn. See docs/specs/platform/mid-turn-steering.md.
// turn. See docs/specs/platform/requirements/mid-turn-steering.md.
SupportsPromptQueueing bool `json:"supports_prompt_queueing"`
// AuthMethods lists authentication methods from ACP initialize.
@@ -541,7 +541,7 @@ type ConfigOptionValue struct {
// per-turn total — see normalizeCodexPromptUsage in
// server/adapter/transport/acp/dialect_codex.go). Rows flagged estimated
// still count toward budget totals at face value per
// docs/specs/office/costs.md.
// docs/specs/office/requirements/costs.md.
type PromptUsage struct {
InputTokens int64 `json:"input_tokens"`
OutputTokens int64 `json:"output_tokens"`
@@ -128,7 +128,7 @@ func (h *Handlers) logout(c *gin.Context) {
// deliberately left alone: on a host serving a default-port instance (or a
// not-yet-upgraded one), it is that instance's LIVE session cookie, not an
// inert legacy jar — expiring it would log the other instance out (see
// docs/specs/fix-multi-instance-cookie-isolation/spec.md: the upgraded
// docs/specs/auth/requirements/fix-multi-instance-cookie-isolation.md: the upgraded
// instance does not proactively delete the legacy cookie).
clearSessionCookie(c, h.svc.CookieNameForRequest(c.Request))
c.Status(http.StatusNoContent)
@@ -271,7 +271,7 @@ func TestEnabledModeAllowlistMatrix(t *testing.T) {
{name: "office without bearer", method: http.MethodGet, path: "/api/v1/office/tasks/t1", blocked: true},
{
// AC-14 (docs/specs/health-endpoint-version/spec.md): /health
// AC-14 (docs/specs/platform/requirements/health-endpoint-version.md): /health
// gained a version field, but /api/v1/system/info is not on the
// allowlist and must stay rejected — the change must not widen it.
name: "system info", method: http.MethodGet, path: "/api/v1/system/info", blocked: true,
+1 -1
View File
@@ -3,7 +3,7 @@ package automation
import "gopkg.in/yaml.v3"
// exportDocumentVersion and exportDocumentType are the fixed values of the export
// document's `version` and `type` keys. See docs/specs/automations-yaml-export/spec.md
// document's `version` and `type` keys. See docs/specs/office/requirements/automations-yaml-export.md
// § Data model.
const (
exportDocumentVersion = 1
@@ -7,7 +7,7 @@ import (
// fieldDisposition records what AC-22 calls a field's "disposition": either exported
// under a named YAML key, or excluded for a stated reason. See
// docs/specs/automations-yaml-export/spec.md § Round-trip completeness, the field
// docs/specs/office/requirements/automations-yaml-export.md § Round-trip completeness, the field
// disposition table under "Data model".
//
// This is the anti-`reports_to` guard: Office's config export declared and emitted an
@@ -12,7 +12,7 @@ import (
)
// ExportHandler serves the two read-only YAML/zip automation export
// endpoints (see docs/specs/automations-yaml-export/spec.md § API surface).
// endpoints (see docs/specs/office/requirements/automations-yaml-export.md § API surface).
type ExportHandler struct {
svc *Service
log *logger.Logger
@@ -11,7 +11,7 @@ import (
)
// yamlNewlineChars is YAML 1.2's line-break set, per
// docs/specs/automations-yaml-export/spec.md's Definitions section:
// docs/specs/office/requirements/automations-yaml-export.md's Definitions section:
// {U+000A, U+000D, U+0085, U+2028, U+2029}. AC-17 and AC-46 are stated against this
// set, not against "\n" alone — a CR-only, NEL-only, LS-only, or PS-only prompt still
// forces a non-literal emission and must still warn under AC-17.
+1 -1
View File
@@ -101,7 +101,7 @@ type Automation struct {
// Derived in SQL (`execution_mode = 'task'`) rather than by reading the
// column into a field, so the raw mode never enters the Go model and
// cannot grow a firing-path branch: this is a migration-window notice,
// not a second destination. See docs/specs/office/automations-settings.md
// not a second destination. See docs/specs/office/requirements/automations-settings.md
// § Migration.
LegacyBoardCard bool `json:"legacy_board_card" db:"legacy_board_card"`
+1 -1
View File
@@ -147,7 +147,7 @@ const (
// automation used to put a card on the board", for a migration notice that
// closes once. Projecting the mode itself would hand every future caller a
// mode to branch on, and the whole point of withdrawing it is that no firing
// path has one. See docs/specs/office/automations-settings.md § Migration.
// path has one. See docs/specs/office/requirements/automations-settings.md § Migration.
const automationColumns = `id, workspace_id, name, description, workflow_id, workflow_step_id,
agent_profile_id, executor_profile_id, prompt, task_title_template,
enabled, max_concurrent_runs, webhook_secret, last_triggered_at, created_at, updated_at,
@@ -108,7 +108,7 @@ func TestBuildAgentProfileMatcher_SkipsDisabledProfile(t *testing.T) {
// TestBuildAgentProfileMatcher_PreservesDisabledExistingBinding is the
// reconciliation regression: a workflow step already bound to a profile that
// gets disabled after the fact must keep that binding on the next sync, per
// docs/specs/agents/profile-disable.md ("existing sessions/workflows keep
// docs/specs/agents/requirements/profile-disable.md ("existing sessions/workflows keep
// their profile"). Excluding disabled profiles from *candidate selection*
// must not also strip a binding that was already in place.
func TestBuildAgentProfileMatcher_PreservesDisabledExistingBinding(t *testing.T) {
@@ -62,7 +62,7 @@ type clarificationResponseClaim struct {
}
// Resolver implements ResolveBundle
// (docs/specs/external-question-answering/spec.md, "Resolution semantics"),
// (docs/specs/integrations/requirements/external-question-answering.md, "Resolution semantics"),
// the single service-layer operation the REST endpoints and the
// answer_question_kandev MCP tool both call. It resolves a bundle's identity
// and authorization (M5, A1-A9), validates the outcome (N6-N8b), claims it
@@ -4,7 +4,7 @@
// cookie scope — so multiple kandev instances on one host (same IP,
// different ports) share one cookie jar. Suffixing cookie names with the
// request's port lets each instance read and write only its own cookies
// (see docs/specs/fix-multi-instance-cookie-isolation/spec.md).
// (see docs/specs/auth/requirements/fix-multi-instance-cookie-isolation.md).
package httpcookie
import (
+1 -1
View File
@@ -230,7 +230,7 @@ type IssueWatch struct {
PollIntervalSeconds int `json:"pollIntervalSeconds" db:"poll_interval_seconds"`
// MaxInflightTasks caps how many open watcher-created tasks this watch can
// hold at once. nil = uncapped. Values <= 0 are rejected at the API layer.
// See docs/specs/throttle-watcher-fanout/spec.md for the open-task definition.
// See docs/specs/tasks/system-design/wip-limit-pull-system.md for the open-task definition.
MaxInflightTasks *int `json:"maxInflightTasks,omitempty" db:"max_inflight_tasks"`
LastPolledAt *time.Time `json:"lastPolledAt,omitempty" db:"last_polled_at"`
// LastError / LastErrorAt are stamped when the dispatch pipeline self-
+1 -1
View File
@@ -75,7 +75,7 @@ const createTablesSQL = `
poll_interval_seconds INTEGER NOT NULL DEFAULT 300,
-- Cap on concurrent open watcher-created tasks for this watch.
-- NULL = uncapped. Positive integer = cap. Values <= 0 are rejected at
-- the API layer. See docs/specs/throttle-watcher-fanout/.
-- the API layer. See docs/specs/tasks/system-design/wip-limit-pull-system.md.
max_inflight_tasks INTEGER DEFAULT 5,
last_polled_at DATETIME,
last_error TEXT NOT NULL DEFAULT '',
+1 -1
View File
@@ -229,7 +229,7 @@ type IssueWatch struct {
PollIntervalSeconds int `json:"pollIntervalSeconds" db:"poll_interval_seconds"`
// MaxInflightTasks caps how many open watcher-created tasks this watch can
// hold at once. nil = uncapped. Values <= 0 are rejected at the API layer.
// See docs/specs/throttle-watcher-fanout/spec.md for the open-task definition.
// See docs/specs/tasks/system-design/wip-limit-pull-system.md for the open-task definition.
MaxInflightTasks *int `json:"maxInflightTasks,omitempty" db:"max_inflight_tasks"`
// SortBy sets the dispatch order for matched issues; empty = Linear default order.
SortBy IssueSortBy `json:"sortBy,omitempty" db:"sort_by"`
+1 -1
View File
@@ -72,7 +72,7 @@ const createTablesSQL = `
poll_interval_seconds INTEGER NOT NULL DEFAULT 300,
-- Cap on concurrent open watcher-created tasks for this watch.
-- NULL = uncapped. Positive integer = cap. Values <= 0 are rejected at
-- the API layer. See docs/specs/throttle-watcher-fanout/.
-- the API layer. See docs/specs/tasks/system-design/wip-limit-pull-system.md.
max_inflight_tasks INTEGER DEFAULT 5,
-- Dispatch order for matched issues under the in-flight cap.
-- '' = Linear default order (updatedAt asc). See linear/issue_sort.go.
@@ -18,7 +18,7 @@ const (
)
// Budget period and action-on-exceed constants. See spec
// docs/specs/office-costs/spec.md for semantics.
// docs/specs/office/requirements/costs.md for semantics.
const (
budgetPeriodMonthly = "monthly"
@@ -191,7 +191,7 @@ func (s *CostService) EvaluateBudget(
// CheckPreExecutionBudget checks all applicable budget policies before
// launching an agent session. Returns (allowed, reason, error). Only
// pause_agent and block_new_tasks return allowed=false (notify_only
// alerts but does not block). See docs/specs/office-costs/spec.md.
// alerts but does not block). See docs/specs/office/requirements/costs.md.
// Implements shared.BudgetChecker.
func (s *CostService) CheckPreExecutionBudget(
ctx context.Context, agentInstanceID, projectID, workspaceID string,
@@ -184,7 +184,7 @@ func (c *Client) catalogVersionLocked() string {
// cold-cache-buffer parse path, where the buffer and its version are
// captured together before parsing — so a background refresh landing
// mid-call can never pair one catalogue's rates with a different
// catalogue's version identifier (docs/specs/office/costs.md).
// catalogue's version identifier (docs/specs/office/requirements/costs.md).
func (c *Client) LookupForModelWithVersion(ctx context.Context, modelID string) (shared.ModelPricing, string, bool) {
key, strategy := Normalize(modelID)
if strategy != StrategyLookup {
@@ -222,7 +222,7 @@ func (c *Client) LookupForModelWithVersion(ctx context.Context, modelID string)
// wholesale under c.mu — so a later, unrelated lookup for the same key would
// then read old rates paired with the new catalogue version, the exact
// provenance lie LookupForModelWithVersion exists to prevent
// (docs/specs/office/costs.md). Compares catalogGen rather than the RFC3339
// (docs/specs/office/requirements/costs.md). Compares catalogGen rather than the RFC3339
// version string: the string only has one-second resolution, so two installs
// landing in the same wall-clock second would otherwise compare equal and
// defeat this guard. The returned (pricing, bufVersion) pair for THIS call is
@@ -3,7 +3,7 @@ package dashboard
import "testing"
// TestDeriveTaskTimestamps locks down the Started/Completed sidebar
// derivation described in docs/specs/office/tasks.md: startedAt is the
// derivation described in docs/specs/office/requirements/tasks.md: startedAt is the
// earliest transition into in_progress, completedAt is the latest
// transition into done. A regression here silently reverts the Office
// task detail sidebar to permanently blank Started/Completed rows.
+1 -1
View File
@@ -135,7 +135,7 @@ func (s BudgetScopeType) Valid() bool {
// before this field existed — never inferred.
type CostSource string
// Cost-source values. See docs/specs/office/costs.md.
// Cost-source values. See docs/specs/office/requirements/costs.md.
const (
// CostSourceProviderReported means the CLI forwarded an authoritative
// USD amount (claude-acp's usage_update.cost.amount); no rates apply.
@@ -287,7 +287,7 @@ var ValidProjectStatuses = map[ProjectStatus]bool{
}
// CostContractVersion is the in-band activation point for the cache-split /
// cost-provenance / turn-attribution columns (docs/specs/office/costs.md).
// cost-provenance / turn-attribution columns (docs/specs/office/requirements/costs.md).
// The Rill cost extract has no schema versioning of its own, so a row
// written under a prior contract is distinguished by comparing
// cost_contract_version, not by a date an analyst has to be told out of
@@ -332,7 +332,7 @@ const CostContractVersion int64 = 3
// turn. The JSON cost-list representation includes this field as null so API
// consumers can make the same distinction. NULL is never backfilled to 0;
// TokensCachedIn keeps its original read+write sum semantics on every row so
// existing consumers of that column are unaffected. See docs/specs/office/costs.md.
// existing consumers of that column are unaffected. See docs/specs/office/requirements/costs.md.
type CostEvent struct {
ID string `json:"id" db:"id"`
SessionID string `json:"session_id" db:"session_id"`
@@ -267,7 +267,7 @@ func (r *Repository) createCostTables() error {
// columns must stay byte-identical to — see base_migrations.go). NULL
// means "not recorded" (legacy row, or an adapter that did not report
// cache data); 0 would silently claim zero cache activity. See
// docs/specs/office/costs.md.
// docs/specs/office/requirements/costs.md.
_, err := r.db.Exec(`
CREATE TABLE IF NOT EXISTS office_cost_events (
id TEXT PRIMARY KEY,
@@ -40,7 +40,7 @@ func (r *Repository) runMigrations() {
// migrateCostEventContract adds the cache read/write split, turn
// attribution, and cost-provenance columns to office_cost_events for
// databases created before this contract (docs/specs/office/costs.md). Every
// databases created before this contract (docs/specs/office/requirements/costs.md). Every
// ALTER is nullable with no DEFAULT: a legacy row must read NULL, never 0,
// because a merged tokens_cached_in cannot be decomposed and an unversioned
// "0" would be indistinguishable from "no cache activity". Keep this column
@@ -11,7 +11,7 @@ import (
)
// TestCostEventContractMigration_LegacyRowsGetNullColumns covers the
// legacy-column contract from docs/specs/office/costs.md: an office_cost_events
// legacy-column contract from docs/specs/office/requirements/costs.md: an office_cost_events
// table that predates the cache-split / cost-provenance / turn-attribution
// columns gets them added as nullable, a pre-existing row is left with NULL
// (never 0) in every new column, the partial unique index on
@@ -11,7 +11,7 @@ import (
// TestPostgresCostEventContractMigration is the PostgreSQL twin of
// TestCostEventContractMigration_LegacyRowsGetNullColumns
// (docs/specs/office/costs.md, ADR 0027): a pre-migration office_cost_events
// (docs/specs/office/requirements/costs.md, ADR 0027): a pre-migration office_cost_events
// table gets the cache-split / provenance / turn-attribution columns added
// nullable, a legacy row is left NULL (never 0), the partial unique index
// exists, and the migration replays cleanly on a second boot. Skips unless
@@ -26,7 +26,7 @@ import (
var ErrDuplicateUsageEvent = errors.New("duplicate usage_event_id")
// usageEventIndexName is the partial unique index enforcing at most one row
// per non-NULL usage_event_id (docs/specs/office/costs.md).
// per non-NULL usage_event_id (docs/specs/office/requirements/costs.md).
const usageEventIndexName = "uniq_office_cost_usage_event"
// sqliteUsageEventViolationMessage is the substring go-sqlite3 puts in a
@@ -53,7 +53,7 @@ func TestTaskMigrations_NewColumnsExist(t *testing.T) {
}
// Verify session columns (cost_subcents stores hundredths of a cent
// per docs/specs/office-costs/spec.md — the column was renamed from
// per docs/specs/office/requirements/costs.md — the column was renamed from
// cost_cents pre-release; no migration kept).
sessionColumns := []string{"cost_subcents", "tokens_in", "tokens_out"}
for _, col := range sessionColumns {
@@ -22,7 +22,7 @@ import (
var ErrTaskNotFound = errors.New("task not found")
// Automation runs never appear in a task list: they are hidden by their
// provenance, not by ephemerality (docs/specs/office/automations-settings.md).
// provenance, not by ephemerality (docs/specs/office/requirements/automations-settings.md).
// is_ephemeral keeps its original quick-chat meaning, so every list read here
// pairs the two.
const (
@@ -758,7 +758,7 @@ func TestCountActionableTasksForAgent_AgentIsolation(t *testing.T) {
// Automation runs are hidden from the task list by their origin, not by
// is_ephemeral: they are ordinary persistent tasks with their own destination
// (docs/specs/office/automations-settings.md). The quick chat alongside them
// (docs/specs/office/requirements/automations-settings.md). The quick chat alongside them
// still behaves exactly as it did.
func TestOfficeTaskListsExcludeAutomationOriginTasks(t *testing.T) {
repo := newSearchTestRepo(t)
@@ -155,7 +155,7 @@ type ResolveOptions struct {
type healthIndex map[ProviderID]map[string]map[string]models.ProviderHealth
// Resolve runs the resolution algorithm described in
// docs/specs/office-provider-routing/plan.md §Phase 2.
// docs/plans/office-execution-profile-routing/plan.md §Phase 2.
func (r *Resolver) Resolve(
ctx context.Context,
workspaceID string,
@@ -15,7 +15,7 @@ import (
// This pair exists because handlePromptUsage previously had two silent
// early returns (decode failure, task lookup failure) and a silent insert
// failure — "if this writer silently stops, nothing today would notice"
// (docs/specs/office/costs.md).
// (docs/specs/office/requirements/costs.md).
var (
costEventsWrittenTotal = expvar.NewMap("cost_events_written_total")
costEventsDroppedTotal = expvar.NewMap("cost_events_dropped_total")
@@ -596,7 +596,7 @@ func (s *Service) handleAgentFailed(ctx context.Context, event *bus.Event) error
// Office failure path (v1): every agent error is terminal. The
// retry-by-classifier path lives behind HandleRunFailure for
// rate-limit-retry callers; we deliberately do NOT call into it
// here. See docs/specs/office-agent-error-handling.
// here. See docs/specs/office/requirements/runtime.md.
errMsg := enrichModelFailureMessage(run, data.ErrorMessage)
if err := s.HandleAgentFailure(ctx, run, errMsg); err != nil {
return err
@@ -684,7 +684,7 @@ func (s *Service) tryPostStartFallback(
// handlePromptUsage records a cost event from a session/prompt usage
// update. Cost resolution follows the two-layer order from
// docs/specs/office/costs.md, and CostSource on the row records which
// docs/specs/office/requirements/costs.md, and CostSource on the row records which
// layer actually produced the dollar amount (see resolveCostForUsage in
// prompt_usage_cost.go — distinct from Estimated, a usage-authority flag):
//
@@ -77,7 +77,7 @@ func (s *Service) resolveCostForUsage(ctx context.Context, data PromptUsageData)
// one atomic snapshot when s.pricingLookup satisfies
// shared.PricingLookupWithVersion, so a concurrent background refresh can
// never pair one catalogue's rates with a different catalogue's version
// identifier on the stored row (docs/specs/office/costs.md). Falls back to two
// identifier on the stored row (docs/specs/office/requirements/costs.md). Falls back to two
// separate calls — accepting that narrower race — only for a PricingLookup
// implementation (e.g. a test double) that doesn't support the atomic form;
// CatalogVersion is optional there too, so a non-implementer simply reports
@@ -10,7 +10,7 @@
// across CLIs, and don't depend on prompt instructions the agent
// may rotate.
//
// See docs/specs/office-heartbeat-rework/spec.md §"Continuation
// See docs/specs/office/requirements/scheduler.md §"Continuation
// summary contract" for the section schema.
package summary
@@ -621,7 +621,7 @@ func seedRunSkillSnapshotHandler(repo *officesqlite.Repository, log *logger.Logg
// Used by the agent dashboard E2E spec to drive the costs section
// (aggregate + per-run rollup) without launching an agent. TokensOut is a
// pointer so a test can omit it to seed the "never measured" NULL shape
// (docs/specs/office/costs.md) instead of always writing a measured 0.
// (docs/specs/office/requirements/costs.md) instead of always writing a measured 0.
type seedCostEventRequest struct {
AgentProfileID string `json:"agent_profile_id"`
TaskID string `json:"task_id"`
@@ -1689,7 +1689,7 @@ func (s *Service) handleRecoverableFailureLocked(ctx context.Context, data watch
// working" and the topbar spinner clears. Kanban / quick-chat tasks
// keep the legacy WAITING_FOR_INPUT path so the user can resume via
// the Resume / Start fresh recovery buttons in the existing chat
// surface. (See docs/specs/office-agent-error-handling.)
// surface. (See docs/specs/office/requirements/runtime.md.)
nextState := models.TaskSessionStateWaitingForInput
if s.isOfficeSession(ctx, data.SessionID) {
nextState = models.TaskSessionStateFailed
@@ -513,7 +513,7 @@ const McpModeTaskTitlePending = "task-title-pending"
// McpModeOffice restricts the MCP toolset for office (autonomous) agents to
// interaction + plan tools. Office agents manage tasks via the kandev CLI
// (exposed through agentctl + $KANDEV_CLI), not MCP — see
// docs/specs/office-agent-cli/spec.md.
// docs/specs/office/system-design/agents-03.md.
const McpModeOffice = "office"
// LaunchOptions contains optional parameters for LaunchPreparedSession.
@@ -117,7 +117,7 @@ type MessageCreator interface {
// (Task tool) invocation observed on a tool-call frame. It returns nothing —
// a repository failure never fails the enclosing message write, turn, or
// agent stream (AC-27 in
// docs/specs/subagent-context-persistence/spec.md). Implemented by
// docs/specs/agents/requirements/subagent-context-persistence.md). Implemented by
// taskservice.Service via an adapter; optional, so an installation that
// never wires it behaves exactly as before.
type SubagentContextRecorder interface {
@@ -1144,7 +1144,7 @@ func (s *Service) startTask(ctx context.Context, taskID string, agentProfileID s
// Office tasks restrict the MCP toolset: kanban tools (move/update/list
// task, etc.) are excluded because office agents call those via the
// kandev CLI ($KANDEV_CLI). See docs/specs/office-agent-cli/spec.md.
// kandev CLI ($KANDEV_CLI). See docs/specs/office/system-design/agents-03.md.
mcpMode := ""
if isOfficeTask {
mcpMode = executor.McpModeOffice
@@ -212,7 +212,7 @@ func (s *Service) dispatchWatcherEvent(ctx context.Context, src WatcherSource, e
// Per-watcher throttle. Synchronous: we MUST acquire the slot before
// spawning the goroutine, otherwise a tight burst of bus events all
// read the same pre-creation DB count and overshoot the cap. See
// docs/specs/throttle-watcher-fanout/spec.md.
// docs/specs/tasks/system-design/wip-limit-pull-system.md.
//
// The slot covers the WHOLE dispatch pipeline, including the case where
// Dispatch returns early because Reserve loses the dedup race (no task
@@ -535,7 +535,7 @@ func (w *pluginWorker) logOverflow(dropped string) {
// Logs and gives up (event is lost from the live-delivery path; a
// subsequent health-poller error transition plus Flush is the recovery
// path for further events, per the at-least-once/error-buffer semantics in
// docs/specs/plugins/spec.md) if every attempt fails.
// docs/specs/plugins/requirements/plugins.md) if every attempt fails.
func (w *pluginWorker) deliverWithRetry(ctx context.Context, d Delivery) {
var lastErr error
attempts := 1 + len(w.deps.retryDelays)
@@ -23,7 +23,7 @@ type bufferedDelivery struct {
// ringBuffer holds events for one plugin while it is in the error state, so
// Deliverer.Flush can replay them in order after a health-poller recovery
// transition. Capacity and TTL are the numbers in
// docs/specs/plugins/spec.md ("Event webhook delivery"): 100 events / 5
// docs/specs/plugins/requirements/plugins.md ("Event webhook delivery"): 100 events / 5
// minutes. The oldest entry is dropped on overflow; expired entries are
// purged lazily on the next Add, Drain, or Len call.
type ringBuffer struct {
+1 -1
View File
@@ -44,7 +44,7 @@ type SetAutoUpdateRequest struct {
// SyncResult is the body of a successful POST /api/plugins/sync (and the
// return value of Service.Sync / Service.bootScan): what the filesystem
// scan under the plugins directory found and did this run, per
// docs/specs/plugins/spec.md ("Filesystem sideloading & sync").
// docs/specs/plugins/requirements/plugins.md ("Filesystem sideloading & sync").
type SyncResult struct {
// Added lists the plugin ids of directory sideloads
// (<pluginsDir>/<id>/<version>/manifest.yaml found with no existing
+1 -1
View File
@@ -96,7 +96,7 @@ var _ pluginsdk.Host = (*pluginHost)(nil)
// permissionDenied builds the gRPC error RemotePlugin/Host RPCs return for
// an undeclared capability, matching the wire-level message from
// docs/specs/plugins/spec.md ("Permissions"): "capability '<name>' not
// docs/specs/plugins/requirements/plugins.md ("Permissions"): "capability '<name>' not
// declared".
func permissionDenied(capability string) error {
return status.Errorf(codes.PermissionDenied, "capability '%s' not declared", capability)
@@ -1,5 +1,5 @@
// Package manifest defines the plugin manifest data model and validation
// rules described in docs/specs/plugins/spec.md ("Data model / Plugin
// rules described in docs/specs/plugins/requirements/plugins.md ("Data model / Plugin
// registration"). A manifest is the YAML document an operator supplies to
// POST /api/plugins/register.
package manifest
@@ -120,7 +120,7 @@ func buildShiftAlteredKeySet() map[string]bool {
}
// Validate checks the manifest against the plugin registration rules
// described in docs/specs/plugins/spec.md. It returns nil if the manifest
// described in docs/specs/plugins/requirements/plugins.md. It returns nil if the manifest
// is well-formed, or a joined error describing every violation found.
func (m *Manifest) Validate() error {
var errs []error
@@ -6,7 +6,7 @@
// The marketplace is a discovery layer only — it never installs, spawns, or
// mutates a plugin. Installing a catalog entry reuses the existing
// POST /api/plugins/install pipeline with the entry's package_url. See
// docs/specs/plugins/marketplace.md.
// docs/specs/plugins/requirements/marketplace.md.
package marketplace
import "time"
@@ -20,7 +20,7 @@ const (
)
// InstallState is the derived state of a catalog entry relative to what is
// installed locally (docs/specs/plugins/marketplace.md → "State machine").
// installed locally (docs/specs/plugins/requirements/marketplace.md → "State machine").
type InstallState string
const (
@@ -22,7 +22,7 @@ const tarballSuffix = ".tar.gz"
// Sync scans the plugins directory for filesystem-level changes an operator
// made outside the install/enable/disable/uninstall API — directory
// sideloads and dropped tarballs — and reconciles the registry with what
// actually exists on disk, per docs/specs/plugins/spec.md ("Filesystem
// actually exists on disk, per docs/specs/plugins/requirements/plugins.md ("Filesystem
// sideloading & sync"):
//
// 1. Dir sideloads: an extracted <id>/<version>/manifest.yaml with no
+1 -1
View File
@@ -1,7 +1,7 @@
// Package state provides a plugin-scoped key/value store backed by SQLite.
// Plugins read and write arbitrary JSON values under a (scope, scope_id, key)
// tuple, always filtered by plugin_id so a plugin can never read or write
// another plugin's state (spec: docs/specs/plugins/spec.md, "plugin_state").
// another plugin's state (spec: docs/specs/plugins/requirements/plugins.md, "plugin_state").
package state
import (
@@ -135,7 +135,7 @@ func TestStoreDeleteRemovesEntry(t *testing.T) {
}
// TestStoreDeleteAllRemovesEveryRowForPlugin pins the Uninstall cleanup
// contract (docs/specs/plugins/spec.md "plugin_state"): a plugin's entire
// contract (docs/specs/plugins/requirements/plugins.md "plugin_state"): a plugin's entire
// state footprint — across every scope and scope_id — must be removable in
// one call, so a reinstalled or id-reused plugin never inherits stale state.
func TestStoreDeleteAllRemovesEveryRowForPlugin(t *testing.T) {
@@ -248,7 +248,7 @@ func TestStoreListReturnsOnlyMatchingScope(t *testing.T) {
}
// TestStorePluginsCannotReadEachOthersState pins the spec invariant that
// plugin state is always filtered by plugin_id (docs/specs/plugins/spec.md
// plugin state is always filtered by plugin_id (docs/specs/plugins/requirements/plugins.md
// "Plugins cannot read others' state").
func TestStorePluginsCannotReadEachOthersState(t *testing.T) {
store := newTestStore(t)
+1 -1
View File
@@ -18,7 +18,7 @@ import (
)
// Status values for a plugin registration, per the state machine in
// docs/specs/plugins/spec.md ("State machine").
// docs/specs/plugins/requirements/plugins.md ("State machine").
const (
StatusRegistered = "registered"
StatusActive = "active"
+1 -1
View File
@@ -15,7 +15,7 @@ import (
)
// Status is a plugin's lifecycle state, per the state machine in
// docs/specs/plugins/spec.md ("State machine"):
// docs/specs/plugins/requirements/plugins.md ("State machine"):
//
// registered -> active -> disabled -> uninstalled
// registered|active|disabled --failure--> error
+1 -1
View File
@@ -1,5 +1,5 @@
// Package review turns a task's working diff into anchored, advisory code-review
// findings. See docs/specs/native-code-review/spec.md.
// findings. See docs/specs/agents/requirements/native-code-review.md.
//
// The package owns run orchestration only: persistence and event publication
// live in internal/task/service (ReviewService), and the inference call is made
@@ -3,7 +3,7 @@
// and exposes a RunQueueAdapter the workflow engine can use to
// enqueue runs without depending on the office package.
//
// Phase 3 of task-model-unification (see docs/specs/task-model-unification/plan.md
// Phase 3 of task-model-unification (see docs/specs/tasks/system-design/model-unification.md
// sections B3.2 and B3.5) lifted this logic out of internal/office/service
// and added an event-driven claim signal so engine-emitted runs reach
// the scheduler in a few ms instead of waiting up to one tick (5s).
+1 -1
View File
@@ -1,5 +1,5 @@
// Package cron owns the Phase 5 cron-driven trigger handlers (see
// docs/specs/task-model-unification/plan.md, B5.1–B5.4).
// docs/specs/tasks/system-design/model-unification.md, B5.1–B5.4).
//
// Three handlers run on a shared tick loop:
//
+1 -1
View File
@@ -2201,7 +2201,7 @@ type TaskReviewRun struct {
// TaskReviewFinding is one anchored, advisory review comment produced by a
// review run. It renders in the Changes/Review diff at File/StartLine..EndLine
// of Repository, and carries FileDiffHash so a client can tell whether the
// diff has moved under it (see ../../../../docs/specs/native-code-review/spec.md).
// diff has moved under it (see ../../../../docs/specs/agents/requirements/native-code-review.md).
type TaskReviewFinding struct {
ID string `json:"id"`
RunID string `json:"run_id"`
@@ -4,7 +4,7 @@ import "time"
// SubagentContext is a durable, queryable row for one subagent (Task tool)
// invocation observed by the orchestrator. See
// docs/specs/subagent-context-persistence/spec.md.
// docs/specs/agents/requirements/subagent-context-persistence.md.
//
// Every nullable column is a pointer. This is not style: ToolUseCount in
// particular must distinguish a reported 0 from "not reported", and a
@@ -579,7 +579,7 @@ type PlanRepository interface {
// SubagentContextRepository persists the durable, queryable record of a
// subagent (Task tool) invocation. See
// docs/specs/subagent-context-persistence/spec.md.
// docs/specs/agents/requirements/subagent-context-persistence.md.
type SubagentContextRepository interface {
// UpsertSubagentContext inserts or merges one subagent invocation row,
// keyed on (task_session_id, tool_call_id). A single atomic statement —
@@ -735,7 +735,7 @@ func (r *Repository) initSessionSchema() error {
// initSubagentContextSchema creates task_session_subagents, the durable
// relational record of a subagent (Task tool) invocation. See
// docs/specs/subagent-context-persistence/spec.md. The three measurement
// docs/specs/agents/requirements/subagent-context-persistence.md. The three measurement
// columns (total_tokens, tool_use_count, duration_ms) deliberately carry no
// DEFAULT: an unreported value must store NULL, never 0 (75% of observed
// invocations report none of them). turn_id carries no FOREIGN KEY so a turn
@@ -180,7 +180,7 @@ func scanClarificationBundleRows(rows *sql.Rows, isPostgres bool) ([]models.Clar
}
// visibilityPredicate builds the L1c/L1d three-way disjunction (spec
// docs/specs/external-question-answering/spec.md, "L1c"/"L1d"): the task's
// docs/specs/integrations/requirements/external-question-answering.md, "L1c"/"L1d"): the task's
// workspace_id is empty, names no existing workspace row, or is in the L1a
// visible set. For an unscoped caller the predicate is satisfied
// unconditionally, so no clause is added at all.
@@ -11,7 +11,7 @@ import "testing"
// or a replayed one. The claim path itself has no statement of its own to
// assert against: CompleteActiveClarificationBundle is the only clarification
// claim in the repository package, enforced by the compiler once the retired
// repo/model files are gone (docs/specs/external-question-answering/spec.md,
// repo/model files are gone (docs/specs/integrations/requirements/external-question-answering.md,
// P1).
func TestClarificationResolutionsTableNeverCreated(t *testing.T) {
repo := newRepoForSessionTests(t)
@@ -2445,7 +2445,7 @@ func (r *Repository) GetActiveTaskInfoByAgentProfile(ctx context.Context, agentP
// let one nightly report block its profile's deletion forever. Those are
// excluded, which makes them non-resumable if the profile goes: replying to
// an old run afterwards fails. That is the accepted trade — see
// docs/specs/office/automations-settings.md.
// docs/specs/office/requirements/automations-settings.md.
rows, err := r.ro.QueryContext(ctx, r.ro.Rebind(`
SELECT DISTINCT t.id, t.title, t.is_ephemeral
FROM task_sessions ts
@@ -34,7 +34,7 @@ func (r *Repository) warnSubagentContextMigration(name string, err error) {
// "subagent_task" (AC-21), and publishes the two write-once kandev_meta
// activation keys that let a consumer distinguish "zero fan-out" from
// "unmeasured, before capture began" (AC-24, AC-25). See
// docs/specs/subagent-context-persistence/spec.md § Backfill.
// docs/specs/agents/requirements/subagent-context-persistence.md § Backfill.
//
// runMigrations() (and therefore this function) runs on every boot, not just
// the first. AC-24d (revision 4) requires the guard to be "both backfill keys
@@ -4,7 +4,7 @@ package sqlite
// subagentPreciseTimestamp builds a SQL expression that evaluates to a value
// safe to compare with plain >= / < against another such expression, at no
// coarser than microsecond precision and monotonically across the whole
// range (AC-24e in docs/specs/subagent-context-persistence/spec.md). expr may
// range (AC-24e in docs/specs/agents/requirements/subagent-context-persistence.md). expr may
// be a TIMESTAMP column reference or a bind-parameter placeholder carrying an
// RFC3339(Nano) string (an activation key from kandev_meta).
//
@@ -37,7 +37,7 @@ type taskScanColumn struct {
// Automation runs are hidden from the board and from task lists by their
// provenance, not by ephemerality: their tasks are ordinary persistent tasks
// that keep their worktree and stay repliable, they just have their own
// destination (docs/specs/office/automations-settings.md). is_ephemeral keeps
// destination (docs/specs/office/requirements/automations-settings.md). is_ephemeral keeps
// its original quick-chat meaning, so every board read pairs the two.
const (
andNotAutomationOrigin = ` AND COALESCE(origin, '') != '` + models.TaskOriginAutomationRun + `'`
@@ -62,7 +62,7 @@ const subagentContextSourceLive = "live"
// invocation. It returns nothing: that is the mechanism by which AC-27
// holds — a writer with no error return cannot fail the enclosing message
// write, turn, or agent stream, and no future caller can accidentally
// propagate one. See docs/specs/subagent-context-persistence/spec.md.
// propagate one. See docs/specs/agents/requirements/subagent-context-persistence.md.
func (s *Service) RecordSubagentContext(ctx context.Context, req RecordSubagentContextRequest) {
if s.subagentContexts == nil {
return
@@ -5,7 +5,7 @@ import "expvar"
// subagentContextTotal exposes writer-health counters for
// RecordSubagentContext, under the existing expvar convention used by
// routing_* (internal/office/scheduler) and subproc_*. Counters only — see
// AC-26 in docs/specs/subagent-context-persistence/spec.md.
// AC-26 in docs/specs/agents/requirements/subagent-context-persistence.md.
//
// Keys: attempted, persisted, skipped_no_identity, anomalous_value, failed,
// unknown_execution (Amendment 1, AC-31).
+1 -1
View File
@@ -193,7 +193,7 @@ func (h *HTTPHandlers) httpRevoke(c *gin.Context) {
// mapShareError translates a service error into the response body + status
// code. Returns (nil, 0) when err is nil. The status table is part of the
// public contract; keep it in sync with docs/specs/public-share-links/spec.md.
// public contract; keep it in sync with docs/specs/auth/requirements/public-share-links.md.
func mapShareError(err error) (*errorBody, int) {
if err == nil {
return nil, 0
@@ -144,7 +144,7 @@ type SetSessionModeAction struct {
// RunCodeReviewAction starts a native code-review pass on step entry.
// AgentProfileID is optional; empty means "use the configured code-review
// utility agent". See docs/specs/native-code-review/spec.md.
// utility agent". See docs/specs/agents/requirements/native-code-review.md.
type RunCodeReviewAction struct {
AgentProfileID string
}
@@ -34,7 +34,7 @@ type AgentProfileResolver func(profileID string) *AgentProfilePortable
// during workflow-sync reconciliation); implementations should keep it when
// it still matches the descriptor even if the profile was since disabled -
// disabling only hides a profile from new selection, it doesn't touch
// existing bindings (docs/specs/agents/profile-disable.md).
// existing bindings (docs/specs/agents/requirements/profile-disable.md).
type AgentProfileMatcher func(agentName, model, mode, currentID string) string
// WorkflowPortable is a workflow without instance-specific fields (IDs, timestamps).
@@ -674,7 +674,7 @@ func TestApplySyncedWorkflows_WorkflowRebindWarningDoesNotFireOnFailedUpdate(t *
// updateSyncedWorkflow passes the step's existing AgentProfileID to the
// matcher as currentID. A real matcher (buildAgentProfileMatcher) uses that
// to preserve a binding that was disabled after the fact instead of treating
// disablement as a rebind (docs/specs/agents/profile-disable.md). This test
// disablement as a rebind (docs/specs/agents/requirements/profile-disable.md). This test
// stubs that same contract: the matcher echoes currentID back when given
// one, standing in for "the bound profile still matches, keep it".
func TestApplySyncedWorkflows_StepMatcherReceivesCurrentBinding(t *testing.T) {
+2 -2
View File
@@ -90,7 +90,7 @@ For rebasing or finishing PRs written against the old Next.js runtime, follow [`
Use subscription hooks only; the WS client auto-deduplicates.
**Task overview vs. session detail:** Shared task rows read `Task.statusSummary` and `task.status_summary.updated`; rich streams stay session-detail-only. Extend the bounded projection per the [spec](../../docs/specs/platform/bounded-task-status-delivery.md) and [ADR](../../docs/decisions/2026-08-01-separate-task-summary-session-stream-traffic.md).
**Task overview vs. session detail:** Shared task rows read `Task.statusSummary` and `task.status_summary.updated`; rich streams stay session-detail-only. Extend the bounded projection per the [spec](../../docs/specs/platform/requirements/bounded-task-status-delivery.md) and [ADR](../../docs/decisions/2026-08-01-separate-task-summary-session-stream-traffic.md).
**Branch-scoped task state:** For live worktree/session state plus `task_prs`, key by `(repository, checked-out branch)`, not task/repository alone. `branch_switched` invalidates prior status/commits; reject late results with a generation/identity guard and preserve siblings. Historical PRs affect Changes only when `repository_id` and normalized `head_branch` match; Review/PR history may still show them. Test single/multi-repo cases and desktop/mobile Changes behavior.
**HTTP/WS cache races:** When HTTP hydrates a cache also updated by WebSockets, guard responses with per-scope revision and request/workspace generation; discard or refresh stale responses and cover deferred responses. `useEnsureTaskSession` re-fires `session.ensure` when an open task page sees zero sessions after a prior ensure, so deleting the last session from that page spawns a replacement; test zero-session states through the backend/API instead.
@@ -246,7 +246,7 @@ mandatory reason — reasonless or stale entries are errors. For zh-tw/zh-hk run
`i18n:check` also gates key/catalog drift, `<Trans>` indices, inline plurals,
module-scope `t()`, em dashes, and the **pseudo-locale** check. Needs **Node 24**.
Guide: [`docs/i18n.md`](../../docs/i18n.md); spec
[`docs/specs/platform/i18n.md`](../../docs/specs/platform/i18n.md).
[`docs/specs/platform/requirements/i18n.md`](../../docs/specs/platform/requirements/i18n.md).
## Markdown safety
@@ -287,7 +287,7 @@ function InheritedTierHint({
// AC-17 / AC-17a / AC-17b: names the wake reasons whose policy shadows the
// tier displayed above, whichever level supplied it. The effective map is
// the agent's own tier_per_reason when it overrides the workspace policy
// (an override replaces the map entirely, per docs/specs/office/routing.md)
// (an override replaces the map entirely, per docs/specs/office/requirements/routing.md)
// and the workspace map otherwise; keys with an empty value do not shadow
// anything and are excluded.
function effectiveWakeReasonMap(
@@ -47,7 +47,7 @@ export function formatShortDate(iso: string): string {
* Renders an int64 subcents (hundredths of a cent) value as USD. The
* backend ships every cost figure as a subcents integer so we don't
* carry float drift; the formatter just divides by 10000 at the edge.
* See docs/specs/office-costs/spec.md for the unit contract.
* See docs/specs/office/requirements/costs.md for the unit contract.
*/
export function formatSubcents(subcents: number): string {
if (!Number.isFinite(subcents)) return "$0.00";
@@ -33,7 +33,7 @@ type Props = {
// RoleTierCard renders the per-role tier policy table on the workspace
// routing settings page: the third of four precedence levels, below a
// wake-reason policy and a per-agent override, above the workspace default.
// See docs/specs/office-agent-tier-routing/spec.md.
// See docs/specs/office/requirements/office-agent-tier-routing.md.
export function RoleTierCard({ roles, config, value, onChange, disabled }: Props) {
const { t } = useTranslation();
const handleRowChange = (role: AgentRole, tier: Tier | typeof USE_WORKSPACE_DEFAULT) => {
@@ -44,7 +44,7 @@ export function rememberWorkspaceSelectionById(id: string, kind: "office" | "kan
* that instance's LIVE selection cookie, and for upgraded ported instances it
* is the validated migration fallback until each writes its own scoped value.
* Scrubbing it from one instance would change the other instances' next boot
* (see docs/specs/fix-multi-instance-cookie-isolation/spec.md: the upgraded
* (see docs/specs/auth/requirements/fix-multi-instance-cookie-isolation.md: the upgraded
* instance does not proactively delete the legacy cookie).
*/
function writeWorkspaceCookie(name: string, value: string): void {
@@ -22,7 +22,7 @@ import { settingsActionClassName } from "@/components/settings/settings-control"
/**
* Operator UI to browse, install, enable, disable, uninstall, and update kandev
* plugins (docs/specs/plugins/marketplace.md). Gated on the `plugins` feature
* plugins (docs/specs/plugins/requirements/marketplace.md). Gated on the `plugins` feature
* flag by the page-level default export.
*/
export function PluginsSettings() {
@@ -23,7 +23,7 @@ export type ConditionalTodoPanelAction = "add" | "remove" | "none";
* Details conditional panel, there is no per-instance identity to reconcile
* and no closed-for-session suppression: the `showTodoListPanel` preference
* is the single authoritative on/off switch (see
* docs/specs/ui/agent-todo-list-panel.md — "true, unconditional visibility
* docs/specs/ui/requirements/agent-todo-list-panel.md — "true, unconditional visibility
* gate"). The "only pin when not empty" sub-option (`onlyPinWhenNotEmpty`)
* gates only the automatic add: while it is on, an absent panel is not added
* until the active session's todo list is non-empty (`todoListNotEmpty`).
@@ -33,7 +33,7 @@ function formatDate(dateStr: string): string {
}
// formatCurrency converts subcents (hundredths of a cent — the office
// cost storage unit per docs/specs/office-costs/spec.md) to a USD string.
// cost storage unit per docs/specs/office/requirements/costs.md) to a USD string.
function formatCurrency(subcents: number): string {
return new Intl.NumberFormat(undefined, {
style: "currency",
@@ -4,7 +4,7 @@ import { AutomationsPage } from "../pages/automations-page";
/**
* E2E coverage for the github_pr_merged ("Pull request merged") trigger type.
*
* Spec: docs/specs/automations-pr-merged-trigger/spec.md
* Spec: docs/specs/office/requirements/automations-pr-merged-trigger.md
* Decisions pinned here:
* - Picker shows "Pull request merged" under GitHub group, immediately after "New pull requests"
* - Config round-trip: all_repos, repos, base_branches survive save/reopen
@@ -10,7 +10,7 @@ import { registerSeparateQueueRows } from "../../helpers/message-queue-settings"
registerSeparateQueueRows(test);
// End-to-end coverage for the mid-turn steering composer contract
// (docs/specs/platform/mid-turn-steering.md). CONTRIBUTING.md requires Playwright
// (docs/specs/platform/requirements/mid-turn-steering.md). CONTRIBUTING.md requires Playwright
// coverage for UI changes; this exercises the flag-gated decision and the real
// steer dispatch path through the mock agent, which advertises promptQueueing.
//
@@ -153,7 +153,7 @@ test.describe.serial("Claude mid-turn steering experiment", () => {
});
// The two outcomes below replay the mid-turn steering spec's "folded" vs
// "deferred" outcome taxonomy (docs/specs/platform/mid-turn-steering.md)
// "deferred" outcome taxonomy (docs/specs/platform/requirements/mid-turn-steering.md)
// via the dedicated mock-agent setup scenarios: steer-fold-setup never
// answers on its own (task 08's mock-replay acceptance for "folded" —
// pinned deterministically at the mock level by
@@ -8,7 +8,7 @@ import { test, expect } from "../../fixtures/test-base";
* externalization sweep: any plain-ASCII user-facing text under `pseudo` is a
* literal that was never routed through `t()`.
*
* See docs/specs/platform/i18n.md and docs/i18n.md.
* See docs/specs/platform/requirements/i18n.md and docs/i18n.md.
*/
const APPEARANCE_URL = "/settings/preferences/appearance";
@@ -20,7 +20,7 @@ async function expandIntegrationsSection(testPage: Page) {
await expect(integrationsToggle).toHaveAttribute("aria-expanded", "true");
}
// Covers docs/specs/integrations/enable-disable-toggle.md's nav-visibility
// Covers docs/specs/integrations/requirements/enable-disable-toggle.md's nav-visibility
// scenarios: with "Hide disabled integrations from left panel navigation"
// off (the default), a disabled-but-configured integration still shows in
// the sidebar; turning the setting on hides it; turning it back off (or
@@ -1,6 +1,6 @@
import { test, expect } from "../../fixtures/test-base";
// Covers docs/specs/integrations/enable-disable-toggle.md's per-workspace
// Covers docs/specs/integrations/requirements/enable-disable-toggle.md's per-workspace
// scoping: the enable toggle belongs to the workspace whose settings page it
// sits on, so disabling an integration in one workspace leaves every other
// workspace's toggle alone. It shipped install-wide first, which silenced an
@@ -1,6 +1,6 @@
import { test, expect } from "../../fixtures/test-base";
// Covers docs/specs/integrations/enable-disable-toggle.md's index-page slider
// Covers docs/specs/integrations/requirements/enable-disable-toggle.md's index-page slider
// scenarios: every integration row has a working slider, toggling it never
// navigates, and the toggle stays in sync with that integration's own
// settings page (the shared per-workspace `useXEnabled` state).
@@ -3,7 +3,7 @@ import { test, expect } from "../../fixtures/office-fixture";
/**
* E2E coverage for the office task approval gate (in_review → done is
* blocked until every approver has a current "approved" decision). Spec:
* docs/specs/office-task-approval-flow/spec.md
* docs/specs/office/requirements/inbox.md
*
* Approach:
* - Use `officeApi.createAgent` to add an approver to the workspace.
@@ -4,7 +4,7 @@ import { test, expect } from "../../fixtures/office-fixture";
* E2E coverage for the office dashboard "Agents" panel: per-agent cards
* rendered always (even with zero activity), driven reactively by the
* existing WS pipeline (`session.state_changed`, `office.task.updated`,
* `office.agent.updated`). Spec: docs/specs/office-dashboard-agent-cards.
* `office.agent.updated`). Spec: docs/specs/office/requirements/dashboard.md.
*
* The lifecycle harness is rich enough to drive these scenarios without a
* real executor: `apiClient.seedTaskSession` writes directly to the DB
@@ -4,7 +4,7 @@ import { balancedExecutionProfileRouting } from "../../helpers/office-routing";
/**
* Phase 7 spec #2 — provider fallback under quota_limited.
*
* Scenario (per docs/specs/office-provider-routing/spec.md):
* Scenario (per docs/specs/office/requirements/routing.md):
* 1. Workspace routing enabled with order `claude-acp → codex-acp`,
* tier `balanced`.
* 2. Backend is launched with `KANDEV_PROVIDER_FAILURES=claude-acp:
@@ -4,7 +4,7 @@ import { test, expect } from "../../fixtures/office-fixture";
* E2E coverage for the office task session lifecycle (per-(task, agent)
* persistent sessions, RUNNING ↔ IDLE state cycling, COMPLETED only on
* participation removal). Spec:
* docs/specs/office-task-session-lifecycle/spec.md
* docs/specs/office/requirements/tasks.md
*
* Harness limitations:
* - The /api/v1/_test/task-sessions route only INSERTS new rows; it does
@@ -4,7 +4,7 @@ import path from "node:path";
import { test, expect } from "../../fixtures/test-base";
import { OfficeApiClient } from "../../helpers/office-api-client";
// Verifies the spec-aligned skill injection (docs/specs/office/agents.md):
// Verifies the spec-aligned skill injection (docs/specs/office/requirements/agents.md):
// when a skill is assigned to an agent profile, launching a session writes
// it to <worktree>/<projectSkillDir>/kandev-<slug>/SKILL.md and appends
// the kandev-* glob to .git/info/exclude.
@@ -1,6 +1,6 @@
/**
* E2E: `PluginComposerCapability` against the real native composers
* (docs/specs/plugins/voice-extraction-host.md).
* (docs/specs/plugins/requirements/voice-extraction-host.md).
*
* These are the prerequisites the extracted Voice Mode plugin stands on, so
* everything here goes through a genuinely registered plugin slot and then
@@ -1,6 +1,6 @@
/**
* Pixel 5 coverage for `PluginComposerCapability`
* (docs/specs/plugins/voice-extraction-host.md, "Mobile Design Contract").
* (docs/specs/plugins/requirements/voice-extraction-host.md, "Mobile Design Contract").
*
* Capability parity, not layout parity: the same action, the same insertion
* and the same native submit have to work by touch, from the shipped mobile
+1 -1
View File
@@ -114,7 +114,7 @@ function pluginExecutablePath(installPath: string): string {
* Stages a filesystem sideload: extracts the same fixture package the
* upload tests use directly into `<pluginsDir>/<id>/<version>/`, with no
* `{id}.yml` record — the on-disk shape `Service.Sync`'s directory-sideload
* step looks for (docs/specs/plugins/spec.md "Filesystem sideloading &
* step looks for (docs/specs/plugins/requirements/plugins.md "Filesystem sideloading &
* sync"). `checksums.txt` lands in the directory alongside the rest of the
* package; the sideload path ignores it (only the tarball-install path
* verifies checksums).
@@ -1,6 +1,6 @@
import { test, expect } from "../../fixtures/test-base";
// Covers docs/specs/agents/hide-disabled-profiles-nav.md's nav-visibility
// Covers docs/specs/agents/requirements/hide-disabled-profiles-nav.md's nav-visibility
// scenarios: with "Hide disabled agent profiles from left panel navigation"
// off (the default), a disabled profile still shows in the Settings left
// panel's Agents tree; turning the setting on hides it; re-enabling the

Some files were not shown because too many files have changed in this diff Show More