From 4518f41fd49e978e6552ebf084b4c72fbdfae2d4 Mon Sep 17 00:00:00 2001 From: octopus Date: Wed, 16 Sep 2026 13:13:18 +0800 Subject: [PATCH] v0.1.3: encoding remediation - restore 11 core files to clean UTF-8 + add check:encoding-health guard --- README.md | 5 +- core/schemas/port-analysis.schema.json | 2 +- core/schemas/review-status.schema.json | 2 +- core/schemas/reviewer-output.schema.json | 4 +- core/schemas/runs-bundle.schema.json | 8 +- core/schemas/runs-index.schema.json | 6 +- core/schemas/runs-meta.schema.json | 6 +- core/schemas/workflow-routing.schema.json | 22 +- core/skills/gitea-rest/SKILL.md | 68 ++--- core/skills/headless-session-ops/SKILL.md | 300 +++++++++++----------- core/skills/project-kickoff/SKILL.md | 98 +++---- core/skills/release/SKILL.md | 158 ++++++------ guards/check-encoding-health.ts | 94 +++++++ package.json | 5 +- 14 files changed, 437 insertions(+), 341 deletions(-) create mode 100644 guards/check-encoding-health.ts diff --git a/README.md b/README.md index eac17c4..2c8f813 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ the corpus internally consistent and publish-ready. The corpus does not assume any specific forge (GitHub / Gitea / GitLab), ticketing, or CI system. System-specific behavior lives behind an explicit adapter contract (`core/adapters/`), so the same core can be adopted by any -octopus instance — or any other agent platform — with its own adapters. +octopus instance 鈥?or any other agent platform 鈥?with its own adapters. ## Layout @@ -20,7 +20,7 @@ octopus instance — or any other agent platform — with its own adapters. | `core/templates/` | Artifact templates (DAG, iteration docs) | | `core/schemas/` | JSON schemas under the public namespace `https://eightarms.net/octopus-workflow/schemas/` | | `core/adapters/` | Adapter contract: terminology SSOT + Gitea reference implementation | -| `core/CORE-MANIFEST.json` | Registry of core↔instance mappings and sync semantics (verbatim / verbatimDir / rewritten / split / core-only) | +| `core/CORE-MANIFEST.json` | Registry of core鈫攊nstance mappings and sync semantics (verbatim / verbatimDir / rewritten / split / core-only) | | `examples/` | Organization-agnostic narrative essays referencing the core | | `guards/` | Standalone drift guards (bun + node stdlib only) | @@ -32,6 +32,7 @@ Runnable inside this repo with [bun](https://bun.sh), no install step needed: bun run check:core-cohesion # corpus structural invariants bun run check:core-p1 # publish-readiness neutrality (no org-internal links) bun run delink:core -- --check # no replaceable org-internal refs remain +bun run check:encoding-health # core/ text is clean UTF-8 (no GBK mojibake) ``` The instance-side guards (`check-core-parity`, `check-schema-ids`, diff --git a/core/schemas/port-analysis.schema.json b/core/schemas/port-analysis.schema.json index 0228bc6..d66b2c7 100644 --- a/core/schemas/port-analysis.schema.json +++ b/core/schemas/port-analysis.schema.json @@ -2,7 +2,7 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://eightarms.net/octopus-workflow/schemas/port-analysis.schema.json", "title": "Port Analysis Reviewer Output", - "description": "Schema for per-dimension port-analysis reviewer findings. Used by the large-port-pipeline source analysis review (Phase B1.5). The port source-analysis pipeline uses a deliberate three-value severity contract 鈥?BLOCKER / MAJOR / MINOR 鈥?and never emits INFO (matching implement/reference/large-port-pipeline.md and source-analysis.md, which name only those three). This is a documented exception to the canonical four-value set (BLOCKER / MAJOR / MINOR / INFO) in reviewer-output.schema.json and synthesis.schema.json.", + "description": "Schema for per-dimension port-analysis reviewer findings. Used by the large-port-pipeline source analysis review (Phase B1.5). The port source-analysis pipeline uses a deliberate three-value severity contract — BLOCKER / MAJOR / MINOR — and never emits INFO (matching implement/reference/large-port-pipeline.md and source-analysis.md, which name only those three). This is a documented exception to the canonical four-value set (BLOCKER / MAJOR / MINOR / INFO) in reviewer-output.schema.json and synthesis.schema.json.", "type": "object", "required": ["dimension", "findings"], "properties": { diff --git a/core/schemas/review-status.schema.json b/core/schemas/review-status.schema.json index 6a9c840..d874c74 100644 --- a/core/schemas/review-status.schema.json +++ b/core/schemas/review-status.schema.json @@ -19,7 +19,7 @@ "max_rounds": { "type": "integer", "default": 3, - "description": "Per-skill bound: `review-artifact` skill (process target) = 3 (the roadmap/design/plan targets were archived 2026-08-21, [org-internal #3072] phase 3); `review-code` = 2 default / 3 high-risk (`workflow-routing.yaml` `review.max_rounds`, [org-internal #3567]). `review-dag` (the review-artifact skill's DAG single-gate target) binds per depth from `dag_metrics.review_depth`: D1 鈮?, D2 鈮?, D3 鈮?, D4 鈮? 鈥?D4 reaches round 4, exceeding the shared `review-artifact` default, so the DAG per-depth cap takes precedence (see review-artifact/SKILL.md review-dag Phase D MAX_ROUNDS). Set explicitly by the skill; default 3 is a fallback only." + "description": "Per-skill bound: `review-artifact` skill (process target) = 3 (the roadmap/design/plan targets were archived 2026-08-21, [org-internal #3072] phase 3); `review-code` = 2 default / 3 high-risk (`workflow-routing.yaml` `review.max_rounds`, [org-internal #3567]). `review-dag` (the review-artifact skill's DAG single-gate target) binds per depth from `dag_metrics.review_depth`: D1 ≤2, D2 ≤3, D3 ≤3, D4 ≤4 — D4 reaches round 4, exceeding the shared `review-artifact` default, so the DAG per-depth cap takes precedence (see review-artifact/SKILL.md review-dag Phase D MAX_ROUNDS). Set explicitly by the skill; default 3 is a fallback only." }, "converged": { "type": "boolean" diff --git a/core/schemas/reviewer-output.schema.json b/core/schemas/reviewer-output.schema.json index e626653..9658c96 100644 --- a/core/schemas/reviewer-output.schema.json +++ b/core/schemas/reviewer-output.schema.json @@ -17,7 +17,7 @@ "reviewer_slot": { "type": "integer", "minimum": 1, - "description": "1-based slot of this reviewer WITHIN its dimension, when dim_split assigns >1 reviewer to the dimension (e.g. review-dag D3 TOPO:2, D4 TOPO:2 鈥?dim_split per `workflow-routing.yaml` dag.size_derivation.review_depth). Multi-reviewer dimensions write one file per slot: findings-{DIMENSION_CODE}-r{k}.json. Omitted for single-reviewer dimensions (file stays findings-{DIMENSION_CODE}.json). Preserves per-reviewer attribution for the retrospective overlap analysis ([org-internal #2996] step 1a)." + "description": "1-based slot of this reviewer WITHIN its dimension, when dim_split assigns >1 reviewer to the dimension (e.g. review-dag D3 TOPO:2, D4 TOPO:2 — dim_split per `workflow-routing.yaml` dag.size_derivation.review_depth). Multi-reviewer dimensions write one file per slot: findings-{DIMENSION_CODE}-r{k}.json. Omitted for single-reviewer dimensions (file stays findings-{DIMENSION_CODE}.json). Preserves per-reviewer attribution for the retrospective overlap analysis ([org-internal #2996] step 1a)." }, "reviewer_count": { "type": "integer", @@ -79,7 +79,7 @@ "checklist_ref": { "type": "string", "pattern": "^[A-Z0-9]{2,6} \\d+(\\.\\d+)+(\\s*\\([^\\)]*\\))?(\\s*/\\s*[A-Z0-9]{2,6} \\d+(\\.\\d+)+(\\s*\\([^\\)]*\\))?)*$", - "description": "Checklist item reference, e.g. COR 1.1, INV 1.3, SEC 3.7.1, STD 8.4 鈥?dimension/section code followed by item number(s)." + "description": "Checklist item reference, e.g. COR 1.1, INV 1.3, SEC 3.7.1, STD 8.4 — dimension/section code followed by item number(s)." }, "location": { "type": "string", diff --git a/core/schemas/runs-bundle.schema.json b/core/schemas/runs-bundle.schema.json index ba6a61b..e80f9cb 100644 --- a/core/schemas/runs-bundle.schema.json +++ b/core/schemas/runs-bundle.schema.json @@ -11,7 +11,7 @@ "index": true, "working_notes": { "type": "string", - "description": "Final content of runs/{slug}/working-notes.md ([org-internal #2600]), inlined at archive time when the file exists. The one Tier 1 artifact whose CONTENT (not just digest) rides in the bundle 鈥?the run's narrative memory (decisions, dead ends, reviewer feedback), consumed by later retro probes and the notes-injection degradation path ([org-internal #2601]). Absent when the run kept no working notes." + "description": "Final content of runs/{slug}/working-notes.md ([org-internal #2600]), inlined at archive time when the file exists. The one Tier 1 artifact whose CONTENT (not just digest) rides in the bundle — the run's narrative memory (decisions, dead ends, reviewer feedback), consumed by later retro probes and the notes-injection degradation path ([org-internal #2601]). Absent when the run kept no working notes." }, "review_history": { "type": "array", @@ -55,7 +55,7 @@ "stage": { "type": "string", "minLength": 1, - "description": "Review stage identifier matching the reviews/{stage}/ directory (e.g. code, review-dag, audit-process 鈥?the live values; design-space is a retired historical value)." + "description": "Review stage identifier matching the reviews/{stage}/ directory (e.g. code, review-dag, audit-process — the live values; design-space is a retired historical value)." }, "rounds": { "type": "integer", "minimum": 1, "description": "Number of executed rounds." }, "final_verdict": { "type": "string", "enum": ["PASS", "WARN", "FAIL"] }, @@ -109,7 +109,7 @@ "type": "object", "additionalProperties": true, "required": ["slug", "state", "created_at"], - "description": "Legacy profile (schema_version 1 or absent): historical bundles AND current live-workspace runs 鈥?runs-meta.schema.json / runs-index.schema.json pin schema_version 1 today. Core fields type-checked when present; extra context properties (route/pr/node/kind/...) tolerated as-is. The v2 strict profile (below) is the migration target: it becomes the profile for new runs once the live-workspace schemas are bumped to emit schema_version 2 with the context bag.", + "description": "Legacy profile (schema_version 1 or absent): historical bundles AND current live-workspace runs — runs-meta.schema.json / runs-index.schema.json pin schema_version 1 today. Core fields type-checked when present; extra context properties (route/pr/node/kind/...) tolerated as-is. The v2 strict profile (below) is the migration target: it becomes the profile for new runs once the live-workspace schemas are bumped to emit schema_version 2 with the context bag.", "properties": { "schema_version": { "type": "integer", "minimum": 1 }, "slug": { "type": "string", "pattern": "^[a-z0-9][a-z0-9-]*$" }, @@ -132,7 +132,7 @@ "type": "object", "additionalProperties": false, "required": ["schema_version", "slug", "state", "created_at", "updated_at", "ticket"], - "description": "Strict profile (schema_version 2): closed core field set. Run-specific context (route/pr/node/task_node/...) goes in the context bag 鈥?the ONLY place for non-core fields.", + "description": "Strict profile (schema_version 2): closed core field set. Run-specific context (route/pr/node/task_node/...) goes in the context bag — the ONLY place for non-core fields.", "allOf": [ { "if": { "properties": { "state": { "const": "archived" } }, "required": ["state"] }, diff --git a/core/schemas/runs-index.schema.json b/core/schemas/runs-index.schema.json index 6b6f3b0..09c7122 100644 --- a/core/schemas/runs-index.schema.json +++ b/core/schemas/runs-index.schema.json @@ -2,7 +2,7 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://eightarms.net/octopus-workflow/schemas/runs-index.schema.json", "title": "Tier 1 Run Artifact Index", - "description": "Schema for runs/{slug}/index.json 鈥?the artifact manifest for a Tier 1 local structured run. This is the compact-recovery entry point after compaction (compact never deletes local files). Parallel to, not a replacement for, the Tier 2 '## 宸ヤ欢绱㈠紩' issue comment. Authoritative layout: templates/runs-layout.md. Boundary rule: rules/two-tier-artifacts.md.", + "description": "Schema for runs/{slug}/index.json — the artifact manifest for a Tier 1 local structured run. This is the compact-recovery entry point after compaction (compact never deletes local files). Parallel to, not a replacement for, the Tier 2 '## 工件索引' issue comment. Authoritative layout: templates/runs-layout.md. Boundary rule: rules/two-tier-artifacts.md.", "type": "object", "additionalProperties": false, "required": ["schema_version", "slug", "artifacts"], @@ -24,7 +24,7 @@ "artifacts": { "type": "array", "items": { "$ref": "#/$defs/artifact" }, - "description": "One entry per Tier 1 artifact in this run. Tier 2 artifacts (Gitea issue comments, wiki pages, commit status, PRs) are NEVER listed here 鈥?this index is Tier 1 only." + "description": "One entry per Tier 1 artifact in this run. Tier 2 artifacts (Gitea issue comments, wiki pages, commit status, PRs) are NEVER listed here — this index is Tier 1 only." } }, "$defs": { @@ -55,7 +55,7 @@ "browser-evidence", "other" ], - "description": "Artifact category. 'meta'/'index' for the manifest files themselves; stage categories for produced artifacts. 'review-synthesis' marks a synthesis result artifact (round verdict) as distinct from 'synthesis-prompt' (the Phase B task prompt) and 'review-status' (the lifecycle file). 'precondition-gate' marks the review-code mechanical-green gate log. 'working-notes' is the cross-stage narrative memory file ([org-internal #2600]) 鈥?one file per run, appended by every role at close, registered once (stage/produced_by null: multi-stage, multi-role). 'browser-evidence' marks a browser evidence pack manifest ([org-internal #4497] N-03, browser/{session-id}/manifest.json 鈥?shared/pack-manifest-v1)." + "description": "Artifact category. 'meta'/'index' for the manifest files themselves; stage categories for produced artifacts. 'review-synthesis' marks a synthesis result artifact (round verdict) as distinct from 'synthesis-prompt' (the Phase B task prompt) and 'review-status' (the lifecycle file). 'precondition-gate' marks the review-code mechanical-green gate log. 'working-notes' is the cross-stage narrative memory file ([org-internal #2600]) — one file per run, appended by every role at close, registered once (stage/produced_by null: multi-stage, multi-role). 'browser-evidence' marks a browser evidence pack manifest ([org-internal #4497] N-03, browser/{session-id}/manifest.json — shared/pack-manifest-v1)." }, "schema": { "type": ["string", "null"], diff --git a/core/schemas/runs-meta.schema.json b/core/schemas/runs-meta.schema.json index 59b28c2..1730560 100644 --- a/core/schemas/runs-meta.schema.json +++ b/core/schemas/runs-meta.schema.json @@ -2,7 +2,7 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://eightarms.net/octopus-workflow/schemas/runs-meta.schema.json", "title": "Tier 1 Run Metadata", - "description": "Schema for runs/{slug}/meta.json 鈥?run metadata for a Tier 1 local structured run. Authoritative layout: templates/runs-layout.md. Boundary rule: rules/two-tier-artifacts.md. State transitions active -> archived at close (archive-at-close).", + "description": "Schema for runs/{slug}/meta.json — run metadata for a Tier 1 local structured run. Authoritative layout: templates/runs-layout.md. Boundary rule: rules/two-tier-artifacts.md. State transitions active -> archived at close (archive-at-close).", "type": "object", "additionalProperties": false, "required": [ @@ -89,12 +89,12 @@ "created_at": { "type": "string", "format": "date-time", - "description": "RFC 3339 timestamp 鈥?run creation." + "description": "RFC 3339 timestamp — run creation." }, "updated_at": { "type": "string", "format": "date-time", - "description": "RFC 3339 timestamp 鈥?last metadata mutation." + "description": "RFC 3339 timestamp — last metadata mutation." }, "closed_at": { "type": ["string", "null"], diff --git a/core/schemas/workflow-routing.schema.json b/core/schemas/workflow-routing.schema.json index c7add7b..a821445 100644 --- a/core/schemas/workflow-routing.schema.json +++ b/core/schemas/workflow-routing.schema.json @@ -2,7 +2,7 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://eightarms.net/octopus-workflow/schemas/workflow-routing.schema.json", "title": "Workflow Routing Table", - "description": "Schema for workflow-routing.yaml 鈥?type-driven (Kind/*) process selection. Referenced by rules/workflow-routing.md and the auto-approve rule. When this schema and the rules markdown disagree, the YAML (validated by this schema) is authoritative.", + "description": "Schema for workflow-routing.yaml — type-driven (Kind/*) process selection. Referenced by rules/workflow-routing.md and the auto-approve rule. When this schema and the rules markdown disagree, the YAML (validated by this schema) is authoritative.", "type": "object", "additionalProperties": false, "required": ["version", "default", "routes"], @@ -23,7 +23,7 @@ "$defs": { "stage": { "type": "string", - "description": "A pipeline stage identifier 鈥?the 5 live gates (post-[org-internal #3072] phase 3, 2026-08-21): review-dag, review-code, verify, audit-process, merge-pr. Production-phase names (requirements, design, roadmap, implement) and the 7 retired stage ids (roadmap-exit, review-roadmap, requirements-exit, design-exit, review-design-space, review-iteration-plan, plan-iterations) are no longer valid anywhere 鈥?the sizing block that consumed the phase names was retired the same ruling.", + "description": "A pipeline stage identifier — the 5 live gates (post-[org-internal #3072] phase 3, 2026-08-21): review-dag, review-code, verify, audit-process, merge-pr. Production-phase names (requirements, design, roadmap, implement) and the 7 retired stage ids (roadmap-exit, review-roadmap, requirements-exit, design-exit, review-design-space, review-iteration-plan, plan-iterations) are no longer valid anywhere — the sizing block that consumed the phase names was retired the same ruling.", "enum": ["review-dag", "review-code", "verify", "audit-process", "merge-pr"] }, "entry": { @@ -56,20 +56,20 @@ "preflight": { "type": "array", "items": { "$ref": "#/$defs/preflightItem" }, - "description": "Evidence-based producer pre-flight checklist ([org-internal #2599]) 鈥?landed by a human from retrospective PRE-FLIGHT PROPOSAL rows. Absent/empty = no injection. Cap is governed by preflight.max_items (policy), not the schema." + "description": "Evidence-based producer pre-flight checklist ([org-internal #2599]) — landed by a human from retrospective PRE-FLIGHT PROPOSAL rows. Absent/empty = no injection. Cap is governed by preflight.max_items (policy), not the schema." }, "notes": { "type": "string" }, "effort": { "type": "string", "enum": ["low", "high", "max"], - "description": "Reasoning-effort hint for this route (llm-latency phase 2): maps onto the active model's variant table when it exposes one (GLM-5.2+ low/high/max); fail-open otherwise. Reserve `low` for genuinely lightweight routes (direct-edit, no pipeline) 鈥?reasoning dominates GLM-5.3 generation latency." + "description": "Reasoning-effort hint for this route (llm-latency phase 2): maps onto the active model's variant table when it exposes one (GLM-5.2+ low/high/max); fail-open otherwise. Reserve `low` for genuinely lightweight routes (direct-edit, no pipeline) — reasoning dominates GLM-5.3 generation latency." } } }, "preflightPolicy": { "type": "object", "additionalProperties": false, - "description": "Thresholds for the retro 鈫?routes.{Kind}.preflight evidence loop ([org-internal #2599]). Authoritative values live here; the retrospective consults this block, it does not restate the numbers.", + "description": "Thresholds for the retro → routes.{Kind}.preflight evidence loop ([org-internal #2599]). Authoritative values live here; the retrospective consults this block, it does not restate the numbers.", "properties": { "max_items": { "type": "integer", "minimum": 1 }, "min_sample": { "type": "integer", "minimum": 0 }, @@ -86,7 +86,7 @@ "item": { "type": "string", "description": "One checklist line the Producer verifies before implementing." }, "evidence": { "type": "string", - "description": "Retro reference justifying the item, e.g. 'retro [org-internal #2599]: COR 脳 unhandled-null, 5/8 first-round FAILs on Kind/Bug'." + "description": "Retro reference justifying the item, e.g. 'retro [org-internal #2599]: COR × unhandled-null, 5/8 first-round FAILs on Kind/Bug'." }, "added_cycle": { "type": "string", @@ -97,7 +97,7 @@ "filing": { "type": "object", "additionalProperties": false, - "description": "Filing policy (绔嬫/鐧昏绛栫暐). sprint-mode ([org-internal #3007]): when true, verify Phase 5.5 promotes registry rows to tickets in sprint mode only. work-ticket-wip-cap ([org-internal #3005] G3): open non-TD work tickets beyond this count require scheduling evidence, else they stay registry rows.", + "description": "Filing policy (立案/登记策略). sprint-mode ([org-internal #3007]): when true, verify Phase 5.5 promotes registry rows to tickets in sprint mode only. work-ticket-wip-cap ([org-internal #3005] G3): open non-TD work tickets beyond this count require scheduling evidence, else they stay registry rows.", "properties": { "sprint-mode": { "type": "boolean", @@ -113,7 +113,7 @@ "review": { "type": "object", "additionalProperties": false, - "description": "Review round budget (process-diet P0b, [org-internal #3567]/TD-564): a tail cut, not the norm 鈥?at budget without convergence, STOP; residual findings flow to the TD registry. High risk = a BLOCKER survived a round or the diff touches a frozen cross-session contract.", + "description": "Review round budget (process-diet P0b, [org-internal #3567]/TD-564): a tail cut, not the norm — at budget without convergence, STOP; residual findings flow to the TD registry. High risk = a BLOCKER survived a round or the diff touches a frozen cross-session contract.", "required": ["max_rounds"], "properties": { "max_rounds": { @@ -152,7 +152,7 @@ "preflight": { "type": "array", "items": { "$ref": "#/$defs/preflightItem" }, - "description": "Evidence-based pre-flight checklist for the DAG route ([org-internal #2905] 鏂规 3) 鈥?same item shape as routes.{Kind}.preflight ([org-internal #2599]). Landed by a human from retrospective PRE-FLIGHT PROPOSAL rows, including the 'REQ 脳 late-discovery' root cause (requirements found only after the DAG froze: DAG oversize signals, demo-period feedback). Consumed by analyze-dag (the route's entry skill) before decomposition. Absent/empty = no injection. Optional 鈥?deliberately NOT in dagRoute.required (spec-02 搂2.8 frozen required array unchanged). Cap governed by preflight.max_items (policy), not the schema." + "description": "Evidence-based pre-flight checklist for the DAG route ([org-internal #2905] 方案 3) — same item shape as routes.{Kind}.preflight ([org-internal #2599]). Landed by a human from retrospective PRE-FLIGHT PROPOSAL rows, including the 'REQ × late-discovery' root cause (requirements found only after the DAG froze: DAG oversize signals, demo-period feedback). Consumed by analyze-dag (the route's entry skill) before decomposition. Absent/empty = no injection. Optional — deliberately NOT in dagRoute.required (spec-02 §2.8 frozen required array unchanged). Cap governed by preflight.max_items (policy), not the schema." } } }, @@ -183,7 +183,7 @@ "replaces": { "type": "array", "items": { "type": "string" }, - "description": "Historical documentation of which legacy gates the single gate replaced ([org-internal #3072] phase 3 retired them) 鈥?free-form strings, not the live stage vocabulary." + "description": "Historical documentation of which legacy gates the single gate replaced ([org-internal #3072] phase 3 retired them) — free-form strings, not the live stage vocabulary." }, "depth_from": { "type": "string", "enum": ["size_derivation"] }, "never_trim": { "type": "boolean" } @@ -201,7 +201,7 @@ "items": { "type": "string", "enum": ["Kind/Epic", "Kind/Feature"] }, "uniqueItems": true, "minItems": 1, - "description": "Ticket kinds whose DAG children reference them as ## 鐖剁骇 / Parent ([org-internal #3061] Phase 2 / TD-388: Kind/Feature added alongside Kind/Epic)." + "description": "Ticket kinds whose DAG children reference them as ## 父级 / Parent ([org-internal #3061] Phase 2 / TD-388: Kind/Feature added alongside Kind/Epic)." }, "parent_aggregator": { "type": "boolean" } } diff --git a/core/skills/gitea-rest/SKILL.md b/core/skills/gitea-rest/SKILL.md index 8c9b503..248cbed 100644 --- a/core/skills/gitea-rest/SKILL.md +++ b/core/skills/gitea-rest/SKILL.md @@ -1,6 +1,6 @@ --- name: gitea-rest -description: Use ONLY when an agent must call the Gitea REST API over HTTP (curl / fetch / HttpClient) 鈥?not for other gitea tooling (the retired gitea-mcp wrapper / gitea_* MCP tools). Covers auth schemes + token scopes, pagination, error format, this fork's specifics, copy-paste recipes for the daily loop (issues, PRs, comments, labels, releases, raw files), and the on-demand recipe for extracting any endpoint's definition from the auto-generated swagger spec (`/swagger.v1.json`) instead of keeping an endpoint catalog in context. This is the only supported Gitea API path 鈥?the gitea-mcp wrapper is retired. +description: Use ONLY when an agent must call the Gitea REST API over HTTP (curl / fetch / HttpClient) — not for other gitea tooling (the retired gitea-mcp wrapper / gitea_* MCP tools). Covers auth schemes + token scopes, pagination, error format, this fork's specifics, copy-paste recipes for the daily loop (issues, PRs, comments, labels, releases, raw files), and the on-demand recipe for extracting any endpoint's definition from the auto-generated swagger spec (`/swagger.v1.json`) instead of keeping an endpoint catalog in context. This is the only supported Gitea API path — the gitea-mcp wrapper is retired. triggers: # Direct Gitea HTTP/REST. Composite "gitea rest"/"gitea api" to avoid # colliding with other gitea tooling. @@ -11,20 +11,20 @@ triggers: - /api/v1 - gitea swagger - swagger.v1.json - - 璋?gitea api - - gitea 鎺ュ彛 + - 调gitea api + - gitea 接口 - gitea rest api role: Producer --- -> Core 涓珛鐗堬紙Increment 6a 鏀瑰啓锛夈€傛湰鏂囦欢鍚屾椂浣滀负 gitea adapter 鐨?**reference implementation** 绀轰緥锛坒rontmatter 鍘熸牱锛夛紱瀹炰緥鏈瀵圭収 `core/adapters/TERMINOLOGY.md`銆? -# Gitea REST 鈥?conventions + on-demand endpoint lookup +> Core 中立版(Increment 6a 改写)。本文件同时作为 gitea adapter 的 **reference implementation** 示例(frontmatter 原样);实例术语对照 `core/adapters/TERMINOLOGY.md`。 +# Gitea REST — conventions + on-demand endpoint lookup Gitea ships a JSON REST API under **`/api/v1`**. The instance also publishes an -**auto-generated OpenAPI spec at `/swagger.v1.json`** (~889KB, 316 endpoints) 鈥? +**auto-generated OpenAPI spec at `/swagger.v1.json`** (~889KB, 316 endpoints) — never load it whole into context; extract single endpoint definitions with jq (see "Endpoint lookup"). The spec is generated from this fork's code, so it is -always current 鈥?no manually maintained catalog can drift. +always current — no manually maintained catalog can drift. ## Quick start @@ -37,7 +37,7 @@ curl -fsS -H "$AUTH" $BASE/api/v1/version # => {"version":"1.22.0"} Inside a workspace container: `WORKSPACE_GIT_TOKEN` already carries the provisioned token, and the instance host resolves to the VPC-internal address -(no EIP bandwidth cap) 鈥?use it as `TOKEN` directly. +(no EIP bandwidth cap) — use it as `TOKEN` directly. ## Authentication @@ -47,7 +47,7 @@ provisioned token, and the instance host resolves to the VPC-internal address | OAuth2 JWT | `Authorization: bearer ` | OAuth2-app token. | | Basic | `Authorization: Basic ` | Server must enable it; 2FA needs `X-Gitea-OTP`. | -- Query-param token (`?token=`) is deprecated 鈥?avoid. +- Query-param token (`?token=`) is deprecated — avoid. - `Sudo: ` header acts as another user (admin only). - Keep tokens out of git and out of process argv where possible: read from a file/env into a shell var at call time, not in a committed script. @@ -55,21 +55,21 @@ provisioned token, and the instance host resolves to the VPC-internal address ## Token scopes (write implies read) Categories: `issue`, `repository`, `organization`, `user`, `notification`, -`package`, `admin`, `misc`, `activitypub` 鈥?each as `read:X` / `write:X`. -HTTP method sets the level: **GET 鈫?read**, **POST/PUT/PATCH/DELETE 鈫?write**. +`package`, `admin`, `misc`, `activitypub` — each as `read:X` / `write:X`. +HTTP method sets the level: **GET → read**, **POST/PUT/PATCH/DELETE → write**. A scope miss returns `403` with `token does not have at least one of required -scope(s)...` 鈥?re-issue the token rather than widening other permissions. +scope(s)...` — re-issue the token rather than widening other permissions. The full agent loop (push commits, create/merge PRs, comment) needs at minimum **`write:repository` + `write:issue`**. Workspace container tokens are -provisioned `read:repository` by default 鈥?write operations fail with 403 +provisioned `read:repository` by default — write operations fail with 403 until the operator widens the scope. ## Pagination - `page` (1-based, default 1), `limit` (default 30, **hard cap 50**). - `X-Total-Count` response header = total results. -- `Link: ; rel="next"` 鈥?walk `next` links until absent. +- `Link: ; rel="next"` — walk `next` links until absent. - Some endpoints (commits, pulls) also set `X-Page` / `X-PerPage` / `X-PageCount` / `X-HasMore`. @@ -78,12 +78,12 @@ until the operator widens the scope. Every error is `{"message":"...","url":".../api/swagger"}` with the matching status: `400` bad arg, `403` permission/scope, `404` not found or wrong BASE, `409` already exists, `413` too large, `422` malformed JSON / missing field. -There is **no rate limiting** on `/api/v1` 鈥?don't expect `X-RateLimit-*`. +There is **no rate limiting** on `/api/v1` — don't expect `X-RateLimit-*`. ## Endpoint lookup (swagger, on demand) For any endpoint not covered by the recipes below, pull the spec once per -session and jq out just the definition you need (50鈥?00 tokens each): +session and jq out just the definition you need (50–200 tokens each): ```sh curl -s $BASE/swagger.v1.json -o /tmp/gitea-sw.json @@ -109,7 +109,7 @@ curl -fsS -X POST "$BASE/api/v1/repos/$OWNER/$REPO/issues" -H "$AUTH" -H 'Conten -d '{"title":"Bug: X fails","body":"steps...","labels":[12]}' ``` -**Comment on an issue / PR** (same path for both 鈥?PRs are issues by index) +**Comment on an issue / PR** (same path for both — PRs are issues by index) ```sh curl -fsS -X POST "$BASE/api/v1/repos/$OWNER/$REPO/issues/42/comments" -H "$AUTH" \ @@ -130,48 +130,48 @@ curl -fsS -X POST "$BASE/api/v1/repos/$OWNER/$REPO/pulls/7/merge" -H "$AUTH" -H -d '{"Do":"merge","MergeTitleField":"feat: add X","MergeMessageField":"closes #7"}' ``` -**Merge returned 405? Read the response body 鈥?two distinct causes:** +**Merge returned 405? Read the response body — two distinct causes:** -- `{"message":"head branch is behind the base branch"}` 鈥?the keep-mergeable +- `{"message":"head branch is behind the base branch"}` — the keep-mergeable bot advanced main after your push, or Gitea's mergeable cache is stale. Fix: `git fetch origin main && git merge origin/main && git push` the head - branch, wait ~2s, retry the merge. (Recurring on this server 鈥?seen 3 PRs + branch, wait ~2s, retry the merge. (Recurring on this server — seen 3 PRs in a row on 2026-08-23.) -- `{"message":"The PR is already merged"}` 鈥?**treat as success**. The merge +- `{"message":"The PR is already merged"}` — **treat as success**. The merge landed during a previous "failed" attempt (racy async recompute). Confirm with `GET .../pulls/7 | jq '.merged, .merge_commit_sha'` before assuming failure. Never re-push/re-create the PR on this signal. -**Auth hygiene** 鈥?always pass the token via the `Authorization` header +**Auth hygiene** — always pass the token via the `Authorization` header (`-H "Authorization: token $TOKEN"`), never embedded in a remote URL (`https://user:token@host/...`): it persists into `.git/config` and shell history. If a token leaks into a remote URL, rewrite the remote (`git remote set-url`) and rotate. -**List PRs / changed files** 鈥?`GET .../pulls?state=open`, `GET .../pulls/7/files` +**List PRs / changed files** — `GET .../pulls?state=open`, `GET .../pulls/7/files` -**Search issues across repos** 鈥?`GET $BASE/api/v1/repos/issues/search?q=&type=pulls` +**Search issues across repos** — `GET $BASE/api/v1/repos/issues/search?q=&type=pulls` -**List labels** 鈥?`GET .../labels?limit=50 | jq '.[] | {name,color}'` (label create/update via POST/PATCH on the same path) +**List labels** — `GET .../labels?limit=50 | jq '.[] | {name,color}'` (label create/update via POST/PATCH on the same path) -**Raw file contents** 鈥?`GET .../raw/README.md` (plain text; the `contents/{path}` variant returns base64 in `.content`) +**Raw file contents** — `GET .../raw/README.md` (plain text; the `contents/{path}` variant returns base64 in `.content`) -**Create a release** 鈥?`POST .../releases {"tag_name":"v1.2.0","target":"main",...}` +**Create a release** — `POST .../releases {"tag_name":"v1.2.0","target":"main",...}` -**Create a wiki page** 鈥?`POST .../wiki/new {"title":"My Page","content_base64":"","message":"add page"}`. **`content_base64` is the ONLY field that writes body text** 鈥?this fork **silently ignores** the upstream-style `content` field on wiki create/update (no error, `201` returned, page saved 0 bytes; evidence [org-internal #3944]: commits f5eaf18/4f0217d/bad5880). `content` is valid only on the file API, never on wiki. +**Create a wiki page** — `POST .../wiki/new {"title":"My Page","content_base64":"","message":"add page"}`. **`content_base64` is the ONLY field that writes body text** — this fork **silently ignores** the upstream-style `content` field on wiki create/update (no error, `201` returned, page saved 0 bytes; evidence [org-internal #3944]: commits f5eaf18/4f0217d/bad5880). `content` is valid only on the file API, never on wiki. -**Read / edit / delete a wiki page** 鈥?`GET|PATCH|DELETE .../wiki/page/{pageName}`. **Read by the mangled name, not the logical title**: this fork rewrites stored filenames for titles containing `/`, spaces, or `:` (slash percent-encoded into the filename + a `.-` suffix 鈥?see [org-internal #3218]), so `GET /wiki/page/bugfix-3204/bugfix-report` 404s. Always `GET .../wiki/pages` first and use the returned `sub_url` **verbatim** (it already carries the `%2F` encoding and `.-` suffix). PATCH/DELETE take the same mangled `pageName`. For PATCH, **omit `title` to keep the page name** and send only `content_base64`+`message` (fixed in gitea `dev-421-g7ff56aec08`, [org-internal #3510] 鈥?before that a title-less PATCH silently renamed the page to `unnamed.md` and later edits deleted target pages). Same trap as create: a PATCH carrying `content` instead of `content_base64` returns `200` with a **0-byte page** ([org-internal #3944]) 鈥?always base64-encode the body and verify non-empty via a follow-up `GET`. +**Read / edit / delete a wiki page** — `GET|PATCH|DELETE .../wiki/page/{pageName}`. **Read by the mangled name, not the logical title**: this fork rewrites stored filenames for titles containing `/`, spaces, or `:` (slash percent-encoded into the filename + a `.-` suffix — see [org-internal #3218]), so `GET /wiki/page/bugfix-3204/bugfix-report` 404s. Always `GET .../wiki/pages` first and use the returned `sub_url` **verbatim** (it already carries the `%2F` encoding and `.-` suffix). PATCH/DELETE take the same mangled `pageName`. For PATCH, **omit `title` to keep the page name** and send only `content_base64`+`message` (fixed in gitea `dev-421-g7ff56aec08`, [org-internal #3510] — before that a title-less PATCH silently renamed the page to `unnamed.md` and later edits deleted target pages). Same trap as create: a PATCH carrying `content` instead of `content_base64` returns `200` with a **0-byte page** ([org-internal #3944]) — always base64-encode the body and verify non-empty via a follow-up `GET`. ## This fork's specifics -- **Projects are repo-level only** 鈥?no `/orgs/.../projects` or `/users/.../projects` REST endpoints. -- **Aggregated inline review comments**: no single list-all endpoint 鈥?list +- **Projects are repo-level only** — no `/orgs/.../projects` or `/users/.../projects` REST endpoints. +- **Aggregated inline review comments**: no single list-all endpoint — list reviews, then fetch each review's `/comments`. -- Wiki REST = 6 method脳path combos on 4 paths (`new`, `page/{pageName}` +- Wiki REST = 6 method×path combos on 4 paths (`new`, `page/{pageName}` GET/PATCH/DELETE, `pages`, `revisions/{pageName}`). This fork has **no** upstream `raw/{pageName}` or `pages/{pageName}` single-page endpoints, and `page/{pageName}` only matches the mangled filename (see the wiki recipe - above and [org-internal #3218]) 鈥?clone the wiki git repo (default branch `main`) only if + above and [org-internal #3218]) — clone the wiki git repo (default branch `main`) only if you need history beyond `/wiki/revisions` or bulk filename surgery. - The authoritative route table is `routers/api/v1/api.go` in the Gitea source; the human-readable docs UI is `/api/swagger`. @@ -180,4 +180,4 @@ history. If a token leaks into a remote URL, rewrite the remote Prefer `HttpClient` from `@effect/platform` with the same headers (see `rules/effect-rules` on the wiki). Outside Effect, `curl` / `Bun.fetch` are -fine 鈥?the wire format is identical. +fine — the wire format is identical. diff --git a/core/skills/headless-session-ops/SKILL.md b/core/skills/headless-session-ops/SKILL.md index 2f70a83..d978b60 100644 --- a/core/skills/headless-session-ops/SKILL.md +++ b/core/skills/headless-session-ops/SKILL.md @@ -1,94 +1,94 @@ --- name: headless-session-ops description: > - Use ONLY when an agent must autonomously launch a headless main session 鈥? + Use ONLY when an agent must autonomously launch a headless main session — create a session and drive agent generation over the HTTP `prompt_async` - endpoint with NO persistent client attached (Web UI / TUI absent) 鈥?OR + endpoint with NO persistent client attached (Web UI / TUI absent) — OR lifecycle-manage such a session afterwards: health-check polling, hung-stream diagnosis (state=generating + frozen message count), abort + re-wake rescue, - provider quota-wall recovery (worker killed mid-task by a usage limit 鈥? + provider quota-wall recovery (worker killed mid-task by a usage limit — detect, re-wake on a known-good provider, inject facts), and fact-baseline injection to correct a stale worldview in a woken worker. This is the path used by in-session agents spawning sibling sessions, `octopus run --attach` non-interactive mode, and cron / CI drivers. Covers - the 3-step flow (create session 鈫?POST prompt_async WITH a known-good - `model` 鈫?poll `/session/:id/message` to verify generation), how to REUSE + the 3-step flow (create session → POST prompt_async WITH a known-good + `model` → poll `/session/:id/message` to verify generation), how to REUSE the current session's model for the new session, the model-field requirement, the directory default (current session's directory unless targeting another repo), and the ticket-driven recipe (launch a session seeded from issue #N). Do NOT use when a persistent client (Web UI / TUI) - is driving the session 鈥?those use the synchronous `prompt` endpoint and + is driving the session — those use the synchronous `prompt` endpoint and always send `model`. triggers: - # English 鈥?the brand phrases for headless launch. + # English — the brand phrases for headless launch. - headless session - launch session - prompt_async - prompt async - autonomous session - # English 鈥?the failure mode (agent sees a stalled session and needs to know why). + # English — the failure mode (agent sees a stalled session and needs to know why). - session not generating - session generation stuck - # English 鈥?ticket-driven launch. + # English — ticket-driven launch. - launch session for issue - launch session for ticket - spawn session for ticket - # English 鈥?patrol mode (delayed self-wake polling loops, [org-internal #3937]). + # English — patrol mode (delayed self-wake polling loops, [org-internal #3937]). - patrol loop - patrol mode - delayed wake - # Chinese 鈥?bare noun phrases (matches() is a contiguous substring). - - 鎷夎捣涓讳細璇? - - 鎷夎捣浼氳瘽 - - 鑷不浼氳瘽 - - 鏃犲ご浼氳瘽 - # Chinese 鈥?patrol mode ([org-internal #3937]). - - 宸℃妯″紡 - - 寤惰繜鍞ら啋 - - 浼氳瘽涓嶇敓鎴? - - 浼氳瘽鍗℃ - - 涓哄伐鍗曟媺璧蜂細璇? - - 宸ュ崟鎷夎捣浼氳瘽 - # English 鈥?lifecycle (hung rescue, stale-worldview correction, [org-internal #2459]). + # Chinese — bare noun phrases (matches() is a contiguous substring). + - 拉起主会话 + - 拉起会话 + - 自治会话 + - 无头会话 + # Chinese — patrol mode ([org-internal #3937]). + - 巡检模式 + - 延迟唤醒 + - 会话不生成 + - 会话卡死 + - 为工单拉起会话 + - 工单拉起会话 + # English — lifecycle (hung rescue, stale-worldview correction, [org-internal #2459]). - abort session - revive session - session rescue - fact baseline - # English 鈥?quota-wall recovery ([org-internal #3669]): worker killed by provider usage limit. + # English — quota-wall recovery ([org-internal #3669]): worker killed by provider usage limit. - quota wall - usage limit reached - quota exhausted - switch provider - # Chinese 鈥?lifecycle ops ([org-internal #2459]). - - 鍞ら啋浼氳瘽 - - 浼氳瘽鎸傛 - - 浜嬪疄鍩虹嚎 - # Chinese 鈥?quota-wall recovery ([org-internal #3669]). - - 閰嶉澧? - - 閰嶉鑰楀敖 - - 鐢ㄩ噺涓婇檺 - - 鎹緵搴斿晢 + # Chinese — lifecycle ops ([org-internal #2459]). + - 唤醒会话 + - 会话挂死 + - 事实基线 + # Chinese — quota-wall recovery ([org-internal #3669]). + - 配额墙 + - 配额耗尽 + - 用量上限 + - 换供应商 role: Producer --- -> Core 涓珛鐗堬紙Increment 6a 鏀瑰啓锛屽師 deferHard verbatimDir锛夈€傛満鍒躲€佺粨鏋勪笌 frontmatter 淇濇寔锛涘疄渚嬫湳璇紙宸ュ叿鍚嶃€佽矾寰勩€佸伐鍗曞彿锛夋寜 `core/adapters/TERMINOLOGY.md` 缁戝畾鍒板叿浣撳疄渚嬨€? +> Core 中立版(Increment 6a 改写,原 deferHard verbatimDir)。机制、结构与 frontmatter 保持;实例术语(工具名、路径、工单号)按 `core/adapters/TERMINOLOGY.md` 绑定到具体实例。 # Headless Main Session Launch (prompt_async) Launch a **main session for a specific issue / work item from a headless -context** 鈥?no Web UI, no TUI, just HTTP. This is the "fire-and-forget" path: an +context** — no Web UI, no TUI, just HTTP. This is the "fire-and-forget" path: an agent inside another session, a cron job, a CI script, or `octopus run --attach` non-interactive mode all land here when they need to spin up a session that actually generates. -## Directory convention 鈥?independent session in the current workspace, no worktree +## Directory convention — independent session in the current workspace, no worktree The default is an **independent session sharing the current session's -directory** 鈥?the repo you are already working in. No new worktree, no +directory** — the repo you are already working in. No new worktree, no checkout, no extra `bun install` or `.codegraph/` rebuild. Session creation and worktree discipline are **orthogonal**: this skill only opens a session record pointed at a directory; whether the launched session later creates its own worktree is a _workflow_ decision (per the worktree-discipline rule) it -makes when it starts actual work 鈥?not something the launcher does. One +makes when it starts actual work — not something the launcher does. One exception: the **ticket-driven recipe** below, where claim-first ([org-internal #2297]) requires the launcher to push the workflow branch before launch, making a pre-built worktree the claim carrier + ready workspace (see Pitfall #10 @@ -100,7 +100,7 @@ carve-out). So, unless a different repo / path is specifically targeted, from your own session record via `GET /session/$LAUNCHER_SID`). The new session runs independently in that same workspace. Pass an explicit different `?directory=` only when the new session must work in another - **repo** 鈥?never a worktree of the same repo. The Web UI sidebar groups + **repo** — never a worktree of the same repo. The Web UI sidebar groups sessions by directory (`sidebar-project.tsx` per-workspace `workspaceSessions(directory)`, fetched via `session.list({ directory })`), so a session pointed at a worktree disappears from the workspace the creator @@ -109,23 +109,23 @@ carve-out). So, unless a different repo / path is specifically targeted, - **No current session** (cron / CI / bare script): the server-side fallback when `?directory=` is omitted is the **server process's `process.cwd()`** (`server/routes/instance/httpapi/middleware/workspace-routing.ts:76`), which - is wherever the server was started 鈥?unreliable. In that context `?directory=` + is wherever the server was started — unreliable. In that context `?directory=` remains effectively required. -- **Dispatch convention 鈥?directory = code location, not tracker repo.** When +- **Dispatch convention — directory = code location, not tracker repo.** When launching a session for a ticket, set `?directory=` to the **main workspace** checkout that holds the **code the ticket edits**, which may differ from the repo where the ticket is tracked. Example: `/#` is filed in - the ticket-tracker repo but edits the main repo's workflow files 鈫?the + the ticket-tracker repo but edits the main repo's workflow files → the session's `?directory=` is the **code checkout root** (`//`), not the tracker repo checkout. Never point - `?directory=` at a ticket worktree 鈥?the worktree goes in the claim comment + + `?directory=` at a ticket worktree — the worktree goes in the claim comment + seed prompt, not the directory field (sidebar visibility, see bullet 1). The owner-check (`GET /session?directory=$DIR`) is scoped per directory, so rooting the session at the correct code checkout is also what makes the duplicate-session guard meaningful. Server-side backstop ([org-internal #3190]): creating a session whose directory IS a linked git worktree answers with an `X-Session-Directory-Warning` response header and a server warn log naming - the owning main repo 鈥?if you see that header, the session will be invisible + the owning main repo — if you see that header, the session will be invisible to `GET /session?directory=` owner-checks; re-check your launch parameters before proceeding. @@ -135,10 +135,10 @@ carve-out). So, unless a different repo / path is specifically targeted, > corrected the misdiagnosed [org-internal #1691], see comment 9205). Code citations below were > verified against `` at HEAD. -## The `model` field 鈥?always provide a known-good model (read this first) +## The `model` field — always provide a known-good model (read this first) `prompt_async` is **fire-and-forget**: after HTTP returns 204 (or 202 when the -message is queued behind a wedge/zombie session 鈥?no live runner in that +message is queued behind a wedge/zombie session — no live runner in that process), the server runs the full agent loop in a separate fiber. **Always include an explicit `model` in the payload, and reuse the current session's model for it** (see the next @@ -154,7 +154,7 @@ section). Two reasons: // ^^^^^^^^^ agent ^^^^^^^^^^^^^^^^^ session/default ``` - So omission does **not** produce a clean validation error 鈥?it silently + So omission does **not** produce a clean validation error — it silently resolves to the agent's model, the session's last-used model, or the provider default (`currentModel`, `prompt.ts:824-838`). @@ -164,15 +164,15 @@ section). Two reasons: (`prompt.ts:1399`, `:804-822`). In the **async** handler that error is caught by the delivery fork's failure handling (`forkPromptDelivery` in `handlers/session.ts`) and never reaches the HTTP - caller 鈥?so the session looks healthy (HTTP 204, user message persisted) yet + caller — so the session looks healthy (HTTP 204, user message persisted) yet **never generates**. This is exactly the "silent stall" observed empirically in [org-internal #1695]: a session with a 1-message transcript (user only) and frozen `time.updated`. | payload | result | | -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | -| `{agent, parts}` 鈥?no `model` | 204/202; model filled by fallback; if the resolved model is unusable 鈫?**apparent silent stall** (no error surfaced, no generation). | -| `{agent, model:{providerID,modelID}, parts}` | 204/202; deterministic generation within seconds (verified: "1+1 绛変簬 2銆? in ~6s). | +| `{agent, parts}` — no `model` | 204/202; model filled by fallback; if the resolved model is unusable → **apparent silent stall** (no error surfaced, no generation). | +| `{agent, model:{providerID,modelID}, parts}` | 204/202; deterministic generation within seconds (verified: "1+1 等于 2。 in ~6s). | > **Iron Law:** every `prompt_async` payload MUST carry a **known-good** `model`, > and the safest known-good model is **the one driving this session right now** @@ -181,14 +181,14 @@ section). Two reasons: **Field-name gotcha when copying a model:** the prompt payload's `ModelRef` uses `{ providerID, modelID }` (`prompt.ts:1852-1855`), but the **session record's** -model uses `{ id, providerID, variant }` (`session.ts:256-260`) 鈥?i.e. `modelID` +model uses `{ id, providerID, variant }` (`session.ts:256-260`) — i.e. `modelID` is called `id`. When you reuse a model read from a session record, remap -`model.id 鈫?modelID` (see recipe below). User-message `info.model` already uses +`model.id → modelID` (see recipe below). User-message `info.model` already uses `modelID` (`message.ts:403-407`). ## Reuse the current session's model (recommended default) -The model powering the launching session is proven to work 鈥?reuse it. Discover +The model powering the launching session is proven to work — reuse it. Discover it by whichever path fits your context: - **You are an in-session agent** (most common): read it straight from your own @@ -196,27 +196,27 @@ it by whichever path fits your context: > "You are powered by the model named **{modelID}** (provider: **{providerID}**)." - e.g. `modelID = "glm-5.2"`, `providerID = "zai-coding-plan"`锛堟浘鍚? - zhipuai-coding-plan锛?026-08 鏇村悕锛? + e.g. `modelID = "glm-5.2"`, `providerID = "zai-coding-plan"`(曾名 + zhipuai-coding-plan,2026-08 更名) - **Programmatic / no system prompt** (cron, CI, sibling caller): `GET` the launching session's record and read `.model`: ```sh curl -s "$BASE/session/$LAUNCHER_SID?directory=$DIR" \ - | jq '.model' # 鈫?{ "id": "glm-5.2", "providerID": "zai-coding-plan", "variant": ... } + | jq '.model' # → { "id": "glm-5.2", "providerID": "zai-coding-plan", "variant": ... } ``` - Then **remap** `model.id 鈫?modelID` when building the payload. + Then **remap** `model.id → modelID` when building the payload. - **Verify the provider is actually connected** before launching (cheap - insurance 鈥?the model only generates if its provider is live): + insurance — the model only generates if its provider is live): ```sh curl -s "$BASE/provider?directory=$DIR" | jq '.connected | index("")' - # 鈫?null = provider NOT connected (do NOT launch); 0..n = connected + # → null = provider NOT connected (do NOT launch); 0..n = connected ``` or call the `list-models` tool. Note: model-level availability is only - knowable at runtime 鈥?a _configured_ model can still fail (subscription-tier + knowable at runtime — a _configured_ model can still fail (subscription-tier or region errors); `GET /config/providers` lists _configured_ models, not usable ones. The safest pre-flight is "provider connected" + reusing the current session's model, which is generating by definition. There is **no @@ -226,9 +226,9 @@ it by whichever path fits your context: - A reachable octopus HTTP instance (e.g. `http://127.0.0.1:4096`). - The target directory (passed as `?directory=`). Defaults to the current - session's directory for in-session launches 鈥?see the Directory convention + session's directory for in-session launches — see the Directory convention above; only specify a different one explicitly when targeting another repo. -- A known-good `model` (reuse the current session's 鈥?see above). Model IDs +- A known-good `model` (reuse the current session's — see above). Model IDs must match the provider config in `/octopus/octopus.jsonc`; credentials live in the octopus account keyring (not env), so an isolated HOME cannot reuse them. @@ -242,27 +242,27 @@ BASE=http://127.0.0.1:4096 DIR=/data/octopus ``` -### Step 1 鈥?Create the session +### Step 1 — Create the session ```sh SID=$(curl -s -X POST "$BASE/session?directory=$DIR" \ -H 'content-type: application/json' \ - -d '{"title":"#N 鈥?","agent":"builder"}' | jq -r .id) + -d '{"title":"#N — ","agent":"builder"}' | jq -r .id) echo "session=$SID" ``` - `POST /session` returns `Session.Info` with `.id` (`groups/session.ts:207-211`). - `?directory=` points at the repo root. For in-session launches it defaults to - the **current session's directory** (Directory convention above) 鈥?keep it + the **current session's directory** (Directory convention above) — keep it that way so the new session stays visible in the creator's sidebar; pass an explicit path only when targeting a different repo (a worktree of the same - repo is NOT a directory target 鈥?hand it over via the seed prompt), and + repo is NOT a directory target — hand it over via the seed prompt), and always pass it from cron / CI where no current session exists (server falls back to its own `process.cwd()` otherwise). - `agent` is `builder` (the main agent). Sub-agents (explorer/worker) are - spawned within a session 鈥?never named here. + spawned within a session — never named here. -### Step 2 鈥?Deliver the seed prompt (CRITICAL: include a known-good `model`) +### Step 2 — Deliver the seed prompt (CRITICAL: include a known-good `model`) ```sh curl -s -X POST "$BASE/session/$SID/prompt_async?directory=$DIR" \ @@ -278,17 +278,17 @@ curl -s -X POST "$BASE/session/$SID/prompt_async?directory=$DIR" \ body** when processing starts now (idle session, live runner, noReply injection, or waiting-question preemption), **202** when the message is queued behind a session that is generating with no live runner in this - process (wedge/zombie shape 鈥?stored and re-driven on restart drain or the + process (wedge/zombie shape — stored and re-driven on restart drain or the next runner), **202** when `delay_sec` defers delivery to an in-memory - delayed wake ([org-internal #3937] A 鈥?see "宸℃妯″紡 / Patrol mode" below), and **409** + delayed wake ([org-internal #3937] A — see "巡检模式 / Patrol mode" below), and **409** when the session is paused (message NOT stored) or the maintenance-mode hold queue is full. - **Global route alternative ([org-internal #4307]):** when the client only knows the sessionID (no `?directory=` routing), `POST /prompt_async` (no path prefix) resolves the instance from the app database by the body's `sessionID` and - delegates to the same delivery core 鈥?same 204/202/409 semantics, same + delegates to the same delivery core — same 204/202/409 semantics, same `delay_sec` contract, body is the session-scoped payload plus a required - `sessionID` field. Unknown sessionID 鈫?404 naming the session-scoped + `sessionID` field. Unknown sessionID → 404 naming the session-scoped route. This is the natural fit for the completion-report POST (fixed port, no directory context). - `delay_sec` (optional, integer 0..86400, else 400) schedules the prompt for @@ -297,16 +297,16 @@ curl -s -X POST "$BASE/session/$SID/prompt_async?directory=$DIR" \ at intake. **Precedence ([org-internal #3937] review):** `delay_sec` applies only when the intake would otherwise start processing now (immediate class). For the queued-behind-wedge 202 member and under maintenance queue mode it is - stripped 鈥?retention semantics win (the server logs the drop; the delay is - NOT honored). Pending wakes are **in-memory only 鈥?a server restart abandons + stripped — retention semantics win (the server logs the drop; the delay is + NOT honored). Pending wakes are **in-memory only — a server restart abandons them**; a patrol loop must therefore tolerate a missed wake and re-arm. - Never use `sleep ; curl ...` in a bash tool call to wait 鈥?that blocks + Never use `sleep ; curl ...` in a bash tool call to wait — that blocks the turn with zero output (the [org-internal #3937] incident shape); the bash tool now warns on bare sleeps > 120s (long-sleep guard, [org-internal #3937] C). - `model` is `{ providerID, modelID }` or the shorthand string - `"providerID/modelID"` (split on the FIRST `/`; accepted since [org-internal #4307] 鈥? + `"providerID/modelID"` (split on the FIRST `/`; accepted since [org-internal #4307] — e.g. `"zai-coding-plan/glm-5.2"`). **Do not omit it.** Fill the values from - your own session's model (see "Reuse the current session's model" above) 鈥? + your own session's model (see "Reuse the current session's model" above) — never hardcode a specific model. A string without a `/` is rejected with 400 naming both accepted forms. - When the seed prompt contains newlines/quotes, build the payload from a file @@ -315,87 +315,87 @@ curl -s -X POST "$BASE/session/$SID/prompt_async?directory=$DIR" \ jq -n --rawfile p seed.txt '{agent:"builder",model:{providerID:"",modelID:""},parts:[{type:"text",text:$p}]}' ``` -### Step 3 鈥?Verify the session actually generated (do NOT assume success) +### Step 3 — Verify the session actually generated (do NOT assume success) -HTTP 204/202 鈮?generation success. Poll until an `assistant` message appears. The -endpoint is **`/session/:id/message` 鈥?SINGULAR**, and it returns a bare array +HTTP 204/202 ≠generation success. Poll until an `assistant` message appears. The +endpoint is **`/session/:id/message` — SINGULAR**, and it returns a bare array whose elements are `{ info, parts }` with `role` at `.info.role` -(`groups/session.ts:88,183-194`). Poll the **tail with `limit=1`** 鈥?the -no-`limit` form loads the ENTIRE transcript server-side (O(messages脳parts) per +(`groups/session.ts:88,183-194`). Poll the **tail with `limit=1`** — the +no-`limit` form loads the ENTIRE transcript server-side (O(messages×parts) per poll; pitfall #14's 478-message session paid it on every health check), while `limit=1` returns just the newest message (REQ-F-010 windowing, `message.ts` `page()`): ```sh -# wait 5鈥?0s, then poll the TAIL 鈥?never the full transcript +# wait 5–10s, then poll the TAIL — never the full transcript curl -s "$BASE/session/$SID/message?directory=$DIR&limit=1" \ | jq '{latest_role: .[0].info.role, latest_id: .[0].info.id}' ``` -Verdict: `latest_role == "assistant"` 鈫?the first generation completed (the +Verdict: `latest_role == "assistant"` → the first generation completed (the seed is `user`; the newest message flips to `assistant` only once the model -answered 鈥?an errored turn also persists an assistant message carrying the 鈿狅笍 +answered — an errored turn also persists an assistant message carrying the ⚠️ usage-limit part, [org-internal #2912], so check the tail text before declaring success). -Stays `user` across 2鈥? polls spaced 10鈥?0 s 鈫?**apparent silent stall**; +Stays `user` across 2–3 polls spaced 10–30 s → **apparent silent stall**; almost certainly the model did not resolve to a usable one (fallback landed badly, or you omitted `model`). Redeliver Step 2 with an explicit known-good `model`. Need everything newer than a known point? Page forward with -`after=&limit=N` and follow the `X-Next-Cursor` response header 鈥? +`after=&limit=N` and follow the `X-Next-Cursor` response header — just never omit `limit`. -> 鈿狅笍 Do NOT write `/messages` (plural) 鈥?that path does not exist for listing. +> ⚠️ Do NOT write `/messages` (plural) — that path does not exist for listing. > (`POST /session/:id/message`, same singular path but POST, is the synchronous > prompt endpoint; don't confuse the two.) ## Launch a session for a specific ticket (#N) The common case: an agent (or cron/CI) spins up a fresh main session to work a -tracked issue 鈥?seed prompt composed **from the issue body**, launched on the +tracked issue — seed prompt composed **from the issue body**, launched on the current session's model. **Step-0 owner check is MANDATORY ([org-internal #1803])**: four data sources (session-title scan, assignee/claim, open PRs, remote branches); any -live claim 鈫?ABORT the launch (`session-scope-guard.md` points here for that -pre-step). Full recipe 鈥?owner-check scan script with `X-Total-Count` / +live claim → ABORT the launch (`session-scope-guard.md` points here for that +pre-step). Full recipe — owner-check scan script with `X-Total-Count` / `X-Has-More` pagination handling ([org-internal #3190]), claim-first atomic 3-step ([org-internal #2297], + one-command provisioning via `script/claim-provision.sh`, [org-internal #3642]), seed-prompt MUSTs, backend duplicate-ticket hard guard ([org-internal #1989], on by default [org-internal #2350]), launch + poll commands: `reference/ticket-recipe.md` (read BEFORE creating any ticket-driven session). -## Completion-report protocol (瀹屾垚鍥炴姤鍗忚, [org-internal #2374]) +## Completion-report protocol (完成回报协议, [org-internal #2374]) `prompt_async` is fire-and-forget **for the launcher too**: without this protocol the orchestrator has NO push channel and must poll forever ([org-internal #2366]). The worker actively reports `status=done|blocked|handoff` back into the orchestrator's session via `prompt_async`, with an issue-comment fallback. -Default report format is `branch=`-based 鈥?workers never open PRs +Default report format is `branch=`-based — workers never open PRs (TD-678/[org-internal #4425]); `pr=#N` appears only on `uncoordinated` self-opens. Hard constraints: a headless worker MUST NOT call the `question` tool -([org-internal #2378] 鈥?directional/irreversible decisions are `status=blocked` reports, +([org-internal #2378] — directional/irreversible decisions are `status=blocked` reports, then STOP), and MUST NOT self-merge its PR / self-close the issue / touch -`main` ([org-internal #2386] 鈥?keep the PR open; the orchestrator merges and closes). Full -protocol 鈥?orchestrator-side seed block (verbatim template), worker-side +`main` ([org-internal #2386] — keep the PR open; the orchestrator merges and closes). Full +protocol — orchestrator-side seed block (verbatim template), worker-side steps, decision-authority layering: `reference/completion-report.md` (read BEFORE composing a seed prompt for any worker you need to hear back from). ## Post-launch lifecycle: health check, hung rescue, stale-worldview correction ([org-internal #2459]) -Launching is half the job 鈥?a headless worker can hang silently or wake with -a stale worldview. Health check = two polls 30鈥?0 s apart (`.state` + `progress` +Launching is half the job — a headless worker can hang silently or wake with +a stale worldview. Health check = two polls 30–60 s apart (`.state` + `progress` + message count); `state=generating` with ALL counters frozen across BOTH -samples = hung 鈫?abort FIRST (`POST /session/:id/abort`), then re-wake with a -known-good model + the anti-re-hang clause. Stale worldview 鈫?fact-baseline +samples = hung → abort FIRST (`POST /session/:id/abort`), then re-wake with a +known-good model + the anti-re-hang clause. Stale worldview → fact-baseline injection (only the CURRENT authority injects; if YOU might be the stale one, -verify identity against the durable record first). Full runbooks 鈥?verdict +verify identity against the durable record first). Full runbooks — verdict table + [org-internal #3215] dual-sample rule, abort + re-wake sequence, fact-baseline structure, authorization asymmetry: `reference/lifecycle-ops.md` (read when -managing a launched session). Quota wall 鈥?the worker's turn killed by a +managing a launched session). Quota wall — the worker's turn killed by a terminal usage-limit error (looks like silent completion: no report, ticket -stalled) 鈥?has its own runbook: detection signals (finish=error 鈿狅笍 tail, +stalled) — has its own runbook: detection signals (finish=error ⚠️ tail, provider quota markers), provider-switch re-wake, fact-baseline template, pre-dispatch provider check: `reference/quota-wall-recovery.md` ([org-internal #3669], evidence [org-internal #3627]; read when a worker goes silent mid-task). -## 宸℃妯″紡 / Patrol mode (delayed self-wake, [org-internal #3937]) +## 巡检模式 / Patrol mode (delayed self-wake, [org-internal #3937]) A patrol loop periodically nudges a headless session without holding a connection or burning a turn on a blocking sleep. Since [org-internal #3937] A the server @@ -414,82 +414,82 @@ curl -s -X POST "$BASE/session/$SID/prompt_async?directory=$DIR" \ Rules of the road: -- **202 + `delay_sec` 鈮?queued-behind-wedge 202.** Both are 202, but the +- **202 + `delay_sec` ≠queued-behind-wedge 202.** Both are 202, but the delayed wake holds the prompt in memory and delivers it exactly once when the delay elapses (session must have been idle at intake; `0` equals - omission 鈫?immediate 204 path). If the session was NOT idle at intake, the + omission → immediate 204 path). If the session was NOT idle at intake, the intake keeps the queued semantics and `delay_sec` is ignored (logged - server-side) 鈥?the same precedence as maintenance queue mode, where the + server-side) — the same precedence as maintenance queue mode, where the intake is retained and `delay_sec` never delays the flush. -- **Paused at wake 鈫?dropped, not stored.** The wake's `prompt()` rejects and +- **Paused at wake → dropped, not stored.** The wake's `prompt()` rejects and the server publishes an error event; the message is never stored. A patrol driver should check session state before assuming delivery. -- **Restart abandons pending wakes.** They are in-memory server fibers 鈥?no +- **Restart abandons pending wakes.** They are in-memory server fibers — no persistence, no drain. After any server restart the patrol driver owns re-arming the next wake; treat a missed wake as expected, not as an error. - **Latest wake wins, per session ([org-internal #4069]).** Arming `delay_sec` on a session that already has a pending wake SUPERSEDES it: the previous countdown is cancelled, only the latest wake fires. Re-arming each turn is the intended - patrol pattern 鈥?a superseded wake never delivers its prompt and publishes - no error. (Pre-[org-internal #4069] stacks both FIFO 鈥?on old binaries, re-arm only after + patrol pattern — a superseded wake never delivers its prompt and publishes + no error. (Pre-[org-internal #4069] stacks both FIFO — on old binaries, re-arm only after the previous wake fired.) - **Never emulate a delay with the bash tool.** `sleep 900; curl ...` blocks the turn with zero output and gets manually aborted as a hang (the [org-internal #3937] incident). The bash tool now prepends a `` on bare sleeps - beyond 120s ([org-internal #3937] C) 鈥?treat that warning as a redirect to this section. + beyond 120s ([org-internal #3937] C) — treat that warning as a redirect to this section. ## Do NOT use this skill when -- A **persistent client** (Web UI / interactive TUI) is attached 鈥?those use the +- A **persistent client** (Web UI / interactive TUI) is attached — those use the synchronous `prompt` endpoint (it blocks until generation completes) and send `model` automatically. Using `prompt_async` there gains nothing and loses the in-line response. This includes the rescue runbook: a hung session driven by a persistent client is the client user's to handle (refresh / re-prompt from - the UI) 鈥?abort + re-wake targets headless / managed sessions only. -- You want the response **inline** in the same HTTP call 鈥?`prompt_async` is + the UI) — abort + re-wake targets headless / managed sessions only. +- You want the response **inline** in the same HTTP call — `prompt_async` is fire-and-forget; use the synchronous `prompt` endpoint instead. ## Common pitfalls | # | Pitfall | Fix | | --- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| 1 | Omitting `model` / relying on the fallback 鈫?apparent silent stall (most common) | Always send a known-good `model:{providerID,modelID}` 鈥?reuse the current session's | -| 2 | Listing endpoint written `/messages` (plural) 鈫?404 | Use `/session/:id/message` (singular); returns a bare array, role at `.info.role` | -| 3 | Copying the session record's model verbatim into the payload | Remap field names: session `.model.id` 鈫?payload `modelID` (session uses `id`, payload uses `modelID`) | +| 1 | Omitting `model` / relying on the fallback → apparent silent stall (most common) | Always send a known-good `model:{providerID,modelID}` — reuse the current session's | +| 2 | Listing endpoint written `/messages` (plural) → 404 | Use `/session/:id/message` (singular); returns a bare array, role at `.info.role` | +| 3 | Copying the session record's model verbatim into the payload | Remap field names: session `.model.id` → payload `modelID` (session uses `id`, payload uses `modelID`) | | 4 | Stale `octopus` binary rejects newer config schema (`Unrecognized key`) | Use the source CLI `bun run ./src/index.ts ...`; the config itself is valid | -| 5 | Missing `?directory=` query param | In-session launch: default to the current session's directory. Cron/CI (no current session): pass the repo root explicitly 鈥?server falls back to its own `process.cwd()` | -| 6 | Using synchronous `prompt` for headless launch | `prompt` blocks until done 鈥?wrong for fire-and-forget; use `prompt_async` | +| 5 | Missing `?directory=` query param | In-session launch: default to the current session's directory. Cron/CI (no current session): pass the repo root explicitly — server falls back to its own `process.cwd()` | +| 6 | Using synchronous `prompt` for headless launch | `prompt` blocks until done — wrong for fire-and-forget; use `prompt_async` | | 7 | Shell-escaping errors in a multi-line seed prompt | Build the payload with `jq -n --rawfile` from a file | -| 8 | Verifying models via `GET /model` 鈥?no such endpoint (404) | Use `GET /provider` 鈫?`.connected` (provider live check); `list-models` tool also works. `GET /config/providers` lists _configured_ models only | -| 9 | Launching a session for a ticket another live session already owns 鈫?conflicting diffs, duplicate work ([org-internal #1744]/[org-internal #1753]) | Run the Step-0 owner check (`GET /session` title scan + worktree/branch/PR scan); ABORT and report on conflict | -| 10 | Creating a worktree / fresh checkout for the new session (unnecessary `bun install` + `.codegraph/` rebuild cost) | Don't 鈥?for _general_ launches: a session is a conversation pointed at a directory, so reuse the current workspace; worktree creation is a separate _workflow_ decision the launched session makes if/when it starts a workflow. **Ticket-recipe carve-out ([org-internal #2297]):** the ticket's work happens in a dedicated worktree anyway (worktree discipline), and claim-first pushes the workflow branch before launch 鈥?so the launcher pre-builds the worktree as the claim carrier + ready workspace, and the session reuses it (cost is front-loaded, not wasted). The worktree path goes in the claim comment + seed prompt; `?directory=` stays on the creator's workspace | -| 11 | Launching a worker and never hearing back 鈥?orchestrator polls a few times, stops, loses visibility ([org-internal #2374]) | Append the completion-report block to the seed prompt (Completion-report protocol section): worker `prompt_async`s a one-line status back into the orchestrator session on done/blocked/handoff, with issue-comment fallback | -| 12 | Headless worker calls the `question` tool 鈫?no attached client: the question deadlocks in a queue, or `auto_approve` silently auto-approves a direction-setting decision ([org-internal #2378]) | NEVER call `question` from a headless worker. Decision boundary (Completion-report protocol section): directional/irreversible decision 鈫?`status=blocked` report + one-line decision point, then STOP; the orchestrator is the sole human-decision entry point | -| 13 | Headless worker self-merges its PR / self-closes the issue once the PR looks ready, despite a seed instruction to keep it open ([org-internal #2386]; N-02 [org-internal #2367] / N-03 [org-internal #2368], commits `d1565c99` / `040f21b7`) | Self-merge / self-close / touching `main` are irreversible directional actions 鈫?blocked by the Decision boundary. PR ready 鈫?keep it OPEN, report `status=done branch=`; the orchestrator opens/admits the PR, merges and closes uniformly | -| 14 | Trusting `state=generating` as "alive" 鈥?a hung stream goes unrescued (session A, 2026-08-16: `generating` 6+ min, count frozen at 478) | Poll state AND message count twice 30鈥?0 s apart; frozen count = hung 鈫?abort + re-wake (Post-launch lifecycle section) | -| 15 | Re-sending a wake prompt to a hung session and waiting 鈥?queued prompts are never consumed while the stream is dead | Abort FIRST (`POST /session/:id/abort` 鈫?200, verify `idle`), THEN re-send; a prompt queued before the abort may still never fire 鈥?always re-deliver | -| 16 | A worker woken from restart / long idle acts on its stale worldview 鈥?re-dispatches superseded work or claims authority it no longer holds (2026-08-16: revived worker re-ran an already-published FAIL verify; ops session announced an inherited "orchestrator" takeover) | Fact-baseline injection (Post-launch lifecycle section): authoritative facts + invalidated assumptions + one concrete re-assignment. Only the CURRENT authority injects; a session that suspects IT is stale verifies identity against the durable record first (`rules/compact.md` identity clause) | -| 17 | Worker goes silent mid-task: provider quota wall killed the turn (usage limit / 429) 鈥?no completion-report, no blocked report; from the issue side indistinguishable from silent completion (2026-08-29 [org-internal #3627]: opencode-go `weekly usage limit reached` after the 13:18 Round-1 FAIL; ticket stalled until manually re-woken) | Detect via tail probe (`finish=error` + 鈿狅笍 usage-limit part, `metadata.reason` in the quota family) + `GET /provider` quota markers; re-wake on a known-good provider with the fact-baseline template 鈥?`reference/quota-wall-recovery.md` ([org-internal #3669]). Prevent: pre-dispatch provider check + the seed's 閰嶉鑷姤 clause (`reference/completion-report.md`, [org-internal #3669]) 鈥?quota exhaustion is a `status=blocked reason=quota-exhausted` report, not a silent death | +| 8 | Verifying models via `GET /model` — no such endpoint (404) | Use `GET /provider` → `.connected` (provider live check); `list-models` tool also works. `GET /config/providers` lists _configured_ models only | +| 9 | Launching a session for a ticket another live session already owns → conflicting diffs, duplicate work ([org-internal #1744]/[org-internal #1753]) | Run the Step-0 owner check (`GET /session` title scan + worktree/branch/PR scan); ABORT and report on conflict | +| 10 | Creating a worktree / fresh checkout for the new session (unnecessary `bun install` + `.codegraph/` rebuild cost) | Don't — for _general_ launches: a session is a conversation pointed at a directory, so reuse the current workspace; worktree creation is a separate _workflow_ decision the launched session makes if/when it starts a workflow. **Ticket-recipe carve-out ([org-internal #2297]):** the ticket's work happens in a dedicated worktree anyway (worktree discipline), and claim-first pushes the workflow branch before launch — so the launcher pre-builds the worktree as the claim carrier + ready workspace, and the session reuses it (cost is front-loaded, not wasted). The worktree path goes in the claim comment + seed prompt; `?directory=` stays on the creator's workspace | +| 11 | Launching a worker and never hearing back — orchestrator polls a few times, stops, loses visibility ([org-internal #2374]) | Append the completion-report block to the seed prompt (Completion-report protocol section): worker `prompt_async`s a one-line status back into the orchestrator session on done/blocked/handoff, with issue-comment fallback | +| 12 | Headless worker calls the `question` tool → no attached client: the question deadlocks in a queue, or `auto_approve` silently auto-approves a direction-setting decision ([org-internal #2378]) | NEVER call `question` from a headless worker. Decision boundary (Completion-report protocol section): directional/irreversible decision → `status=blocked` report + one-line decision point, then STOP; the orchestrator is the sole human-decision entry point | +| 13 | Headless worker self-merges its PR / self-closes the issue once the PR looks ready, despite a seed instruction to keep it open ([org-internal #2386]; N-02 [org-internal #2367] / N-03 [org-internal #2368], commits `d1565c99` / `040f21b7`) | Self-merge / self-close / touching `main` are irreversible directional actions → blocked by the Decision boundary. PR ready → keep it OPEN, report `status=done branch=`; the orchestrator opens/admits the PR, merges and closes uniformly | +| 14 | Trusting `state=generating` as "alive" — a hung stream goes unrescued (session A, 2026-08-16: `generating` 6+ min, count frozen at 478) | Poll state AND message count twice 30–60 s apart; frozen count = hung → abort + re-wake (Post-launch lifecycle section) | +| 15 | Re-sending a wake prompt to a hung session and waiting — queued prompts are never consumed while the stream is dead | Abort FIRST (`POST /session/:id/abort` → 200, verify `idle`), THEN re-send; a prompt queued before the abort may still never fire — always re-deliver | +| 16 | A worker woken from restart / long idle acts on its stale worldview — re-dispatches superseded work or claims authority it no longer holds (2026-08-16: revived worker re-ran an already-published FAIL verify; ops session announced an inherited "orchestrator" takeover) | Fact-baseline injection (Post-launch lifecycle section): authoritative facts + invalidated assumptions + one concrete re-assignment. Only the CURRENT authority injects; a session that suspects IT is stale verifies identity against the durable record first (`rules/compact.md` identity clause) | +| 17 | Worker goes silent mid-task: provider quota wall killed the turn (usage limit / 429) — no completion-report, no blocked report; from the issue side indistinguishable from silent completion (2026-08-29 [org-internal #3627]: opencode-go `weekly usage limit reached` after the 13:18 Round-1 FAIL; ticket stalled until manually re-woken) | Detect via tail probe (`finish=error` + ⚠️ usage-limit part, `metadata.reason` in the quota family) + `GET /provider` quota markers; re-wake on a known-good provider with the fact-baseline template — `reference/quota-wall-recovery.md` ([org-internal #3669]). Prevent: pre-dispatch provider check + the seed's 配额自报 clause (`reference/completion-report.md`, [org-internal #3669]) — quota exhaustion is a `status=blocked reason=quota-exhausted` report, not a silent death | ## Known limitation (separate from this workaround) A missing `model` producing an **apparent silent stall** (no surfaced error, no generation) is an observability / robustness gap. Current code does NOT -hard-require `model` 鈥?it fills it via a fallback chain (`prompt.ts:858`) and any +hard-require `model` — it fills it via a fallback chain (`prompt.ts:858`) and any resolution failure surfaces as a published `Session.Event.Error` on the synchronous path (`prompt.ts:804-822`), but the **async** `prompt_async` handler catches that cause (`forkPromptDelivery` in `handlers/session.ts`) so the HTTP caller never sees it. The proper fix is to surface the error (or reject unusable models at the boundary) so a stalled session is diagnosable. Until then, **always -provide an explicit known-good `model`** 鈥?this skill is the documented -workaround (see [org-internal #1695] "閬楃暀 minor"). +provide an explicit known-good `model`** — this skill is the documented +workaround (see [org-internal #1695] "遗留 minor"). **Update ([org-internal #2912], 2026-08-20)**: usage-limit turn failures (HTTP 429 / FreeUsageLimitError / GoUsageLimitError) are no longer silent shells. When such a turn terminates (retry schedule ends or the spin is aborted), the errored -assistant message persists `finish=error` plus a human-readable 鈿狅笍 text part +assistant message persists `finish=error` plus a human-readable ⚠️ text part carrying provider, limit reason, and reset hint (`metadata: { error: true, -reason, provider }`) 鈥?visible via `GET /session/:id/message` and rendered in +reason, provider }`) — visible via `GET /session/:id/message` and rendered in the web UI like any assistant text. Pitfall #14's double-poll is still the liveness check while a 429 retry spin is IN progress (the spin itself retries with backoff until interrupted); the explicit error part only appears once the @@ -502,13 +502,13 @@ turn ends. The general async-path observability gap above remains tracked by - current-model reuse, and corrects the endpoint/claim against current code). - Completion-report protocol: **[org-internal #2374]** (worker-side active report back to the launching orchestrator session; fallback to issue comment). -- Quota-wall recovery recipe: **[org-internal #3669]** (evidence instance [org-internal #3627]) 鈥?detect a +- Quota-wall recovery recipe: **[org-internal #3669]** (evidence instance [org-internal #3627]) — detect a worker killed by a provider usage limit, re-wake on a known-good provider, pre-dispatch prevention. - Decision boundary: **[org-internal #2378]** (headless worker MUST NOT call `question`; directional decisions go through `status=blocked` reports to the orchestrator) and **[org-internal #2386]** (the worker MUST NOT self-merge its PR, - self-close the issue, or touch `main` 鈥?the orchestrator merges and closes + self-close the issue, or touch `main` — the orchestrator merges and closes uniformly). - Correction of the prior misdiagnosis: **[org-internal #1691]** (closed Invalid), comment 9205. - Synchronous client always sends `model`: @@ -519,29 +519,29 @@ turn ends. The general async-path observability gap above remains tracked by ## References -**On-demand references** (NOT injected 鈥?read at the declared timing): +**On-demand references** (NOT injected — read at the declared timing): -- `reference/ticket-recipe.md` 鈥?ticket-driven launch (`Launch a session for a -specific ticket (#N)`): read BEFORE creating any session for issue #N 鈥? +- `reference/ticket-recipe.md` — ticket-driven launch (`Launch a session for a +specific ticket (#N)`): read BEFORE creating any session for issue #N — Step-0 owner check, seed-prompt composition, launch + poll commands. -- `reference/completion-report.md` 鈥?completion-report protocol (瀹屾垚鍥炴姤鍗忚, +- `reference/completion-report.md` — completion-report protocol (完成回报协议, [org-internal #2374]): read BEFORE composing a seed prompt for a worker you need to hear - back from 鈥?orchestrator block template, worker-side report steps, decision + back from — orchestrator block template, worker-side report steps, decision boundary ([org-internal #2378] / [org-internal #2386]), quota self-report clause ([org-internal #3669]). -- `reference/lifecycle-ops.md` 鈥?post-launch lifecycle ([org-internal #2459]): read when - managing a launched session 鈥?health-check polling, hung-stream diagnosis +- `reference/lifecycle-ops.md` — post-launch lifecycle ([org-internal #2459]): read when + managing a launched session — health-check polling, hung-stream diagnosis (state=generating + frozen counters), abort + re-wake rescue, fact-baseline injection, authorization asymmetry. -- `reference/quota-wall-recovery.md` 鈥?quota-wall recovery ([org-internal #3669], evidence +- `reference/quota-wall-recovery.md` — quota-wall recovery ([org-internal #3669], evidence [org-internal #3627]): read when a worker goes silent mid-task (suspected provider usage - limit) or BEFORE dispatching a long-running worker 鈥?detection signals - (finish=error 鈿狅笍 tail, provider quota markers, log grep patterns), + limit) or BEFORE dispatching a long-running worker — detection signals + (finish=error ⚠️ tail, provider quota markers, log grep patterns), provider-switch re-wake + fact-baseline template, pre-dispatch provider check, quota self-report clause. -- `rules/headless-session-ops` wiki page 鈥?authoritative reference (this +- `rules/headless-session-ops` wiki page — authoritative reference (this skill is its runtime carrier; link in the Directory-convention note above). -- `core/rules/session-scope-guard.md` 鈥?owner-check + claim-first rule +- `core/rules/session-scope-guard.md` — owner-check + claim-first rule (the ticket recipe's Step 0 is its launch-time application). -- `core/rules/compact.md` 鈥?identity-verification clause backing the +- `core/rules/compact.md` — identity-verification clause backing the authorization-asymmetry rule (`reference/lifecycle-ops.md`). diff --git a/core/skills/project-kickoff/SKILL.md b/core/skills/project-kickoff/SKILL.md index b9f50e7..a2a474c 100644 --- a/core/skills/project-kickoff/SKILL.md +++ b/core/skills/project-kickoff/SKILL.md @@ -2,41 +2,41 @@ name: project-kickoff description: > Use ONLY when the user asks about project setup, kickoff, or project - initialization ("kickoff", "project setup", "椤圭洰鍒濆鍖?, "椤圭洰閰嶇疆"). + initialization ("kickoff", "project setup", "项目初始化", "项目配置"). Provides guidance on the octopus project kickoff workflow: checking readiness, running /kickoff or octopus kickoff, configuring codegraph, and understanding - project scaffolding. Do not use for application-level configuration 鈥? + project scaffolding. Do not use for application-level configuration — this skill covers the octopus project lifecycle, not user application code. triggers: - # High-discrimination phrases only ([org-internal #4525]): bare "setup"/"init"/"閰嶇疆" fire + # High-discrimination phrases only ([org-internal #4525]): bare "setup"/"init"/"配置" fire # on any configuration mention, and the skill gate folds the previous - # assistant turn on approval turns 鈥?one stray "閰嶇疆" in a reply re-fires + # assistant turn on approval turns — one stray "配置" in a reply re-fires # the gate every turn. Keep triggers to phrases that name THIS workflow; # generic setup/init questions still route via the description in the # skills listing. - kickoff - project setup - - 椤圭洰鍒濆鍖? - - 椤圭洰閰嶇疆 + - 项目初始化 + - 项目配置 role: Producer --- -> Core 涓珛鐗堬紙Increment 6a 鏀瑰啓锛屽師 deferHard verbatimDir锛夈€傛満鍒躲€佺粨鏋勪笌 frontmatter 淇濇寔锛涘疄渚嬫湳璇紙宸ュ叿鍚嶃€佽矾寰勩€佸伐鍗曞彿锛夋寜 `core/adapters/TERMINOLOGY.md` 缁戝畾鍒板叿浣撳疄渚嬨€? +> Core 中立版(Increment 6a 改写,原 deferHard verbatimDir)。机制、结构与 frontmatter 保持;实例术语(工具名、路径、工单号)按 `core/adapters/TERMINOLOGY.md` 绑定到具体实例。 # Project Kickoff ## 1. Overview -Project kickoff is the octopus project initialization workflow that gets a project from zero to fully configured in one command. It covers readiness detection 鈫?setup 鈫?scaffold 鈫?config 鈫?codegraph init 鈫?skills overlay pull, producing a project that is ready for the full SDLC pipeline without manual configuration steps. +Project kickoff is the octopus project initialization workflow that gets a project from zero to fully configured in one command. It covers readiness detection → setup → scaffold → config → codegraph init → skills overlay pull, producing a project that is ready for the full SDLC pipeline without manual configuration steps. ## 2. When This Skill Activates This skill activates when the user's text matches any of the trigger keywords: -- `kickoff` 鈥?asking about the `/kickoff` command or the kickoff workflow -- `project setup` 鈥?asking how to set up a project in octopus -- `椤圭洰鍒濆鍖朻 / `椤圭洰閰嶇疆` 鈥?asking how to initialize or configure an octopus project (Chinese) +- `kickoff` — asking about the `/kickoff` command or the kickoff workflow +- `project setup` — asking how to set up a project in octopus +- `项目初始化` / `项目配置` — asking how to initialize or configure an octopus project (Chinese) -Generic "setup"/"init"/"閰嶇疆" mentions are deliberately NOT triggers ([org-internal #4525]): they collide with everyday configuration talk, and the hard skill gate folds the previous assistant turn into its match text on approval turns 鈥?a passing "鎴栭厤缃?x: false" in a reply would re-fire the gate every turn. `/init` note: `/init` generates AGENTS.md via LLM; kickoff uses the managed template from `octopus setup` 鈥?the Builder distinguishes based on context. +Generic "setup"/"init"/"配置" mentions are deliberately NOT triggers ([org-internal #4525]): they collide with everyday configuration talk, and the hard skill gate folds the previous assistant turn into its match text on approval turns — a passing "或配置x: false" in a reply would re-fire the gate every turn. `/init` note: `/init` generates AGENTS.md via LLM; kickoff uses the managed template from `octopus setup` — the Builder distinguishes based on context. Typical user intents include: encountering a readiness prompt at session start, wondering how to initialize a project, or wanting to understand the kickoff workflow steps. @@ -57,43 +57,43 @@ ls .gitea/workflows/ .github/workflows/ 2>/dev/null || echo "[GAP: no CI workflo ``` If no workflows exist, recommend creating `.gitea/workflows/ci.yml` (this repo -uses Gitea Actions) 鈥?or `.github/workflows/ci.yml` for a GitHub-hosted project -鈥?with at minimum: typecheck, lint, and test gates. Without CI, every merge is a -manual gate 鈥?type errors, lint regressions, and test failures can land on +uses Gitea Actions) — or `.github/workflows/ci.yml` for a GitHub-hosted project +— with at minimum: typecheck, lint, and test gates. Without CI, every merge is a +manual gate — type errors, lint regressions, and test failures can land on main and go undetected until the next local checkout. ## 4. Workflow The kickoff workflow runs in four automated steps: -1. **Check readiness** 鈥?Determine if the project needs setup. Run `checkReadinessWithGitea()` or `octopus kickoff --check-only` to inspect the current state. -2. **Run kickoff** 鈥?Execute `/kickoff` from the TUI or `octopus kickoff` from the terminal. -3. **What happens** 鈥?A Phase 0 gate, then five sequential phases: - - Phase 0 鈥?**Gitea auth check**: derives the `gitea` block from the git remote origin (persisting missing fields), then verifies the token (`GET /user`) and runs the auth wizard on failure; never aborts kickoff - - [1/5] **Setup** 鈥?Creates project directories and writes `AGENTS.md` from the managed template - - [2/5] **Scaffold** 鈥?Creates the `/` skeleton (`core/skills/` directory + default `octopus.jsonc` template if absent); domain skills content arrives via the [5/5] overlay pull - - [3/5] **Config stub + MCP wiring** 鈥?Writes the `mcp.codegraph` block into `octopus.jsonc` - - [4/5] **Codegraph init** 鈥?Initializes and builds the per-project `.codegraph/` index - - [5/5] **Skills remote + overlay pull** 鈥?Opt-in semantics ([org-internal #4370]); branches by flags when no `overlay.remotes` entry is configured: - - no flags (interactive run) 鈥?**safe default**: nothing is written; a remediation hint is printed (pass `--skills-remote`, or edit `overlay.remotes`), and the readiness skills item stays not-ready - - `--yes` 鈥?legacy behavior kept: writes the default skills remote, then pulls domain skills content (`octopus skills pull` equivalent) - - `--skills-remote ` (+ optional `--skills-remote-base-url `) 鈥?writes the specified remote instead (ref `main`; base URL defaults to ``), then pulls from it - - `overlay.remotes` already configured 鈥?the write is skipped (idempotent; an explicit `--skills-remote` is ignored with a warning) -4. **Post-kickoff verification** 鈥?Run `octopus kickoff --check-only` again. After a `--yes` or `--skills-remote` run it reports all 5 items as ready; after a no-flag interactive run it reports 4 of 5 鈥?the skills item stays not-ready until a remote is configured (`--skills-remote`, `--yes`, or a manual `overlay.remotes` edit + `octopus skills pull`). +1. **Check readiness** — Determine if the project needs setup. Run `checkReadinessWithGitea()` or `octopus kickoff --check-only` to inspect the current state. +2. **Run kickoff** — Execute `/kickoff` from the TUI or `octopus kickoff` from the terminal. +3. **What happens** — A Phase 0 gate, then five sequential phases: + - Phase 0 — **Gitea auth check**: derives the `gitea` block from the git remote origin (persisting missing fields), then verifies the token (`GET /user`) and runs the auth wizard on failure; never aborts kickoff + - [1/5] **Setup** — Creates project directories and writes `AGENTS.md` from the managed template + - [2/5] **Scaffold** — Creates the `/` skeleton (`core/skills/` directory + default `octopus.jsonc` template if absent); domain skills content arrives via the [5/5] overlay pull + - [3/5] **Config stub + MCP wiring** — Writes the `mcp.codegraph` block into `octopus.jsonc` + - [4/5] **Codegraph init** — Initializes and builds the per-project `.codegraph/` index + - [5/5] **Skills remote + overlay pull** — Opt-in semantics ([org-internal #4370]); branches by flags when no `overlay.remotes` entry is configured: + - no flags (interactive run) — **safe default**: nothing is written; a remediation hint is printed (pass `--skills-remote`, or edit `overlay.remotes`), and the readiness skills item stays not-ready + - `--yes` — legacy behavior kept: writes the default skills remote, then pulls domain skills content (`octopus skills pull` equivalent) + - `--skills-remote ` (+ optional `--skills-remote-base-url `) — writes the specified remote instead (ref `main`; base URL defaults to ``), then pulls from it + - `overlay.remotes` already configured — the write is skipped (idempotent; an explicit `--skills-remote` is ignored with a warning) +4. **Post-kickoff verification** — Run `octopus kickoff --check-only` again. After a `--yes` or `--skills-remote` run it reports all 5 items as ready; after a no-flag interactive run it reports 4 of 5 — the skills item stays not-ready until a remote is configured (`--skills-remote`, `--yes`, or a manual `overlay.remotes` edit + `octopus skills pull`). ### First-ticket hint ([org-internal #3061]) For a brand-new project still validating its concept, open the FIRST ticket as -`Kind/MVP` 鈥?the interactive no-pipeline mode (decision log + debt register in +`Kind/MVP` — the interactive no-pipeline mode (decision log + debt register in the ticket body). It graduates by relabeling to `Kind/Feature` (analyze-dag backfill mode picks up the decision log) when a second session, an external contract, or a second consumer of a shared type appears. Do not route early -concept work into the full pipeline "to be safe" 鈥?the pipeline's coordination +concept work into the full pipeline "to be safe" — the pipeline's coordination machinery is cost, not quality, at zero coordination. ## 5. Re-kickoff / Idempotency -Re-running kickoff on an already-configured project is safe 鈥?every step is idempotent: +Re-running kickoff on an already-configured project is safe — every step is idempotent: - **Setup** skips existing directories - **Scaffold** skips existing files @@ -107,26 +107,26 @@ Re-running kickoff on an already-configured project is safe 鈥?every step is id Common issues and their resolutions: -- **`codegraph: command not found`** 鈥?Run the `codegraph-setup` skill, or install via `bun add -g @colbymchenry/codegraph` -- **Permission denied** 鈥?Check project directory ownership and write permissions -- **Partially complete state** 鈥?Re-run `/kickoff`; idempotent steps will complete the missing parts without affecting already-completed steps -- **MCP tools not available** 鈥?Verify `mcp.codegraph` exists in `octopus.jsonc`; re-run `octopus kickoff` to write it +- **`codegraph: command not found`** — Run the `codegraph-setup` skill, or install via `bun add -g @colbymchenry/codegraph` +- **Permission denied** — Check project directory ownership and write permissions +- **Partially complete state** — Re-run `/kickoff`; idempotent steps will complete the missing parts without affecting already-completed steps +- **MCP tools not available** — Verify `mcp.codegraph` exists in `octopus.jsonc`; re-run `octopus kickoff` to write it ## 7. Related Commands -- `/init` 鈥?Generates `AGENTS.md` using an LLM (custom, context-aware); kickoff uses the managed template from `octopus setup` -- `octopus setup` 鈥?Creates directories + `AGENTS.md` from the managed template; kickoff calls this internally -- `octopus scaffold` 鈥?Creates the `/` skeleton (`core/skills/` directory + default `octopus.jsonc` template if absent); run `octopus skills pull` to fetch domain skills content; kickoff calls this internally -- `octopus kickoff --check-only` 鈥?Readiness check only, no pipeline execution -- `octopus kickoff --skills-remote ` 鈥?Explicit opt-in: writes the given skills remote to `overlay.remotes` (ref `main`) and pulls it instead of the default -- `octopus kickoff --skills-remote-base-url ` 鈥?Base URL for `--skills-remote` (requires it); defaults to `` when omitted -- `octopus kickoff --yes` 鈥?Skips the opt-in confirmation and keeps the legacy behavior of writing the default skills remote (`Octopus/octopus-skills`) -- `codegraph-setup` skill 鈥?For installing the codegraph CLI and initializing the per-project index +- `/init` — Generates `AGENTS.md` using an LLM (custom, context-aware); kickoff uses the managed template from `octopus setup` +- `octopus setup` — Creates directories + `AGENTS.md` from the managed template; kickoff calls this internally +- `octopus scaffold` — Creates the `/` skeleton (`core/skills/` directory + default `octopus.jsonc` template if absent); run `octopus skills pull` to fetch domain skills content; kickoff calls this internally +- `octopus kickoff --check-only` — Readiness check only, no pipeline execution +- `octopus kickoff --skills-remote ` — Explicit opt-in: writes the given skills remote to `overlay.remotes` (ref `main`) and pulls it instead of the default +- `octopus kickoff --skills-remote-base-url ` — Base URL for `--skills-remote` (requires it); defaults to `` when omitted +- `octopus kickoff --yes` — Skips the opt-in confirmation and keeps the legacy behavior of writing the default skills remote (`Octopus/octopus-skills`) +- `codegraph-setup` skill — For installing the codegraph CLI and initializing the per-project index ## 8. Reference -- `/src/project-kickoff/readiness.ts` 鈥?`checkReadiness()` implementation (sync 4-item base) -- `/src/project-kickoff/readiness-gitea.ts` 鈥?`checkReadinessWithGitea()` composed 5-item check (adds gitea auth reachable) -- `/src/cli/cmd/kickoff.ts` 鈥?`/kickoff` command handler -- `core/skills/codegraph-setup/SKILL.md` 鈥?CodeGraph setup skill -- Project roadmap at `.artifacts/project-kickoff/` (deprecated 鈥?now stored as wiki page `project-kickoff/roadmap/` via `wiki 璇诲啓 API锛堣 TERMINOLOGY锛塦) +- `/src/project-kickoff/readiness.ts` — `checkReadiness()` implementation (sync 4-item base) +- `/src/project-kickoff/readiness-gitea.ts` — `checkReadinessWithGitea()` composed 5-item check (adds gitea auth reachable) +- `/src/cli/cmd/kickoff.ts` — `/kickoff` command handler +- `core/skills/codegraph-setup/SKILL.md` — CodeGraph setup skill +- Project roadmap at `.artifacts/project-kickoff/` (deprecated — now stored as wiki page `project-kickoff/roadmap/` via `wiki 读写 API(见 TERMINOLOGY)`) diff --git a/core/skills/release/SKILL.md b/core/skills/release/SKILL.md index f346a14..b7b0e35 100644 --- a/core/skills/release/SKILL.md +++ b/core/skills/release/SKILL.md @@ -4,22 +4,22 @@ description: > Use ONLY when ready to cut a release. The Release Manager (Worker) inspects project state (git log, version files, build) to determine the semver bump, generate the changelog, tag, and produce a release artifact. Works on any - project state 鈥?no pipeline artifacts required. + project state — no pipeline artifacts required. triggers: - release - cut release - - 鍙戠増 + - 发版 - publish release - ship release role: Producer --- -> Core 涓珛鐗堬紙Increment 6a 鏀瑰啓锛屽師 deferHard verbatimDir锛夈€傛満鍒躲€佺粨鏋勪笌 frontmatter 淇濇寔锛涘疄渚嬫湳璇紙宸ュ叿鍚嶃€佽矾寰勩€佸伐鍗曞彿锛夋寜 `core/adapters/TERMINOLOGY.md` 缁戝畾鍒板叿浣撳疄渚嬨€? +> Core 中立版(Increment 6a 改写,原 deferHard verbatimDir)。机制、结构与 frontmatter 保持;实例术语(工具名、路径、工单号)按 `core/adapters/TERMINOLOGY.md` 绑定到具体实例。 # Release -Prepare and tag a release based on project facts 鈥?git history since the last +Prepare and tag a release based on project facts — git history since the last tag, current version, build status, and dependency health. No dependency on `.artifacts/{slug}/` -(**deprecated** 鈥?SDLC artifacts now live in the Gitea wiki at `{slug}/...`; +(**deprecated** — SDLC artifacts now live in the Gitea wiki at `{slug}/...`; see `_shared/gitea-read-patterns.md`) or any pipeline skill outputs. **This SKILL does not deploy.** Deployment is project-specific (K8s, Docker, @@ -32,12 +32,12 @@ The release is owned and executed by the **Release Manager** (Worker). **Context compaction**: release is a pipeline stage boundary. The main session compacts at this clean boundary ONLY when a capacity/projection trigger holds, -per `core/rules/compact.md` 搂"Stage-boundary compaction" (long multi-stage -runs 鈥?DAG Epic orchestration 鈥?keep the legacy every-boundary compaction; -short runs 鈥?bugfix / DAG task 鈥?and standalone runs default to NOT +per `core/rules/compact.md` §"Stage-boundary compaction" (long multi-stage +runs — DAG Epic orchestration — keep the legacy every-boundary compaction; +short runs — bugfix / DAG task — and standalone runs default to NOT compacting). The Release Manager itself is single-phase and artifact-driven (version files, changelog, tag); a mid-run compaction loses -nothing 鈥?re-read the release checklist state and git log to resume. +nothing — re-read the release checklist state and git log to resume. --- @@ -50,7 +50,7 @@ Before starting the release: --- -## Phase 1 鈥?Pre-release Gate +## Phase 1 — Pre-release Gate Run each check against the live project. Stop and report failures. @@ -65,82 +65,82 @@ Run each check against the live project. Stop and report failures. - No new HIGH or CRITICAL CVEs block the release. - Pre-existing HIGH/CRITICAL CVEs do NOT block but **MUST be filed as tech-debt** before the release lands. File them in TWO TIERS (batching - rule, [org-internal #3846] 鈥?a single release audit once fanned out to 14 TDs 鈫?14 PRs 鈫? + rule, [org-internal #3846] — a single release audit once fanned out to 14 TDs → 14 PRs → 56-84 CI runs on a saturated runner pool): Tier A below is the general mechanical-change batching rule; Tier B and the escape hatch follow it. -### Tier A 鈥?閫氱敤鏈烘鍙樻洿骞跺崟瑙勫垯 / Universal mechanical-change batching rule +### Tier A — 通用机械变更并单规则 / Universal mechanical-change batching rule Origin [org-internal #3846] (audit-batch pilot, generalized in place): the rule covers ANY single-source fan-out of mechanical changes, not just dependency audits. -Two source types today: (a) dependency bump audits; (b) docs 鎵归噺淇 +Two source types today: (a) dependency bump audits; (b) docs 批量修订 (batch docs revisions). -#### 瑙勫垯姝f枃 / Rule text +#### 规则正文 / Rule text - **Core**: mechanical changes of ONE class fanning out from a single - source are filed as ONE batch issue + ONE batch PR 鈥?never N脳issue + - N脳PR. The batch body keeps one row per item (per-CVE / per-doc mapping), + source are filed as ONE batch issue + ONE batch PR — never N×issue + + N×PR. The batch body keeps one row per item (per-CVE / per-doc mapping), so per-item traceability is unchanged; work the batch as a single PR. - **(a) Dependency bump audits** (the [org-internal #3846] origin case): advisories whose remediation is a plain version bump (lockfile-only diff, no semver-major jump, no overrides/catalog surgery, no API or adapter fallout) are filed - together via `宸ュ崟 API锛堣 TERMINOLOGY锛塩reate` with + together via `工单 API(见 TERMINOLOGY)create` with `title="[{origin}] audit-batch@{version}: mechanical bumps for {N} advisories (TD-{NNN})"` and labels `[tech_debt_label_id, severity_label_id, kind_bug_label_id]` - (severity = highest in the batch). One row per advisory 鈥?CVE/GHSA ID, - affected package + from鈫抰o version, severity (CVSS), advisory URL, + (severity = highest in the batch). One row per advisory — CVE/GHSA ID, + affected package + from→to version, severity (CVSS), advisory URL, recommended remediation, and a per-advisory Reactivation Trigger ("resolved when `npm audit` / `bun audit` reports no HIGH/CRITICAL for - this advisory") 鈥?so the release-notes `TD-NNN 鈫?#NNNN` mapping stays + this advisory") — so the release-notes `TD-NNN → #NNNN` mapping stays per-advisory. -- **(b) Docs 鎵归噺淇 (batch docs revisions)**: many small same-class docs +- **(b) Docs 批量修订 (batch docs revisions)**: many small same-class docs corrections discovered in one pass (e.g. a terminology sweep) file as ONE issue via the same flow, `title="[{origin}] docs-batch: {class} revisions for {N} files"`, with one row per file (path, correction, reason); same ONE-batch-PR landing. - **Landing**: when members live on separate branches, compose the single batch PR via the `land-batch` skill (`core/skills/land-batch/SKILL.md` - 鈥?cross-branch batch composition, topology B). For dependency-bump + — cross-branch batch composition, topology B). For dependency-bump batches pass `batch-compose --convergent bun.lock` (script `.gitea/scripts/batch-compose.ts`): the regenerable lockfile is exempt from path-overlap admission and lockfile-only conflicts are surgically resolved to the running head's version. -#### 閫傜敤杈圭晫 / Applicability boundary +#### 适用边界 / Applicability boundary -- **闈?mechanical锛堝惈鍒ゆ柇鎴愬垎鐨勫彉鏇达紝涓嶅苟鍗曪級** 鈥?items requiring judgment +- **非mechanical(含判断成分的变更,不并单)** — items requiring judgment (wording decisions, behavior/API changes, review-dependent edits) are never batched; file each as its own issue. -- **璺緞閲嶅彔锛坆atch 鎴愬憳瑙︾鐩稿悓鏂囦欢 鈫?涓嶅苟鍗曪紱鍞竴璞佸厤 = - 鍙啀鐢熷叡浜枃浠?`bun.lock` 缁?batch-compose `--convergent` 鏀舵暃锛?* 鈥?the sole +- **路径重叠(batch 成员触碰相同文件 → 不并单;唯一豁免 = + 可再生共享文件`bun.lock` 经batch-compose `--convergent` 收敛)** — the sole path-overlap exemption is the regenerable shared lockfile under `--convergent`; every other same-file collision stays un-batched. -- **semver-major 璺冲彉 / overrides路catalog 鎵嬫湳 / adapter路peer 鑱斿姩锛堚啋 - Tier B 鐙珛鍗曪紝缁存寔 per-package锛?* 鈥?these stay per-package Tier B +- **semver-major 跳变 / overrides·catalog 手术 / adapter·peer 联动(→ + Tier B 独立单,维持 per-package)** — these stay per-package Tier B issues. Tier B and the escape hatch (below) survive this generalization unchanged. -#### 闄嶇骇璺緞 / Degradation path +#### 降级路径 / Degradation path -- **骞跺崟 PR CI 澶辫触 鈫?鎸夐攣鏂囦欢 hunk 鎷嗗寘鍥為€€锛坆isect锛?*: for dependency +- **并单 PR CI 失败 → 按锁文件 hunk 拆包回退(bisect)**: for dependency batches, bisect by splitting lockfile hunks per package back into - per-package PRs 鈥?the original Tier A bisect semantics carried over - verbatim (鍚岃涔夛紝娉涘寲鎺緸: every mechanical batch degrades the same way). + per-package PRs — the original Tier A bisect semantics carried over + verbatim (同语义,泛化措辞: every mechanical batch degrades the same way). - **Docs batches**: split per file back into per-file PRs. - **land-batch composition fallback** (exit codes per - `.gitea/scripts/batch-compose.ts`): `3` path-overlap 鈫?split the batch + `.gitea/scripts/batch-compose.ts`): `3` path-overlap → split the batch along the reported pairs (or land the overlapping member 1:1) and re-run - per group; `4` merge conflict / `5` transport/git error 鈫?fall back to + per group; `4` merge conflict / `5` transport/git error → fall back to 1:1 PRs for the whole batch. - **Preflight hedge ([org-internal #3846])**: run `bun install --dry-run` before opening - a dependency-batch PR 鈥?the resolver accepting the composed version set + a dependency-batch PR — the resolver accepting the composed version set is a cheap pre-CI rejection of impossible bump combinations. Tier B and the escape hatch (unchanged by the generalization): -- **Tier B 鈥?surgery, one issue per package**: semver-major jumps, +- **Tier B — surgery, one issue per package**: semver-major jumps, adapter/peer fallout, or overrides/catalog surgery keep the per-package issue: `title="[{origin}] {CVE-ID/GHSA-ID} in {package} (TD-{NNN})"` with the same labels; body requirements match the Tier A rows. @@ -148,7 +148,7 @@ Tier B and the escape hatch (unchanged by the generalization): issue when same-day remediation is required (urgent HIGH/CRITICAL). De-duplicate against open `tech-debt` issues (match by -CVE/GHSA ID) before creating. Record the `TD-NNN 鈫?#NNNN` mapping in the +CVE/GHSA ID) before creating. Record the `TD-NNN → #NNNN` mapping in the release notes. See `verify` Phase 5.5 for the tech-debt promotion body template and `_shared/gitea-write-patterns.md` Pattern 3. @@ -157,19 +157,19 @@ template and `_shared/gitea-write-patterns.md` Pattern 3. | Check | Status | | ----------------- | ----------------------------------------------------- | -| Clean workspace | 鉁?/ 鉂? | +| Clean workspace | ✅/ ❌ | | Branch | {branch name} | -| Build | 鉁?/ 鉂? | -| Typecheck + Lint | 鉁?/ 鉂? | -| Tests | 鉁?/ 鉂?(N passed, M failed) | -| Dependency audit | 鉁?/ 鈿狅笍 N known CVEs (pre-existing) / 鉂?N new CVEs | +| Build | ✅/ ❌ | +| Typecheck + Lint | ✅/ ❌ | +| Tests | ✅/ ❌(N passed, M failed) | +| Dependency audit | ✅/ ⚠️ N known CVEs (pre-existing) / ❌N new CVEs | ``` If any gate fails except known CVEs, stop and report what failed. --- -## Phase 2 鈥?Version Bump +## Phase 2 — Version Bump Determine the new version by inspecting git history since the last tag. @@ -193,14 +193,14 @@ Determine the new version by inspecting git history since the last tag. | `docs:` / `chore:` | (skip) | Not user-visible | If no conventional prefix found, infer from the subject line: - - "add", "implement", "introduce" 鈫?MINOR - - "fix", "resolve", "correct" 鈫?PATCH - - "remove", "drop", "rename" (public API) 鈫?MAJOR + - "add", "implement", "introduce" → MINOR + - "fix", "resolve", "correct" → PATCH + - "remove", "drop", "rename" (public API) → MAJOR 5. **Compute bump**: - - If any MAJOR commit 鈫?bump MAJOR. - - Else if any MINOR commit 鈫?bump MINOR. - - Else 鈫?bump PATCH. + - If any MAJOR commit → bump MAJOR. + - Else if any MINOR commit → bump MINOR. + - Else → bump PATCH. ```markdown ## Version Bump @@ -210,12 +210,12 @@ Determine the new version by inspecting git history since the last tag. **New version**: {new version} **Type**: MAJOR / MINOR / PATCH **Commits since last tag**: {N} -**Reason**: {justification 鈥?e.g. "2 feat + 3 fix 鈫?MINOR"} +**Reason**: {justification — e.g. "2 feat + 3 fix → MINOR"} ``` --- -## Phase 3 鈥?Changelog +## Phase 3 — Changelog Generate the changelog from `git log ..HEAD`: @@ -224,10 +224,10 @@ Generate the changelog from `git log ..HEAD`: - Scope (if present, e.g. `feat(auth):`). - Description (the subject line, past tense, human-readable). 2. **Group by type**: - - **Added** 鈥?`feat:` commits. - - **Changed** 鈥?modifications to existing behavior (non-breaking). - - **Fixed** 鈥?`fix:` commits. - - **Breaking** 鈥?`BREAKING CHANGE:` or `!:` commits. + - **Added** — `feat:` commits. + - **Changed** — modifications to existing behavior (non-breaking). + - **Fixed** — `fix:` commits. + - **Breaking** — `BREAKING CHANGE:` or `!:` commits. 3. **Deduplicate**: squash multiple commits for the same change into one entry where it makes narrative sense. 4. Read the existing root `CHANGELOG.md` (if any) and prepend this release. @@ -235,7 +235,7 @@ Generate the changelog from `git log ..HEAD`: Write to root `CHANGELOG.md` (prepend section). ```markdown -## [{version}] 鈥?{YYYY-MM-DD} +## [{version}] — {YYYY-MM-DD} ### Added @@ -251,24 +251,24 @@ Write to root `CHANGELOG.md` (prepend section). ### Breaking -- {breaking change} ({hash}) 鈥?see migration notes above +- {breaking change} ({hash}) — see migration notes above ``` --- -## Phase 4 鈥?Tag & Finalize +## Phase 4 — Tag & Finalize -1. **Update version file** 鈥?write the new version to the project's version +1. **Update version file** — write the new version to the project's version manifest. 2. **Commit**: `git add` version file + changelog file, commit with message: ``` release: {version} ``` -3. **Tag**: `git tag v{version}` (adjust prefix per project convention 鈥? +3. **Tag**: `git tag v{version}` (adjust prefix per project convention — check existing tags with `git tag -l`). 4. **Verify tag**: `git tag -l v{version}` confirms the tag exists. -**Do not push** 鈥?the user must explicitly request pushing to remote. +**Do not push** — the user must explicitly request pushing to remote. ```markdown ## Release Artifact @@ -282,15 +282,15 @@ Write to root `CHANGELOG.md` (prepend section). | File | Change | | -------------- | ------------------------- | -| {version file} | {old} 鈫?{new} | +| {version file} | {old} → {new} | | CHANGELOG.md | Prepended {version} | ``` --- -## Phase 4b 鈥?Publish Release Artifacts (octopus project, manual) +## Phase 4b — Publish Release Artifacts (octopus project, manual) -> Octopus-specific. Other projects: substitute your own artifact pipeline 鈥? +> Octopus-specific. Other projects: substitute your own artifact pipeline — > the goal is identical (turn the tag into downloadable assets). The CI publish pipeline (`.gitea/workflows/publish.yml`) was **retired by @@ -303,17 +303,17 @@ credentials in the environment. - Clean checkout of the release commit (tag pushed or about to be pushed). - Credentials in env: - - `GITEA_TOKEN` 鈥?PAT with `write:repository` (release create/undraft, + - `GITEA_TOKEN` — PAT with `write:repository` (release create/undraft, tag push) and `write:package` (container registry). The `CI_PAT` secret value is the canonical token. - - `NODE_AUTH_TOKEN` 鈥?only if publishing to npm. - - `AUR_KEY` 鈥?only if pushing the AUR package. + - `NODE_AUTH_TOKEN` — only if publishing to npm. + - `AUR_KEY` — only if pushing the AUR package. - Docker logged in to the Gitea container registry: `echo "$GITEA_TOKEN" | docker login -u --password-stdin`. ### Steps -1. **Version + draft release** (idempotent 鈥?skips if already published): +1. **Version + draft release** (idempotent — skips if already published): ```bash GITEA_TOKEN= GH_REPO=Octopus/octopus bun script/version.ts ``` @@ -344,7 +344,7 @@ credentials in the environment. -H "Content-Type: application/json" -d '{"draft": false}' ``` -5. **Verify assets** 鈥?the release must carry 12 assets: +5. **Verify assets** — the release must carry 12 assets: ```bash curl -sS "/api/v1/repos/Octopus/octopus/releases/tags/v{version}" \ -H "Authorization: token $GITEA_TOKEN" | jq '.assets | length' @@ -368,7 +368,7 @@ credentials in the environment. --- -## Phase 5 鈥?Rollback Plan +## Phase 5 — Rollback Plan Document how to undo this release: @@ -379,7 +379,7 @@ Document how to undo this release: 2. **Data rollback** (if applicable): - For each migration, confirm the `down` migration exists and has been tested. - If the release changes data format without a reversible migration, mark - `鈿狅笍 IRREVERSIBLE DATA CHANGE`. + `⚠️ IRREVERSIBLE DATA CHANGE`. ```markdown ## Rollback Plan @@ -403,7 +403,7 @@ git tag -d v{version} --- -## Phase 6 鈥?Post-release Smoke Test +## Phase 6 — Post-release Smoke Test 1. Run the project's build command on the tagged commit. Must pass. 2. Run the test suite. Must pass. @@ -411,15 +411,15 @@ git tag -d v{version} --- -## Phase 7 鈥?Report +## Phase 7 — Report ```markdown ## Release Report -**Version**: {old} 鈫?{new} (MAJOR / MINOR / PATCH) +**Version**: {old} → {new} (MAJOR / MINOR / PATCH) **Tag**: v{version} **Commits**: {N} since last tag ({feat} features, {fix} fixes) -**Typecheck**: 鉁?| Lint: 鉁?| Tests: 鉁? +**Typecheck**: ✅| Lint: ✅| Tests: ✅ ### Changelog @@ -427,21 +427,21 @@ git tag -d v{version} ### Release Artifacts -- Commit: {hash} 鈥?`release: {version}` +- Commit: {hash} — `release: {version}` - Tag: v{version} - Changelog: CHANGELOG.md updated --- -**Deploy**: {manual step 鈥?"merge to main triggers CI", etc.} +**Deploy**: {manual step — "merge to main triggers CI", etc.} **Rollback**: see Phase 5 above -**Next**: `core/skills/retrospective/SKILL.md` (optional 鈥?run on any project state) +**Next**: `core/skills/retrospective/SKILL.md` (optional — run on any project state) ``` --- ## References -- `core/checklists/release.md` 鈥?Release checklist +- `core/checklists/release.md` — Release checklist - Semver spec: https://semver.org - Conventional Commits: https://www.conventionalcommits.org diff --git a/guards/check-encoding-health.ts b/guards/check-encoding-health.ts new file mode 100644 index 0000000..d8d21e8 --- /dev/null +++ b/guards/check-encoding-health.ts @@ -0,0 +1,94 @@ +#!/usr/bin/env bun + +import fs from "node:fs" +import path from "node:path" + +const ROOT = path.resolve(import.meta.dir, "..") +const CORE_DIR = path.join(ROOT, "core") + +const SCAN_EXTENSIONS = new Set([".md", ".json", ".ts", ".tsx", ".js", ".mjs", ".cjs", ".yaml", ".yml", ".txt"]) + +export const MOJIBAKE_SIGNATURES: { name: string; pattern: RegExp }[] = [ + { name: "em-dash-mojibake", pattern: /\u9225/ }, + { name: "right-arrow-mojibake", pattern: /\u922B/ }, + { name: "check-mark-mojibake", pattern: /\u9257|\u9253/ }, + { name: "cross-mark-mojibake", pattern: /\u9242/ }, + { name: "geq-mojibake", pattern: /\u922E/ }, + { name: "multiplication-mojibake", pattern: /\u8137/ }, + { name: "replacement-char", pattern: /\uFFFD/ }, + { name: "truncated-mojibake", pattern: /[\u9000-\u9FFF]\?/ }, + { name: "gbk-cjk-bigram", pattern: /椤圭|鍒濆|閰嶇|鐨勶|鎷夎|浼氳瘽|鏃犲|淇濇|缁戝|瀹屾|涓珛|鏀瑰啓|瀹炰緥|鎵归|寤惰|璋冨|绀轰|鍘熸|閰嶉|鎹薄|閫昏|椤甸|绔犵|宸插|鍒嗛|鍏抽|鍚岀|鍚庣|鍓嶇|杩斿|杩涘|閫変|鎸夌|鏉冮|鏂囦|鏂囨/ }, +] + +export interface EncodingViolation { + file: string + line: number + signature: string + match: string +} + +export function scanFile(filePath: string): EncodingViolation[] { + const text = fs.readFileSync(filePath, "utf8") + const violations: EncodingViolation[] = [] + const lines = text.split(/\r?\n/) + for (let i = 0; i < lines.length; i++) { + for (const sig of MOJIBAKE_SIGNATURES) { + const m = lines[i]!.match(sig.pattern) + if (m) { + violations.push({ + file: filePath, + line: i + 1, + signature: sig.name, + match: m[0]!, + }) + } + } + } + return violations +} + +export function collectFiles(dir: string): string[] { + const out: string[] = [] + if (!fs.existsSync(dir)) return out + const walk = (d: string) => { + for (const entry of fs.readdirSync(d, { withFileTypes: true })) { + const full = path.join(d, entry.name) + if (entry.isDirectory()) walk(full) + else if (SCAN_EXTENSIONS.has(path.extname(entry.name))) out.push(full) + } + } + walk(dir) + return out.sort() +} + +function main(): void { + if (!fs.existsSync(CORE_DIR)) { + console.error(`check-encoding-health: core/ directory not found at ${CORE_DIR} — cannot verify (fail-closed)`) + process.exit(1) + } + const files = collectFiles(CORE_DIR) + if (files.length === 0) { + console.error(`check-encoding-health: 0 scannable files under ${CORE_DIR} — cannot verify (fail-closed)`) + process.exit(1) + } + const violations: EncodingViolation[] = [] + for (const f of files) { + try { + violations.push(...scanFile(f)) + } catch (err) { + console.error(`check-encoding-health: failed to read ${f}: ${(err as Error).message}`) + process.exit(1) + } + } + if (violations.length > 0) { + console.error(`check-encoding-health: ${violations.length} mojibake violation(s) in core/:`) + for (const v of violations) { + const rel = path.relative(ROOT, v.file) + console.error(` ${rel}:${v.line} [${v.signature}] ${JSON.stringify(v.match)}`) + } + process.exit(1) + } + console.log(`check-encoding-health: OK (${files.length} files scanned, 0 violations)`) +} + +if (import.meta.main) main() diff --git a/package.json b/package.json index 2b149b0..ffe6214 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "octopus-workflow", - "version": "0.1.2", + "version": "0.1.3", "description": "Standalone workflow core: provider-neutral agent workflow corpus (skills / rules / checklists / templates / schemas) with drift guards, extracted from the octopus agent platform.", "license": "MIT", "private": false, @@ -9,7 +9,8 @@ "scripts": { "check:core-cohesion": "bun guards/check-core-cohesion.ts", "check:core-p1": "bun guards/check-core-p1.ts", - "delink:core": "bun guards/delink-core.ts" + "delink:core": "bun guards/delink-core.ts", + "check:encoding-health": "bun guards/check-encoding-health.ts" }, "repository": { "type": "git",