@seanmars/tospec 0.24.0 → 0.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +243 -0
- package/README.md +10 -6
- package/assets/dashboard/lib/derive.js +11 -5
- package/assets/dashboard/lib/store.js +4 -2
- package/assets/dashboard/views/change.js +5 -3
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +42 -7
- package/dist/cli/index.js.map +1 -1
- package/dist/commands/config.d.ts.map +1 -1
- package/dist/commands/config.js +213 -100
- package/dist/commands/config.js.map +1 -1
- package/dist/commands/dashboard.d.ts +9 -0
- package/dist/commands/dashboard.d.ts.map +1 -1
- package/dist/commands/dashboard.js +77 -21
- package/dist/commands/dashboard.js.map +1 -1
- package/dist/commands/decision.d.ts +38 -1
- package/dist/commands/decision.d.ts.map +1 -1
- package/dist/commands/decision.js +303 -27
- package/dist/commands/decision.js.map +1 -1
- package/dist/commands/metrics.d.ts +3 -4
- package/dist/commands/metrics.d.ts.map +1 -1
- package/dist/commands/metrics.js +3 -5
- package/dist/commands/metrics.js.map +1 -1
- package/dist/commands/shared-output.d.ts +24 -0
- package/dist/commands/shared-output.d.ts.map +1 -1
- package/dist/commands/shared-output.js +21 -2
- package/dist/commands/shared-output.js.map +1 -1
- package/dist/commands/show.d.ts.map +1 -1
- package/dist/commands/show.js +12 -9
- package/dist/commands/show.js.map +1 -1
- package/dist/commands/validate.d.ts +19 -0
- package/dist/commands/validate.d.ts.map +1 -1
- package/dist/commands/validate.js +243 -64
- package/dist/commands/validate.js.map +1 -1
- package/dist/commands/workflow/instructions.d.ts +7 -1
- package/dist/commands/workflow/instructions.d.ts.map +1 -1
- package/dist/commands/workflow/instructions.js +299 -81
- package/dist/commands/workflow/instructions.js.map +1 -1
- package/dist/commands/workflow/new-change.d.ts.map +1 -1
- package/dist/commands/workflow/new-change.js +26 -17
- package/dist/commands/workflow/new-change.js.map +1 -1
- package/dist/commands/workflow/prompt-escape.d.ts +25 -0
- package/dist/commands/workflow/prompt-escape.d.ts.map +1 -0
- package/dist/commands/workflow/prompt-escape.js +62 -0
- package/dist/commands/workflow/prompt-escape.js.map +1 -0
- package/dist/commands/workflow/schemas.d.ts.map +1 -1
- package/dist/commands/workflow/schemas.js +34 -16
- package/dist/commands/workflow/schemas.js.map +1 -1
- package/dist/commands/workflow/shared.d.ts +46 -10
- package/dist/commands/workflow/shared.d.ts.map +1 -1
- package/dist/commands/workflow/shared.js +66 -30
- package/dist/commands/workflow/shared.js.map +1 -1
- package/dist/commands/workflow/status.d.ts +5 -2
- package/dist/commands/workflow/status.d.ts.map +1 -1
- package/dist/commands/workflow/status.js +62 -28
- package/dist/commands/workflow/status.js.map +1 -1
- package/dist/core/archive.d.ts +38 -0
- package/dist/core/archive.d.ts.map +1 -1
- package/dist/core/archive.js +492 -75
- package/dist/core/archive.js.map +1 -1
- package/dist/core/artifact-graph/index.d.ts +2 -2
- package/dist/core/artifact-graph/index.d.ts.map +1 -1
- package/dist/core/artifact-graph/index.js +2 -2
- package/dist/core/artifact-graph/index.js.map +1 -1
- package/dist/core/artifact-graph/instruction-loader.d.ts +8 -0
- package/dist/core/artifact-graph/instruction-loader.d.ts.map +1 -1
- package/dist/core/artifact-graph/instruction-loader.js +58 -36
- package/dist/core/artifact-graph/instruction-loader.js.map +1 -1
- package/dist/core/artifact-graph/resolver.d.ts +39 -12
- package/dist/core/artifact-graph/resolver.d.ts.map +1 -1
- package/dist/core/artifact-graph/resolver.js +82 -36
- package/dist/core/artifact-graph/resolver.js.map +1 -1
- package/dist/core/artifact-graph/schema.d.ts +24 -0
- package/dist/core/artifact-graph/schema.d.ts.map +1 -1
- package/dist/core/artifact-graph/schema.js +32 -1
- package/dist/core/artifact-graph/schema.js.map +1 -1
- package/dist/core/artifact-graph/stub-detection.d.ts +10 -2
- package/dist/core/artifact-graph/stub-detection.d.ts.map +1 -1
- package/dist/core/artifact-graph/stub-detection.js +18 -11
- package/dist/core/artifact-graph/stub-detection.js.map +1 -1
- package/dist/core/artifact-graph/types.d.ts +2 -2
- package/dist/core/artifact-graph/types.d.ts.map +1 -1
- package/dist/core/artifact-graph/types.js +10 -1
- package/dist/core/artifact-graph/types.js.map +1 -1
- package/dist/core/change-metadata/schema.d.ts.map +1 -1
- package/dist/core/change-metadata/schema.js +18 -4
- package/dist/core/change-metadata/schema.js.map +1 -1
- package/dist/core/change-presenter.d.ts.map +1 -1
- package/dist/core/change-presenter.js +13 -8
- package/dist/core/change-presenter.js.map +1 -1
- package/dist/core/change-status-policy.d.ts.map +1 -1
- package/dist/core/change-status-policy.js +3 -1
- package/dist/core/change-status-policy.js.map +1 -1
- package/dist/core/codex-metrics.d.ts +1 -1
- package/dist/core/codex-metrics.js +1 -1
- package/dist/core/config-schema.d.ts +8 -0
- package/dist/core/config-schema.d.ts.map +1 -1
- package/dist/core/config-schema.js +10 -0
- package/dist/core/config-schema.js.map +1 -1
- package/dist/core/dashboard-activity.d.ts +4 -4
- package/dist/core/dashboard-activity.js +4 -4
- package/dist/core/dashboard-data.d.ts +11 -5
- package/dist/core/dashboard-data.d.ts.map +1 -1
- package/dist/core/dashboard-data.js +54 -28
- package/dist/core/dashboard-data.js.map +1 -1
- package/dist/core/dashboard-hub.d.ts +3 -3
- package/dist/core/dashboard-hub.d.ts.map +1 -1
- package/dist/core/dashboard-hub.js +16 -4
- package/dist/core/dashboard-hub.js.map +1 -1
- package/dist/core/dashboard-routes.d.ts +2 -1
- package/dist/core/dashboard-routes.d.ts.map +1 -1
- package/dist/core/dashboard-routes.js +102 -33
- package/dist/core/dashboard-routes.js.map +1 -1
- package/dist/core/global-config.d.ts +71 -2
- package/dist/core/global-config.d.ts.map +1 -1
- package/dist/core/global-config.js +149 -4
- package/dist/core/global-config.js.map +1 -1
- package/dist/core/id.d.ts +9 -0
- package/dist/core/id.d.ts.map +1 -1
- package/dist/core/id.js +37 -0
- package/dist/core/id.js.map +1 -1
- package/dist/core/init.d.ts +9 -2
- package/dist/core/init.d.ts.map +1 -1
- package/dist/core/init.js +116 -33
- package/dist/core/init.js.map +1 -1
- package/dist/core/list.d.ts.map +1 -1
- package/dist/core/list.js +69 -45
- package/dist/core/list.js.map +1 -1
- package/dist/core/local-server.d.ts +9 -2
- package/dist/core/local-server.d.ts.map +1 -1
- package/dist/core/local-server.js +24 -4
- package/dist/core/local-server.js.map +1 -1
- package/dist/core/markdown-render.d.ts.map +1 -1
- package/dist/core/markdown-render.js +7 -2
- package/dist/core/markdown-render.js.map +1 -1
- package/dist/core/migrate.d.ts +24 -2
- package/dist/core/migrate.d.ts.map +1 -1
- package/dist/core/migrate.js +335 -59
- package/dist/core/migrate.js.map +1 -1
- package/dist/core/parsers/change-parser.d.ts +20 -1
- package/dist/core/parsers/change-parser.d.ts.map +1 -1
- package/dist/core/parsers/change-parser.js +76 -73
- package/dist/core/parsers/change-parser.js.map +1 -1
- package/dist/core/parsers/markdown-parser.d.ts +24 -0
- package/dist/core/parsers/markdown-parser.d.ts.map +1 -1
- package/dist/core/parsers/markdown-parser.js +65 -10
- package/dist/core/parsers/markdown-parser.js.map +1 -1
- package/dist/core/parsers/requirement-blocks.d.ts +127 -6
- package/dist/core/parsers/requirement-blocks.d.ts.map +1 -1
- package/dist/core/parsers/requirement-blocks.js +349 -56
- package/dist/core/parsers/requirement-blocks.js.map +1 -1
- package/dist/core/parsers/requirement-text.d.ts +25 -1
- package/dist/core/parsers/requirement-text.d.ts.map +1 -1
- package/dist/core/parsers/requirement-text.js +62 -6
- package/dist/core/parsers/requirement-text.js.map +1 -1
- package/dist/core/parsers/spec-structure.d.ts.map +1 -1
- package/dist/core/parsers/spec-structure.js +36 -24
- package/dist/core/parsers/spec-structure.js.map +1 -1
- package/dist/core/project-config.d.ts +9 -2
- package/dist/core/project-config.d.ts.map +1 -1
- package/dist/core/project-config.js +30 -7
- package/dist/core/project-config.js.map +1 -1
- package/dist/core/project-layout.d.ts +26 -0
- package/dist/core/project-layout.d.ts.map +1 -1
- package/dist/core/project-layout.js +51 -1
- package/dist/core/project-layout.js.map +1 -1
- package/dist/core/schema-names.d.ts +17 -0
- package/dist/core/schema-names.d.ts.map +1 -1
- package/dist/core/schema-names.js +24 -0
- package/dist/core/schema-names.js.map +1 -1
- package/dist/core/shared/index.d.ts +1 -1
- package/dist/core/shared/index.d.ts.map +1 -1
- package/dist/core/shared/index.js +1 -1
- package/dist/core/shared/index.js.map +1 -1
- package/dist/core/shared/rules-generation.d.ts +12 -0
- package/dist/core/shared/rules-generation.d.ts.map +1 -1
- package/dist/core/shared/rules-generation.js +26 -2
- package/dist/core/shared/rules-generation.js.map +1 -1
- package/dist/core/shared/skill-generation.d.ts +7 -0
- package/dist/core/shared/skill-generation.d.ts.map +1 -1
- package/dist/core/shared/skill-generation.js +19 -0
- package/dist/core/shared/skill-generation.js.map +1 -1
- package/dist/core/shared/tool-detection.d.ts +10 -1
- package/dist/core/shared/tool-detection.d.ts.map +1 -1
- package/dist/core/shared/tool-detection.js +37 -6
- package/dist/core/shared/tool-detection.js.map +1 -1
- package/dist/core/spec-presenter.d.ts.map +1 -1
- package/dist/core/spec-presenter.js +23 -1
- package/dist/core/spec-presenter.js.map +1 -1
- package/dist/core/specs-apply.d.ts +98 -1
- package/dist/core/specs-apply.d.ts.map +1 -1
- package/dist/core/specs-apply.js +426 -63
- package/dist/core/specs-apply.js.map +1 -1
- package/dist/core/templates/fragments/change-diff.d.ts +37 -0
- package/dist/core/templates/fragments/change-diff.d.ts.map +1 -0
- package/dist/core/templates/fragments/change-diff.js +37 -0
- package/dist/core/templates/fragments/change-diff.js.map +1 -0
- package/dist/core/templates/fragments/sync-fail.d.ts +8 -0
- package/dist/core/templates/fragments/sync-fail.d.ts.map +1 -0
- package/dist/core/templates/fragments/sync-fail.js +8 -0
- package/dist/core/templates/fragments/sync-fail.js.map +1 -0
- package/dist/core/templates/fragments/validate.d.ts +14 -1
- package/dist/core/templates/fragments/validate.d.ts.map +1 -1
- package/dist/core/templates/fragments/validate.js +14 -1
- package/dist/core/templates/fragments/validate.js.map +1 -1
- package/dist/core/templates/fragments/verify.d.ts +2 -2
- package/dist/core/templates/fragments/verify.d.ts.map +1 -1
- package/dist/core/templates/fragments/verify.js +8 -5
- package/dist/core/templates/fragments/verify.js.map +1 -1
- package/dist/core/templates/workflows/apply.d.ts +1 -1
- package/dist/core/templates/workflows/apply.d.ts.map +1 -1
- package/dist/core/templates/workflows/apply.js +29 -10
- package/dist/core/templates/workflows/apply.js.map +1 -1
- package/dist/core/templates/workflows/archive.d.ts.map +1 -1
- package/dist/core/templates/workflows/archive.js +27 -9
- package/dist/core/templates/workflows/archive.js.map +1 -1
- package/dist/core/templates/workflows/decision.d.ts.map +1 -1
- package/dist/core/templates/workflows/decision.js +10 -4
- package/dist/core/templates/workflows/decision.js.map +1 -1
- package/dist/core/templates/workflows/explore.d.ts +2 -2
- package/dist/core/templates/workflows/explore.d.ts.map +1 -1
- package/dist/core/templates/workflows/explore.js +7 -3
- package/dist/core/templates/workflows/explore.js.map +1 -1
- package/dist/core/templates/workflows/grill.d.ts +2 -2
- package/dist/core/templates/workflows/grill.d.ts.map +1 -1
- package/dist/core/templates/workflows/grill.js +5 -4
- package/dist/core/templates/workflows/grill.js.map +1 -1
- package/dist/core/templates/workflows/issue.d.ts.map +1 -1
- package/dist/core/templates/workflows/issue.js +9 -3
- package/dist/core/templates/workflows/issue.js.map +1 -1
- package/dist/core/templates/workflows/propose.d.ts.map +1 -1
- package/dist/core/templates/workflows/propose.js +4 -1
- package/dist/core/templates/workflows/propose.js.map +1 -1
- package/dist/core/templates/workflows/sync.d.ts.map +1 -1
- package/dist/core/templates/workflows/sync.js +25 -11
- package/dist/core/templates/workflows/sync.js.map +1 -1
- package/dist/core/templates/workflows/update.d.ts.map +1 -1
- package/dist/core/templates/workflows/update.js +17 -10
- package/dist/core/templates/workflows/update.js.map +1 -1
- package/dist/core/update.d.ts +5 -0
- package/dist/core/update.d.ts.map +1 -1
- package/dist/core/update.js +66 -29
- package/dist/core/update.js.map +1 -1
- package/dist/core/validation/constants.d.ts +7 -1
- package/dist/core/validation/constants.d.ts.map +1 -1
- package/dist/core/validation/constants.js +16 -2
- package/dist/core/validation/constants.js.map +1 -1
- package/dist/core/validation/purpose-placeholder.d.ts.map +1 -1
- package/dist/core/validation/purpose-placeholder.js +35 -11
- package/dist/core/validation/purpose-placeholder.js.map +1 -1
- package/dist/core/validation/section-validator.d.ts +7 -0
- package/dist/core/validation/section-validator.d.ts.map +1 -1
- package/dist/core/validation/section-validator.js +22 -2
- package/dist/core/validation/section-validator.js.map +1 -1
- package/dist/core/validation/sister-changes.d.ts +93 -0
- package/dist/core/validation/sister-changes.d.ts.map +1 -0
- package/dist/core/validation/sister-changes.js +180 -0
- package/dist/core/validation/sister-changes.js.map +1 -0
- package/dist/core/validation/task-markers.d.ts +9 -0
- package/dist/core/validation/task-markers.d.ts.map +1 -0
- package/dist/core/validation/task-markers.js +71 -0
- package/dist/core/validation/task-markers.js.map +1 -0
- package/dist/core/validation/types.d.ts +58 -0
- package/dist/core/validation/types.d.ts.map +1 -1
- package/dist/core/validation/types.js +22 -0
- package/dist/core/validation/types.js.map +1 -1
- package/dist/core/validation/validator.d.ts +79 -2
- package/dist/core/validation/validator.d.ts.map +1 -1
- package/dist/core/validation/validator.js +565 -84
- package/dist/core/validation/validator.js.map +1 -1
- package/dist/utils/change-metadata.d.ts +62 -5
- package/dist/utils/change-metadata.d.ts.map +1 -1
- package/dist/utils/change-metadata.js +193 -31
- package/dist/utils/change-metadata.js.map +1 -1
- package/dist/utils/change-utils.d.ts +21 -0
- package/dist/utils/change-utils.d.ts.map +1 -1
- package/dist/utils/change-utils.js +168 -53
- package/dist/utils/change-utils.js.map +1 -1
- package/dist/utils/command-line.d.ts +11 -0
- package/dist/utils/command-line.d.ts.map +1 -0
- package/dist/utils/command-line.js +43 -0
- package/dist/utils/command-line.js.map +1 -0
- package/dist/utils/display-width.d.ts +10 -0
- package/dist/utils/display-width.d.ts.map +1 -0
- package/dist/utils/display-width.js +15 -0
- package/dist/utils/display-width.js.map +1 -0
- package/dist/utils/file-system.d.ts +16 -0
- package/dist/utils/file-system.d.ts.map +1 -1
- package/dist/utils/file-system.js +49 -2
- package/dist/utils/file-system.js.map +1 -1
- package/dist/utils/frontmatter.d.ts.map +1 -1
- package/dist/utils/frontmatter.js +11 -5
- package/dist/utils/frontmatter.js.map +1 -1
- package/dist/utils/interactive.d.ts.map +1 -1
- package/dist/utils/interactive.js +3 -1
- package/dist/utils/interactive.js.map +1 -1
- package/dist/utils/item-discovery.d.ts +3 -2
- package/dist/utils/item-discovery.d.ts.map +1 -1
- package/dist/utils/item-discovery.js +34 -16
- package/dist/utils/item-discovery.js.map +1 -1
- package/dist/utils/link.d.ts +0 -1
- package/dist/utils/link.d.ts.map +1 -1
- package/dist/utils/link.js +0 -1
- package/dist/utils/link.js.map +1 -1
- package/dist/utils/match.d.ts.map +1 -1
- package/dist/utils/match.js +10 -1
- package/dist/utils/match.js.map +1 -1
- package/dist/utils/option-flags.d.ts +9 -0
- package/dist/utils/option-flags.d.ts.map +1 -0
- package/dist/utils/option-flags.js +12 -0
- package/dist/utils/option-flags.js.map +1 -0
- package/dist/utils/sentence.d.ts +9 -0
- package/dist/utils/sentence.d.ts.map +1 -0
- package/dist/utils/sentence.js +12 -0
- package/dist/utils/sentence.js.map +1 -0
- package/dist/utils/spec-files.d.ts +84 -1
- package/dist/utils/spec-files.d.ts.map +1 -1
- package/dist/utils/spec-files.js +192 -3
- package/dist/utils/spec-files.js.map +1 -1
- package/dist/utils/task-progress.d.ts.map +1 -1
- package/dist/utils/task-progress.js +32 -9
- package/dist/utils/task-progress.js.map +1 -1
- package/dist/utils/timestamp.d.ts +27 -0
- package/dist/utils/timestamp.d.ts.map +1 -1
- package/dist/utils/timestamp.js +37 -0
- package/dist/utils/timestamp.js.map +1 -1
- package/package.json +4 -1
- package/schemas/decision/templates/decision.md +2 -2
- package/schemas/issue/schema.yaml +12 -6
- package/schemas/issue/templates/task.md +1 -1
- package/schemas/sdd/schema.yaml +21 -10
- package/schemas/sdd/templates/proposal.md +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,247 @@
|
|
|
6
6
|
|
|
7
7
|
tospec 是一套 spec-driven development CLI: 以 schema 定義文件結構與工作流程, 進度由檔案系統狀態推算, 開發方法則封裝於 Skill 之中, 使 AI 工具 (Claude Code / Codex) 得以循序完成需求釐清、規格撰寫、設計、任務拆解、實作到歸檔的完整流程.
|
|
8
8
|
|
|
9
|
+
## [0.26.0] - 2026-10-23
|
|
10
|
+
|
|
11
|
+
本版本的主題是姊妹 change (sister change): 同時進行中、碰到同一個 capability 或 requirement 的其他 change. 先在一個不是 git repository 的專案以 0.25.0 跑完整流程 (explore、decision、propose、apply、archive 前再開第二個 change、含 sync 的 archive、issue), 之後在同一個專案做了七輪端對端測試, 每一輪驗證上一輪的修正, 最後一輪沒有發現需要修正的問題. 找到的缺陷大多出自同一個根本原因: validate 只拿一個 change 對照 `tospec/specs/`, archive 的結果卻取決於其他尚未 archive 的 change. 因此 validate、`instructions apply` 與 archive 現在讀取姊妹 change, 並指出要先 archive 哪一個. 接著把每一種由 archive 順序或 requirement 名稱造成的 finding 都改為結構化欄位 (`code`、`blockedBy`、`suggestedHeader`、`archiveRefusals`), agent 不再比對 message 文字. 其餘是 skill 與 workflow 的修正: ADR 的連結與狀態、`allowed-tools` 涵蓋子步驟、issue 的 planning boundary、非 git 專案的 diff 範圍、regression test 的規則. breaking change 集中在「變更」.
|
|
12
|
+
|
|
13
|
+
本版先在 2026-10-18 以 0.26.0-beta 發布到 npm 的 `beta` tag, 正式版的內容與它相同. 0.25.0 沒有發布到 npm, 所以 npm 的 `latest` 從 0.24.0 直接升到 0.26.0; 從 0.24.0 升級時也要讀 0.25.0 的條目, 它有多項 breaking change.
|
|
14
|
+
|
|
15
|
+
### 新增
|
|
16
|
+
|
|
17
|
+
- **validate 與 `instructions apply` 讀取姊妹 change, 指出要先 archive 哪一個**: change A 以 ADDED 建立 capability 且尚未 archive, change B 對同一個 capability 寫 MODIFIED. validate B 只回報 `Archive would refuse this delta: ... target spec does not exist`, `instructions apply --change B` 則回報 `ready` 且沒有任何警告, agent 實作完才在 archive 失敗. 根本原因有兩個: validate 只讀這個 change 與 `tospec/specs/`, 不知道 A 存在; `instructions apply` 只依檔案位置判定 readiness, 從不執行 merge 的前置條件. 現在 merge 以型別 `MissingTargetSpecError` 拋出「target spec does not exist」, validate 依型別辨識而不比對文字, 經由新的 `listSisterChangeDeltas` 讀取其他進行中的 change, 並在 message 加上 `Archive 'A' first: it adds this capability.` (多個時為 `Archive 'a', 'b' first: each adds this capability.`).
|
|
18
|
+
- `instructions apply` 執行 validate 的同一個 dry run (`findArchiveRefusals`), 每個 archive 會拒絕的結果是一筆 `apply_archive_would_refuse` WARNING, message 沿用 validate 的文字, 含 `fix`. `state` 為 `blocked` 時也回報, exit code 維持 0, `all_done` 的文字也涵蓋這個 code. apply skill 遇到它時停下並回報, 不自行修改 delta. 沒有在 `instructions.ts` 另寫一份前置條件清單: 兩份清單之間的落差正是這個缺陷.
|
|
19
|
+
- 「archive 會接受但什麼都不移除」的 WARNING 不回報, 因為 archive 不會因此停下. merge 延後處理的 delta 結構 ERROR (重複的 header、MODIFIED 遺失 scenario) 不在 dry run 內, apply 仍看不到, 只有 validate 回報.
|
|
20
|
+
- requirement 層級也適用: main spec 存在, 但 MODIFIED 或 RENAMED 指向的 requirement 只在姊妹 change 的 ADDED 中時, merge 拋出新的 `MissingTargetRequirementError`, 拒絕附上 `Archive '<x>' first: it adds this requirement.`. 提示以 `appendSentence` 接在 merge 的訊息之後, 前文沒有以 `.`、`!`、`?` 結尾時才補 `. `, 因為 requirement 層級的訊息以 `- not found` 結尾, 而 capability 層級的訊息已有句號. merge 本身的訊息不變, archive 也印出它們.
|
|
21
|
+
- 同一個原因只回報一次: capability 沒有 main spec, 且 archive 拒絕已指名姊妹 change 時, 下一條的重疊掃描不計入那個姊妹 change 的 ADDED. main spec 存在、requirement 由姊妹 change ADDED 時, 重疊掃描是唯一的提示, 照常回報.
|
|
22
|
+
- capability 名稱以完全相同比對, 資料夾名稱只差大小寫時不會得到提示.
|
|
23
|
+
- **改寫同一個 requirement 的兩個 change 互相警告**: change B 的 MODIFIED 從 change A 的 delta 複製. archive A 時 sync 判定 SPEC-UPDATED, 只改寫 requirement 的敘述句, 沒有改任何 bullet. 之後 validate B 回報 `valid: true`, archive B 以整個區塊取代 main spec 的內容, sync 補上的內容消失, 沒有任何命令回報. 根本原因是 MODIFIED 一律整段取代, 而既有的兩道防護 (遺失 scenario 是 ERROR、遺失 bullet 是 WARNING) 都刻意不比對敘述句, 因為重新排版段落會改變每一行. 現在 validate 與 archive 都檢查姊妹 change.
|
|
24
|
+
- validate: 這個 change 的 MODIFIED / REMOVED / RENAMED 與另一個進行中的 change 碰到同一個 requirement 時, 發出 `code: "sister_change_overlap"` 的 WARNING. RENAMED 以 FROM 名稱比對, requirement 名稱不分大小寫與空白, capability 名稱完全相同比對.
|
|
25
|
+
- 兩邊都是改寫時, 依 `created` 排序, 較早建立的先 archive. 較晚的一方收到 `Archive '<older>' first (it was created before this change), then rewrite this delta ...`, 較早的一方收到 `This change was created before '<newer>', so archive this change first; ...`, 無法排序時雙方收到同一句中立的文字. 只有姊妹 change 是 ADDED 時, 順序才由內容決定 (MODIFIED 需要 requirement 已經存在); 若對每個命中的姊妹 change 都寫 `Archive '<sister>' first`, 兩個 change 會互相叫對方先 archive. 排序需要時間, 見「變更」的 `created`.
|
|
26
|
+
- archive: merge 寫入後, 對其他進行中 change 中改寫這次剛改過的 requirement 的 delta, 發出 `archive_sister_change_stale` (含 `target` 與 `fix`), 不阻擋 archive. message 只陳述比對能證明的事: 對方改寫了這次剛改過的 requirement, 照原樣 archive 會以它的文字取代這次的修改. message 不斷言對方依據哪一版文字撰寫, 因為兩者之間可能還有第三個 change 已經 archive. archive 自己的 validation 不讀姊妹 change, 以免在 archive A 時建議先 archive B.
|
|
27
|
+
- 沒有採用比對敘述句 (每個刻意改寫敘述句的 MODIFIED 都會觸發, `--strict` 會擋下)、記錄 base hash (tospec 不保存狀態, delta 又是 agent 手寫, 寫入時機不可靠) 或讀取 git 歷史 (不是 git repository 時無效). 已知的代價: A archive 之後, validate B 不再警告, archive A 時的 `archive_sister_change_stale` 是唯一的提示. A 進行中時, `validate --strict B` 會失敗.
|
|
28
|
+
- **validate 與 apply 以欄位標示 archive 順序與 requirement 名稱造成的 finding**: validate 對 archive 會拒絕的 delta 刻意回報 WARNING, 因為姊妹 change 尚未 archive 時, 這個 change 只是太早, 不是錯誤, 而 ERROR 會讓 propose 無法通過. 但 `--json` 沒有欄位表示「archive 會拒絕」: 只看 `valid` 或 exit code 的 agent 漏掉它, 比對 message 的 agent 在措辭改變時失效. 只有 code 仍不夠, 因為「delta 寫錯」與「archive 順序不對」需要相反的處理, 而 agent 為了消除 WARNING 刪掉正確的 MODIFIED. 現在每一種 finding 的處理方式都由欄位決定.
|
|
29
|
+
- `code: "archive_would_refuse"`: dry run 判定會拒絕的 finding 帶這個 code, `items[]`、`summary.totals` 與 `summary.byType.*` 都有 `archiveRefusals` 計數 (spec 類型恆為 0, 讓兩種類型的形狀一致). level、exit code 與 human 模式不變. code 加在 validate 收集 finding 的位置, 不在 dry run 內, 所以 apply 的輸出不受影響. 這是第一個帶 code 的 validation issue.
|
|
30
|
+
- `blockedBy: string[]`: 只因 archive 順序產生的 finding 列出必須先 archive 的 change. 值取自產生 `Archive '<x>' first` 句子的同一份清單或同一次排序, 欄位與文字不會分歧. 出現在帶這個提示的 `archive_would_refuse`、`sister_change_overlap` 中應等待的一方 (較早建立或無法排序的一方沒有), 以及 apply 的 `apply_archive_would_refuse` (複製 validate 的值). apply 的 fix 依它而定: 有 `blockedBy` 時是 `Archive '<x>' first, then run apply again; no edit to this delta resolves it.`, 沒有時指向 planning skill.
|
|
31
|
+
- `code: "requirement_header_mismatch"` (ERROR, 帶 `suggestedHeader`): MODIFIED / REMOVED / RENAMED 的名稱與姊妹 change 的 ADDED 只差大小寫或空白. 姊妹掃描若以摺疊後的名稱比對, 會給這種名稱 `blockedBy`, 但 merge 要求完全相同的名稱, 姊妹 change archive 後仍以 near-miss 拒絕, 依規則等待的 agent 等不到結果. 現在 `addingChanges` 改用 merge 的 `normalizeRequirementName`, 摺疊後相同但不完全相同的情況是一個 ERROR, 因為不論 archive 順序為何, delta 都無法套用. 同一個 requirement 的 `archive_would_refuse` 不帶提示與 `blockedBy`, 改帶 `suggestedHeader`; 這一對不回報 `sister_change_overlap`, 因為它的建議只在修正名稱後才成立. main spec 已有的名稱不在此列, 由下一條處理.
|
|
32
|
+
- RENAMED 的名稱寫在 `- FROM:` bullet, 不是 header. 所以 RENAMED 的 `requirement_header_mismatch` 以 `line` 指向 FROM 行, message 與 apply 的 fix 說要改 FROM 行; 兩者共用 `headerCorrectionClause`, 不會分歧. parser 為此新增 `renamedEntries` (含 `fromLine`), 與 `removedEntries` 的分法相同. 沒有擴充 `renamed` 的 `{from, to}`, 因為許多呼叫端以完全相同比對這個形狀.
|
|
33
|
+
- main spec 的 near-miss: 名稱與 main spec 既有的 requirement 只差大小寫或空白時, 正確名稱原本只出現在 merge 的句子中. 現在 merge 拋出 `RequirementNearMissError`, dry run 把 main spec 的名稱複製到 `suggestedHeader`, validate 從結構化資料取得名稱, 不解析 archive 也會印出的訊息. 等級仍是 WARNING、`valid: true`, agent 不詢問使用者就修正, 因為 main spec 的名稱已經確定. apply 的 fix 不帶 archive 順序的句子.
|
|
34
|
+
- skill 讀取這些欄位的規則見「變更」的 `VALIDATE_VERDICT_RULE`.
|
|
35
|
+
- **ADR 可以連結到既有的 change, 並在 change 完成後改變狀態**: issue 流程先建立 change 再決定修正方向, 但 `--decisions` 只存在於 `new change`, 使用者只能手動編輯 `.tospec.yaml`. 手動加入的值不經過任何檢查, archive 會寫出指向不存在檔案的連結. 另外, ADR 只能在建立時設定 status, explore 與 grill 一律記錄為 `proposed`, 而 propose 與 explore 只讀 `accepted` 的 ADR, 所以自動記錄的決策之後的規劃都讀不到.
|
|
36
|
+
- `tospec decision new --change <name>`: 寫入 ADR 前確認 change 存在、尚未 archive、`.tospec.yaml` 可讀 (新 code `decision_change_not_found`、`decision_change_archived`), 再把檔名加進 `decisions:`. 直接編輯 YAML document, 保留註解、`x-` key、BOM 與 CRLF; 沒有使用 `writeChangeMetadata`, 因為它重新序列化 parse 後的物件, 會丟掉註解與 `x-` key. `--force` 不加入重複的值. grill 與 decision skill 對既有 change 改用 `--change`. 沒有新增 `decision link`: 連結既有 ADR 時手動編輯, 由 validate 的檢查抓錯.
|
|
37
|
+
- validate 對 `decisions:` 執行與 `new change` 相同的檢查 (ADR 檔名格式、檔案存在, 共用 `findDecisionReferenceProblems`), 等級為 WARNING, 因為 archive 對這個欄位的使用本來就不阻擋. `--strict` 會擋下.
|
|
38
|
+
- `tospec decision status <file> <status>` (`proposed` / `accepted` / `superseded`): 只改寫 `## Status` bullet 中的 status 字, 保留該行其餘內容與換行字元, 寫入前讀回確認, 再以 `decision list --reindex` 的流程重建 `index.md`. 檔案已是該 status 時也修復過時的 index. decision skill 取代舊 ADR 時改用它.
|
|
39
|
+
- archive skill 新增 Step 3: archive 前從 `status --change --json` 的 `changeMetadata.decisions` 讀取連結的 ADR, archive 成功後以 `decision list --status proposed --json` 找出其中仍是 `proposed` 的, 一次詢問使用者哪些改為 `accepted`, 不自行判斷.
|
|
40
|
+
- `decision list` 對檔案已刪除的 index 列只說有幾列, 沒有說怎麼處理. reindex 刻意保留這些列 (ledger 是歷史紀錄), 所以警告改為詢問刪除是否刻意: 是就手動移除 `index.md` 的這些列, 否則還原檔案. `--json` 在成功外殼的 `status[]` 帶 `decision_index_dangling` (含 `target` 與 `fix`), exit 0. 沒有新增 prune 命令: 它會刪掉誤刪檔案的唯一紀錄. 同時移除舊文字中不正確的 `Their status reads "unknown"`.
|
|
41
|
+
- **sync 與 VERIFY 在非 git 專案有替代的 diff 範圍**: tospec 不需要 git, `init` 也不檢查, 但 `CHANGE_DIFF` 只有 git 命令, sync 的 REMOVED 規則也要求 `git log -p`, 非 git 專案沒有任何替代做法. 現在 git 回報 `not a git repository` 時, 範圍改為 `tasks.md` / `task.md` 與 `design.md` 提到的檔案, 加上 apply 期間修改的檔案, 每個檔案完整讀取. report 必須註明使用了替代範圍並列出檔案: 範圍外的檔案沒有被掃描, 看起來像完整 diff 的 report 會隱藏這一點.
|
|
42
|
+
- `sync-report.md` 的 `## Summary` 新增固定的 `Diff scope:` 行 (`git diff <base>` 或 `fallback (not a git repository): <files read>`), 每次都寫, 不只在非 git 時. `Conclusion:` 的 parser 不受影響. VERIFY 不寫 report 檔, 範圍記在步驟 5 的彙整報告中.
|
|
43
|
+
- 非 git 時, REMOVED 的行為描述改從 change 的 delta (Reason / Migration) 與 `tospec/changes/archive/` 中先前的 delta 找回, 以最新的 stamp 為準.
|
|
44
|
+
- 限制: sync 常在 apply 以外的 session 執行, 沒有 git 時「apply 期間修改的檔案」不一定能取得, 替代範圍主要來自 tasks 與 design 提到的檔案. `init` 仍不檢查 git.
|
|
45
|
+
- **`tospec-issue` 有 planning boundary**: propose 規定觸發的 request 只授權 planning, issue 沒有對應的規則, 「X 壞了, 修好它」可以從診斷直接做到修正. 當初制定 propose 的 boundary 時調查了 explore、grill、decision、update, 沒有討論 issue, 所以這是遺漏, 不是決定. 現在 issue 使用 propose 的用語, 但授權的範圍是診斷: 允許的程式碼修改只有 feedback loop 的 failing test 與暫時的 `[DEBUG-xxxx]` log, 呈現 ticket 與 `task.md` 後等待新的 request 才開始 `tospec-apply`. 沒有照搬 propose 的「不修改程式碼」, 那會與步驟 3 的 red test 矛盾. 步驟 4 呈現假設清單後繼續驗證, 不等回覆; 使用者提供的資訊改變排序時才重新排序.
|
|
46
|
+
|
|
47
|
+
### 變更
|
|
48
|
+
|
|
49
|
+
- **`created` 改為寫入本地日期時間, 並拒絕不存在的日期 (breaking change)**: 依 `created` 排序姊妹 change 時, 只有日期無法排序同一天建立的兩個 change. 現在 `new change` 寫入不含時區的本地時間 `YYYY-MM-DDTHH:mm:ss`, schema 仍接受舊的 `YYYY-MM-DD`. `yaml` 的 core schema 沒有 timestamp 型別, 未加引號的值讀回仍是字串. 讀取 `.tospec.yaml` 或 `changeMetadata.created` 並假設只有日期的呼叫端需要調整. 0.25.0 以前的 tospec 只接受日期, 會把 0.26.0 建立的 change 判定為讀不到, 所以同一個專案的使用者應一起升級.
|
|
50
|
+
- 舊的 date-only 值: 只在 `.tospec.yaml` 的 birthtime 本地日期與 `created` 相同時, 以 birthtime 補上時間, 因為 clone、checkout 與複製都會重設 birthtime. 其他情況視為無法排序, 雙方收到中立的文字, 不猜測. 取代整個檔案的寫入 (`sed -i`、編輯器的 atomic save) 也會重設 birthtime, 同一天內發生時, 兩個 date-only change 的排序可能顛倒; tospec 自己的寫入 (包含 `decision new --change`) 不會. migrate 對缺少 `created` 的 change 填入今天的日期, 這些 change 依 migrate 的寫入順序排序, 不是真正的建立順序.
|
|
51
|
+
- schema 原本只以 regex 檢查 `created`, `2026-02-30` 與 `T25:00:00` 都通過 validate. 姊妹排序的 `parseCreated` 會拒絕這種值, 但只在那裡拒絕時, 錯誤值只會靜默成為「無法排序」, 不說明原因. 現在 schema 在 regex 之後再以 `parseCreated` 檢查, 會溢位的值被拒絕; 格式錯誤只回報格式的 message, 不重複回報. 每個讀取端都經過 `readChangeMetadata` 的 `safeParse`, 所以 status、list、instructions、archive 與 dashboard 對這種值的降級方式與 `created: bad` 相同, 而不是只有 validate 多一項其他命令略過的檢查. 已有這種值的 change 會改為讀不到. DST 跳過的本地時間也被拒絕, `new change` 不會寫出這種值.
|
|
52
|
+
- migrate 的 `dayStamp` 與 dashboard 的 `parseLooseDate` 讀取新格式. snapshot 的欄位仍是字串, `DASHBOARD_PROTOCOL` 不變.
|
|
53
|
+
- **REMOVED 指向沒有 main spec 的 capability 時, archive 拒絕 (breaking change)**: 只有 REMOVED 的 delta 指向姊妹 change 才 ADDED 的 capability 時, merge 繼續重建出空的 spec, 再以 validation 失敗拒絕. 這個拒絕不是 `MissingTargetSpecError`, 所以 Purpose WARNING 照常出現, `instructions apply` 也不指名 ADDED 該 capability 的 change. REMOVED 與 ADDED 並存時, archive 成功並丟掉 REMOVED, 只留下 `archive_removed_ignored_new_spec` notice. REMOVED 指名的是作者預期存在的 requirement, 所以現在與 MODIFIED、RENAMED 一樣拋出 `MissingTargetSpecError`, message 改為 `... MODIFIED, REMOVED and RENAMED operations require an existing spec.`. Purpose 的略過、重複回報的略過與 `Archive '<change>' first` 提示因此都涵蓋 REMOVED, 不需另加特例. `archive_removed_ignored_new_spec` 移除.
|
|
54
|
+
- 沒有 main spec 時的 ADDED + REMOVED 原本 archive 成功, 現在以 `archive_spec_update_failed` 拒絕, 與既有的 ADDED + MODIFIED 一致. 這種 REMOVED 只可能指向姊妹 change 的 ADDED (這個 change 先 archive, removal 就會遺失) 或錯誤的 capability. 沒有採用讓每條規則各自認得 REMOVED: 那樣仍會接受遺失 removal 的 delta.
|
|
55
|
+
- **讀不到的 change 一律回報 `change_unreadable` (breaking change)**: `.tospec.yaml` 無法載入時, `status --change`、`instructions <artifact> --change` 與 `instructions apply` 落到 CLI 的 fallback `change_error`, 只有 message, 沒有 `target` 與 `fix`. `status --all` 對同一個 change 在頂層 `status[]` 回報 `change_unreadable`, 那個 change 自己那筆卻是 `change_error`, 一個命令對同一個失敗回報兩種 code. 現在 `workflow/shared.ts` 的 `changeUnreadable` builder 供所有讀取端使用, `target` 是 change 目錄, `fix` 依錯誤來源指向正確的檔案, `status --all` 那筆與頂層的內容相同. code 改為 `change_unreadable`, 因為 agent contract 以它表示讀不到的 change, 沒有 skill 讀取這條路徑的 `change_error`, 已處理 `--all` 的 agent 可以用同一條規則處理 `--change`. 只有 change 的載入在包裝範圍內, change 不存在、`--schema` 錯誤、ticket 查詢失敗與之後的 template 載入失敗保留原本的 code. 比對 `change_error` 的呼叫端需要調整.
|
|
56
|
+
- **`archive --json` 帶出 validation 的 WARNING (breaking change)**: human 模式印出 archive 自己執行的 validation 的每個 finding, `--json` 卻一個都不輸出, 以 `--json` archive 的 agent 看不到使用者在終端機看到的遺失 bullet 或缺少 SHALL. 現在每個 WARNING 是 `status[]` 中的一筆 `archive_validation_warning`. ERROR 不放入, 因為它讓後面的 gate 失敗, 而那個 gate 的 fix 指向 validate. 沒有警告的成功仍不帶 `status`. 這些 warning 排在失敗項目之前, 失敗項目一律在最後, 所以以 `status[0]` 讀取失敗原因的呼叫端要改讀 `severity: "error"` 的那一筆 (archive skill 自 0.25.0 起就這樣讀). Purpose 相關的 finding 在 `--json` 中出現兩次: validation WARNING 與 merge notice 各一次, human 模式原本就是如此.
|
|
57
|
+
- **`VALIDATE_VERDICT_RULE` 允許第三種 WARNING 保留, 並新增一個不由 agent 自行修正的 ERROR**: propose、apply 的步驟 6、issue、update、sync 與 VERIFY 共用的規則要求修正每個 WARNING, 只有兩種可以保留 (非英文 requirement 的 SHALL/MUST 檢查、已確認是刻意的 finding). archive 順序造成的 finding 無法在這個 change 內修正, agent 卻會為了消除它修改 delta. 現在第三種可保留的是 archive 順序造成的 finding, 不可為它修改 delta: 有 `blockedBy` 時請使用者先 archive 列出的 change; `sister_change_overlap` 沒有 `blockedBy` 時, 把 message 轉告使用者. 規則改為檢查 `archive_would_refuse` 與 `summary.totals.archiveRefusals`, 不比對 message 文字.
|
|
58
|
+
- `requirement_header_mismatch` 是唯一不由 agent 自行修正的 ERROR: 詢問使用者一次是否改成 `suggestedHeader`, 同意就修改並重新 validate, 不同意就停止流程, 並說明 archive 會拒絕這個 change. 需要詢問, 因為可能是 delta 複製錯, 也可能是姊妹 change 的名稱打錯, 只有使用者知道哪一個是要的名稱. 沒有「照原樣繼續」的選項, 因為 merge 要求名稱完全相同, 不改名稱的 change 無法 archive.
|
|
59
|
+
- apply skill 依 `blockedBy` 判斷, 不讀 message. archive skill 的 `archive_spec_update_failed` 說明改為指向 code、計數與 `blockedBy`, 並列出 MODIFIED、REMOVED 或 RENAMED 指向沒有 main spec 的 capability 這種拒絕.
|
|
60
|
+
- **explore 與 grill 可以寫入 `tospec/decisions/`, archive 不再自行接手 apply**: explore 與 grill 在步驟 5 不詢問使用者就以 `tospec-decision` 記錄 ADR, 但 `allowed-tools` 只有唯讀工具, 填寫 ADR 時會跳出權限提示. archive 在 sync 判定 CODE-BUG 時交給 `tospec-apply` 修正, 回來後繼續 sync, 但修正需要執行測試與修改程式碼, 不在 archive 的工具範圍內. 根本原因是加入自動記錄 ADR 的步驟時, 沒有同步更新 `allowed-tools`.
|
|
61
|
+
- explore 與 grill 只加入 `Edit(tospec/decisions/**)`. Claude Code 對每個寫檔的內建工具套用 `Edit` 規則, `Write(path)` 規則會被接受但從不參考, 啟動時還會警告, 所以沒有加 `Write(...)`. 修改原始碼或 artifact 時仍會跳出權限提示, 保留「以工具權限阻擋寫入」的設計. 沒有採用加入不限路徑的 `Write` 與 `Edit`, 那會失去這個設計.
|
|
62
|
+
- archive 的 CODE-BUG 選擇「現在修」時, archive 停下, 請使用者執行 `tospec-apply`, 完成後重新執行 archive; 重新執行時 sync 從頭判定並改寫 `sync-report.md`. archive 的工具不變, 沒有為了這一步擴大權限. sync skill 的步驟 4 不再說 archive 會從那裡繼續.
|
|
63
|
+
- 新的 test 掃描 skill 文字中的 sub-step (explore→decision、grill→decision、archive→sync), 斷言所需工具都在呼叫端的 `allowed-tools` 內, 並斷言沒有 skill 使用帶路徑的 `Write(...)`.
|
|
64
|
+
- **`decision list` 的警告改到 stderr**: 已刪除 ADR 的 index 列的 `Warning:` / `Fix:` 以 `console.log` 印出, `2>/dev/null` 仍看得到, pipe 之後的表格也把它們當成資料列. 現在與其他 human 模式的診斷一樣印到 stderr, 表格仍在 stdout. `--reindex` 摘要行原本以冒號引出這則警告, 改為句號. 從 stdout 擷取這則警告的 script 需要調整. `decision list` 與 `--reindex` 的 fix 最後一句刻意不同: 沒有 `--reindex` 時多提示 reindex 也會回報這些列.
|
|
65
|
+
|
|
66
|
+
### 修正
|
|
67
|
+
|
|
68
|
+
- **MODIFIED 指向姊妹 change 才 ADDED 的 capability 時, Purpose WARNING 不再引向錯誤的修正**: validate 除了 archive 會拒絕的 WARNING, 還回報 `Delta has no ## Purpose. This capability has no main spec yet, ...`. 照著加上 `## Purpose` 後, 姊妹 change 一 archive, 這個 capability 就成為既有 capability, 又觸發 `Delta "## Purpose" will be ignored on archive`. 根本原因是 `collectDeltaPurposeIssues` 只看 main spec 是否存在, 不看 delta 的內容; 而 delta 只要有 MODIFIED、REMOVED 或 RENAMED, archive 就在處理 Purpose 之前拒絕, 所以三則 Purpose WARNING 在這種情況下都沒有意義. 現在這種情況不發出 Purpose WARNING, archive 會拒絕的 WARNING 保留並指名姊妹 change. propose skill 新增規則: capability 只存在於另一個進行中 change 的 ADDED delta 時, 告知使用者先 archive 那個 change, 並依 archive 後的 main spec 撰寫 MODIFIED.
|
|
69
|
+
- **RENAMED 的 FROM 只差大小寫時, merge 指出正確的名稱**: merge 對 MODIFIED 與 REMOVED 有 near-miss 檢查 (`not found, but "### Requirement: X" exists`), RENAMED 沒有. FROM 只差大小寫時落入 `source not found`, validate 因此把它當成可能的姊妹 change 順序問題. 現在 RENAMED 也做相同的檢查, 放在「已同步」的 TO 檢查之後, message 為 `... - source not found, but "### Requirement: X" exists; fix the FROM line to match it exactly`. archive 也印出這段文字.
|
|
70
|
+
- **regression test 的規則不再互相矛盾**: issue skill 的步驟 3 先建立會 red 的 feedback loop, 最常見的形式是一個 failing test. issue 的 task template 卻寫 `1.1 Write a regression test (red)`, 假設測試還不存在; apply 的 issue 分支說 regression test 不是必要的; 預設 apply variant 的共用 guardrail 說 `Tests are optional in this loop`, 與 sdd 分支「tasks.md 列出的測試步驟屬於該 slice」相反. agent 可能依 guardrail 跳過列出的測試步驟.
|
|
71
|
+
- task template 的 1.1 改為 `Regression test is red (the one from diagnosis, or write it now)`. stub detection 在執行時載入 template 比對, 所以升級前建立且未編輯的 `task.md` 不再被判定為 stub.
|
|
72
|
+
- apply 的 issue 分支與共用 guardrail 都改為: 列出的測試步驟屬於工作的一部分, 只有超出的 test 是選擇性的. TDD variant 原本就區分三種情況, 不需修改. skill `description` 的「(tests optional)」是摘要, 而且影響 skill 的觸發, 所以不修改.
|
|
73
|
+
|
|
74
|
+
### 其他
|
|
75
|
+
|
|
76
|
+
- **skill**: 本版的 skill 變更都要重跑 `tospec update` 才會寫進專案.
|
|
77
|
+
- **repo 內的文件** (不在 npm 套件內): `docs/agent-contract.md` 新增 archive 的 `archive_validation_warning` 與 `archive_sister_change_stale`, validate 的 `archive_would_refuse`、`archiveRefusals`、`sister_change_overlap`、`blockedBy`、`requirement_header_mismatch` 與 `suggestedHeader`, 以及 `change_unreadable` 涵蓋的命令. skills guide 與 flow 頁面同步本版的 skill 變更, explore 與 grill 標為新的權限等級「唯讀, 只寫 ADR」.
|
|
78
|
+
- **測試**: 162 個檔案, 2929 個 test.
|
|
79
|
+
|
|
80
|
+
## [0.25.0] - 2026-10-17
|
|
81
|
+
|
|
82
|
+
本版本沒有新的主要功能, 是一次以正確性為主的修正. 先對 0.24.0 之後的分支做 review (同時對照上游 OpenSpec 1.14.2 與 mattpocock/skills 的同類修正), 再交由外部測試組實測, 找到的問題記為 `issues.md` 的 TS-001 至 TS-066. 修掉的缺陷大致分成三種. 第一種是**同一件事由多個讀取端各自判定**: 什麼算 delta 檔、什麼算 section 標題、什麼算 task、目錄 link 算不算目錄, validate、archive、status、show、migrate 與 dashboard 各有一套規則, 結果是 validate 通過的 change 在 archive 時少合併了內容. 第二種是**靜默失敗**: archive 與 migrate 丟掉內容卻 exit 0, `list` 把讀不到的項目回報成空的或正常的, 壞掉的全域 config 被預設值覆寫. 第三種是**交給 agent 的指示與實際行為不符**: fix 訊息指向沒有問題的檔案, skill 的步驟彼此矛盾, 非互動環境下的提示等待一個永遠不會來的輸入. 修正方向一致: 把判定集中成一份由所有讀取端共用, 並讓每一份被丟掉的內容都有一則診斷. 因此本版有多項 breaking change, 集中在「變更」.
|
|
83
|
+
|
|
84
|
+
### 新增
|
|
85
|
+
|
|
86
|
+
- **validate 與 archive 點名不會被合併的 delta 內容**: 寫在第一個 `##` 之前的 requirement、拼錯標題 (`## Add Requirements`、`## REMOVED Requirement`) 底下的內容、寫成 `specs/payments.md` 而不是 `specs/payments/spec.md` 的 delta 檔, validate 都通過, archive 也 exit 0, 但這些內容從未合併進 main spec. 根本原因是每個讀取端都只依 delta section 與 `specs/<capability>/spec.md` 讀取, section 以外與位置錯誤的內容從來沒有被看過. 現在 `parseDeltaSpec` 另外回傳 orphan requirement、未合併的 section 與拼錯的 section (與 delta 標題相差不超過 3 個 edit, 不分大小寫; delta 標題都至少 17 個字元, `## Notes` 這類標題至少差 14 個 edit), validate 逐一以 WARNING 寫出行號與建議的標題, archive 以 `archive_delta_content_not_merged` 列出. 採用 WARNING 而不是 ERROR, 是因為掃描分不出放錯位置的 requirement 與引用標題的說明文字. 沒有採用關鍵字判斷 (標題含 `removed` 就警告), 那會標到 `## Removed endpoints` 這類說明標題.
|
|
87
|
+
- `specs/` 根目錄含 delta 標題的 `.md` 在 validate 是 WARNING, 在 archive 是 `archive_root_delta_ignored`, `--skip-specs` 分支也發出同一個 code. 沒有採用上游的 ERROR: validate 與 archive 都只警告, 任一邊才不會拒絕另一邊接受的檔案. 沒有 delta 標題的筆記照舊忽略.
|
|
88
|
+
- 巢狀的 `specs/a/b/spec.md`、dot 目錄的 `specs/.hidden/spec.md`、檔名錯誤的 `specs/a/notes.md`, archive 以新的 `archive_misplaced_delta_ignored` 回報. 這個 code 依檔名選檔, `archive_root_delta_ignored` 依 delta 標題選檔; 分成兩個 code, JSON 呼叫端才能區分兩種檢查.
|
|
89
|
+
- ADDED / MODIFIED 區塊內有內文的 `#` 標題 (例如 `# Note`) 也以 `archive_delta_content_not_merged` 點名, 見「修正」的 delta 區塊終點.
|
|
90
|
+
- 訊息只描述狀況, 處理方式放在 `fix`, 並分別說明 archive 中止與已完成兩種情況. archive 完成後 change 已移到 `archive/`, 「搬檔後重跑」不可行.
|
|
91
|
+
- **apply 與 status 在動手前回報 validate 與 archive 會拒絕的狀態**: sdd change 有 tasks 卻沒有 delta spec, 或宣告 `skip_specs: true` 又留著 spec 檔時, `instructions apply` 回報 `ready`, agent 做完全部工作才在 validate 撞到 ERROR, `all_done` 還寫著可以 archive. 根本原因是 apply、status、validate 各自掃描檔案, 對「哪些檔案算 merge 會套用的 delta」用了不同規則. 現在判定集中到 `findSkipSpecsContradictions`, 並讀取 merge 實際使用的 `findDeltaSpecSources`. 有下列任一警告時, `all_done` 改說 "cannot be archived yet".
|
|
92
|
+
- `apply_specs_missing`: schema 的 `specs` 為必要、不存在、也沒有以 `skip_specs` 宣告略過. `skip_specs: "yes"` 這類無效值會被指出, 而不是當成沒有宣告.
|
|
93
|
+
- `apply_skip_specs_contradicted` (blocked 時也回報) 與 `status` 的 `skip_specs_contradicted`: 宣告略過卻有 spec 檔, 包含 stub. `status` 仍把 specs 顯示為 `done` 再加警告, 沒有新增 artifact 狀態, 因為 status 應只是磁碟檔案的函式.
|
|
94
|
+
- `apply_specs_misplaced`: 不論 marker 為何, 點名 validate 會因位置拒絕的檔案. 位置規則抽成 `specLayoutFault`, 與 validate 共用.
|
|
95
|
+
- 必要 artifact 是 template stub 或空檔時, apply 改回 `blocked`, instruction 寫 `Still empty or the unfilled template: <ids>.`. 原本 `missingArtifacts` 只檢查檔案是否存在, 沒有使用 `loadChangeContext` 已算好的 `context.stubs`; 現在與 `status` 讀同一份結果. human 模式的標題改為 `Missing or incomplete artifacts:`, 因為檔案已在磁碟上.
|
|
96
|
+
- `instructions --json` 的 `dependencies[]` 新增 `optional` 與 `skipped`. 原本被 `skip_specs` 略過或未寫的 optional 相依 artifact 只有 `done: false`, 與尚未寫的必要相依 artifact 無法分辨, propose 會去讀不存在的檔案. 文字模式把這兩種相依 artifact 移到 "there is no file to read" 一段, `tospec-propose` 也略過它們.
|
|
97
|
+
- **`apply.tracks` 對不到任何 artifact 時回報 `schema_tracks_unmatched`**: 自訂 schema 的 `tracks` 對不到任何 `generates` 時, 進度回退到 `tasks.md`: apply 說 "The TODO.md file is missing", 而 TODO.md 就在 change 裡, `list` 與 archive gate 也計算錯的檔案. 現在 `schemas`、`status`、`instructions apply`、`archive`、`list` 都回報這個 code 與可用的值, 不改變任何 exit code. 沒有採用拒絕載入, 那會讓使用該 schema 的既有 change 全部失效; 也沒有把 tracks 當成字面路徑讀取, 那會形成 `list`、dashboard 與 archive gate 不共用的第二套解析規則. `tracks: ''` 載入時轉成 null: 原本 apply 把它當成沒有 tracks, 進度卻把它當成對不到的名稱.
|
|
98
|
+
- **`tospec-update` 可以在已完成的 glob artifact 底下補建檔案**: glob artifact 只要有一個檔案就是 done, 而 propose 只處理 ready 的 artifact. coherence review 發現少了第二個 capability spec 時, 沒有任何 workflow 能建立它. 現在 update 在使用者確認後, 依 `tospec instructions` 的 template 與 instruction 在具體路徑擬稿, 以獨占方式建立, 不覆寫既有檔案. 完全沒有檔案的 artifact 仍由 propose 或 issue 處理.
|
|
99
|
+
|
|
100
|
+
### 安全
|
|
101
|
+
|
|
102
|
+
- **移除 regex 的二次方回溯 (ReDoS)**: repo 內一行精心構造的文字就能讓命令停住數秒: REMOVED bullet 上 4 萬個空白 300-650 ms, RENAMED / MODIFIED bullet 比對 8 萬個空白 1.9-2.7 s, ticket 的 `ref:` 行 13.5 s, `tasks.md` 的 task 行 7.5 s, 含 U+2028 / U+2029 的標題 2.5-5.4 s (requirement 名稱前的空白呈三次方成長). 這些輸入來自 repo 的檔案, 而 agent 正是在剛 clone 的 repo 裡執行這些命令. 根本原因是 lazy capture 或重疊的量詞會在空白串的每個位置重試結尾; `\s` 會吃 U+2028 / U+2029 而 `.` 在那裡停止, `normalizeDocument` 只轉換 CR 與 LF, 所以這兩個字元會到達每個 parser. 現在改為 greedy capture 後在程式碼中判斷結尾, 或在空白後加 `(?![ \t])`, HTML comment 與字串結尾改用掃描. 同樣的輸入都降到 1 ms 以下, 每個 pattern 都以數十萬到數百萬個隨機字串 (CR、LF、CRLF、U+2028、U+2029、NBSP、BOM) 比對新舊邏輯, 結果相同. 範圍涵蓋 parser、validate、archive、init / update 的 `generatedBy` 比對、migrate 與 dashboard 的 `safeUrl`. 唯一的行為差異是跨行的 `generatedBy` 值不再被讀取, tospec 從不寫這種值.
|
|
103
|
+
- **`tospec instructions` 的文字輸出中和 repo 內容裡的標籤**: 文字輸出以 pseudo-XML 標籤包住 config context、rules、template 與 change metadata. `config.yaml` 的 context 只要含 `</project_context><task>...`, 就能關掉背景區塊, 讓後面的內容被讀成頂層指示. 現在 `prompt-escape.ts` 只中和 printer 自己使用的標籤 (開頭與結尾, 允許空白), 屬性值跳脫 `"`, inline 欄位攤平換行與控制字元. 每個輸出點都套用, 包含 apply 的 task 描述、artifact id 與 context 檔案路徑 (POSIX 檔名可以含換行, 能偽造標題). 沒有採用先把所有內容 entity encode: 上游這樣做之後又必須回退, 而且一般的 `<name>` placeholder 會變得無法閱讀.
|
|
104
|
+
- sdd proposal template 的 placeholder `<description>` 改為 `<summary>`. `description` 是 printer 的標籤, 這個 placeholder 原本被印成 `<description>`. 沒有縮小中和範圍, 那會重新開啟這個漏洞. 代價是升級前建立且未編輯的 `proposal.md` 不再被判定為 stub.
|
|
105
|
+
- **archive 不刪除, 也一定回報 project 以外的寫入**: capability 目錄或 `tospec/specs` 本身是連到 project 外的 link 時, archive 會寫到那裡, retire 時還會以 `fs.rm` 刪掉 project 不擁有的檔案. 現在 prepare 階段的 `checkTargetWithinProject` 解析目標的實際落點 (尚未建立的路徑經由最深的既有上層目錄解析). 寫入照常進行, 但回報 `archive_spec_outside_project` 與實際位置; retire 落在 project 外則在寫入任何 spec 之前拒絕. dangling link 讓 `realpath` 失敗得像不存在的路徑, `writeFile` 卻會跟隨它, 所以第一個不存在的段落改用 `lstat`, 並從 link 目標繼續解析. 只有完整的 `..` 段落算離開 project, `..shared` 是目錄名稱.
|
|
106
|
+
- **schema 名稱必須是單一路徑段落**: `--schema ../../x`、`tospec/config.yaml` 或 `.tospec.yaml` 的 `schema:` 可以從 schema 目錄外載入 schema, `templates`、`status`、`instructions` 都 exit 0. 根本原因是 resolver 直接 `path.join(dir, name)`, 只有 `new change` 與 metadata 讀取驗證名稱. 現在檢查放在最底層的 lookup, 三個來源都經過, 錯誤為 `Schema '<name>' is a path, not a schema name.`. 沒有改用 kebab-case 規則: 使用者自訂 schema 的目錄名稱可能含大寫或 `_`, 較嚴格的規則會拒絕已存在的 schema.
|
|
107
|
+
- **dashboard 的讀取限制在實際的 project root 內**: `/api/render` 經由 junction 或 symlink 可以讀取 root 外的 `.md`, `/api/specs/<id>` 的 `answerSpec` 則直接 `readFile`. 搭配 `--allow-remote` 時, 這些檔案會暴露到 LAN. 根本原因是這兩條路徑只做字面路徑檢查, task 寫入卻有 realpath 檢查. 現在三者共用 `confineToRealRoot()`, root 外回 403; root 本身也做 realpath, 經由 link 開啟的專案不會被誤判. change 詳細頁與清單也只列實際路徑在 root 內的項目. 檢查仍在 `dashboard-routes.ts`, 由專案端執行.
|
|
108
|
+
- 任何大小寫的 `archive` 路徑段落都視為已歸檔 (403). 原本 `ARCHIVE` 被當成 change 名稱, 只因其他原因回 409; Windows 上 `/api/changes/ARCHIVE` 還會把 archive 目錄當成 change 回傳.
|
|
109
|
+
- **`--allow-remote` 的 hub 拒絕 task 寫入**: 0.24.0 說明 `--allow-remote` 時頁面是唯讀的, 但從 LAN 送出的 task POST 回 200 並改寫了檔案, 只有回答問題被擋下. 根本原因是 `interactive` 檢查只加在 answer 上, 比它早存在的 task 寫入沒有被納入. 現在 `DashboardHub.write()` 在非 interactive 時回 403, 不把指令放進 session 的佇列, 之後新增的寫入也經過同一個限制. 頁面的 checkbox 一併停用並說明原因. 採用強制唯讀而不是修改文件, 因為 README、AGENTS.md 與頁面本身都已承諾唯讀.
|
|
110
|
+
- hub POST 的 Content-Type 改為取 `;` 前的 media type, 轉小寫後完全比對 `application/json`. 原本的 `/^application\/json\b/` 區分大小寫, 卻接受 `application/json-seq`. CSRF 防護因此更嚴格, 而不是放寬.
|
|
111
|
+
- **issue workflow 遮蔽貼上的命令與輸出中的 secret**: issue workflow 會讓 agent 把重現命令與輸出寫進 `task.md`, 這份檔案會被 commit, 並永久留在 archive. 現在 `tospec-issue` 與 issue schema 的 task instruction 要求把 secret 改寫成 `<REDACTED>`、從環境變數讀取憑證、只引用帶有訊號的那幾行. 那幾行本身不遮蔽, 因為刪改過的錯誤訊息無法診斷. 需要使用者登入的步驟由使用者自己執行. prompt 只能降低外洩機率, 無法保證不外洩.
|
|
112
|
+
|
|
113
|
+
### 變更
|
|
114
|
+
|
|
115
|
+
- **`skip_specs` 只依使用者明確表示設定, apply 遇到 `apply_specs_missing` 只剩兩條路 (breaking change)**: apply 原本允許使用者「明確同意」後略過缺少的 delta spec 先實作, 但 step 6 的 `tospec validate` 會回報 "No deltas found" ERROR, 而 `VALIDATE_VERDICT_RULE` 要求修掉每個 ERROR. agent 只能補寫使用者決定延後的 spec, 或自行設定 `skip_specs: true`, 兩者都推翻了使用者的決定. 另外, archive skill、sdd schema 與多則 CLI fix 以「change 沒有外部可觀察的效果 (tooling、CI、docs)」作為設定 flag 的條件, 等於請 agent 自己判斷. 根本原因是使用者的決定沒有記錄成每個讀取端都讀得到的資料, 只存在一個 template 的流程文字中. 現在 apply step 2 只剩兩條路: 回到 planning skill 撰寫 delta spec, 或在使用者明確表示不需要時設定 `skip_specs: true`; 「先實作再說」不再是選項. 提供這個 flag 的八個地方 (兩個 apply 變體、archive skill、schema 的兩段 instruction、`apply_specs_missing` / `GUIDE_NO_DELTAS` / `archive_validation_failed` / `skip_specs_invalid` 的 fix) 都改成「使用者確認不需要 delta spec」, 並移除用來判斷效果的範例. 沒有採用在 step 6 加豁免: `VERIFY.md` 與 review 仍以 validate 通過為完成條件, 豁免只存在一個 template, 每個 consumer 都要各自重複一次; flag 則是每個命令以同樣方式讀取的單一事實. 已安裝的 skill 要重跑 `tospec update`.
|
|
116
|
+
- **init / update 的 JSON 回報與 exit code (breaking change)**: init 或 update 有工具寫入失敗時只印 `Failed:`, JSON 走成功信封並 exit 0, CI 與 agent 都看不到失敗, 這違反 batch 命令部分失敗的約定. 現在每個失敗的工具對應一筆 `init_tool_failed` / `update_tool_failed`, data 與 `failed[]` 照常填入, 並 exit 1. 上游只處理 Codex bootstrap 失敗, 這裡涵蓋所有工具. 同時統一工具的識別方式: `update.tools` 原本是 display name, `init` / `rules` 用 tool id, 兩邊的 `failed[]` 也都用 display name.
|
|
117
|
+
- `update.tools` 從 `["Agents","Claude Code"]` 改為 tool id `["agents","claude"]`.
|
|
118
|
+
- init / update 的 `failed[]` 從 `{name, error}` 改為 `{tool, error}`, 與 `created` / `refreshed` / `rules[]` 形狀相同. display name 只留在 message 與 human 輸出.
|
|
119
|
+
- `init_tool_failed`、`update_tool_failed`、`update_new_tool_detected` 帶 `target: <tool id>`. `target` 只能放一個 tool id, 所以 `update_new_tool_detected` 改為每個 tool 一筆.
|
|
120
|
+
- **非互動環境不再開啟提示, 環境變數改為 `TOSPEC_INTERACTIVE` (breaking change)**: 非 TTY 或設定 `CI` 時, `archive` 的確認提示與 `config reset --all` 仍會等待輸入, 讀到 stdin 的 EOF 後當成取消, 以 `Cancelled.` exit 130. 呼叫端看到的是「使用者取消」, 而不是「缺少 `--yes`」. 根本原因是這幾個提示沒有經過 `isInteractive()`. 現在 archive 的三個提示 (gate 確認、"Proceed with spec updates?"、change picker) 與 `config reset` 都改用 `isInteractive()`, 在任何寫入之前以 exit 1 拒絕, 並在 `Fix:` 提示 `--yes`. spec 更新提示的拒絕使用新 code `archive_confirmation_required`. 沒有採用 JSON 模式的「不問就合併」, 因為 human 模式承諾會問這個問題. `--json` 的行為不變, skill 都以 `--json` 呼叫 archive.
|
|
121
|
+
- `isInteractive()` 讀取的環境變數從 OpenSpec 遺留的 `OPEN_SPEC_INTERACTIVE` 改為 `TOSPEC_INTERACTIVE` (`0` 關閉提示). 不保留舊名稱, 與 `TOSPEC_CONCURRENCY` 的先例相同. 舊名稱從未寫進 tospec 的文件, CI 靠 `CI` 變數, 不受影響.
|
|
122
|
+
- `--no-validate` gate 在 human 模式的 fix 不再帶 `--json`. 原本照著 fix 重跑會得到 JSON 輸出.
|
|
123
|
+
- **目錄 link (junction / symlink) 在所有命令一律跟隨 (breaking change)**: Windows 上 `Dirent.isDirectory()` 對 junction 回報 false, 所以 `list`、`validate --all`、`list --specs` 與 archive 的 delta 掃描略過被 link 的 change 或 capability, `status` 與 `instructions` 卻經由 `fs.stat` 讀到它們. archive 因此靜默丟掉 link capability 的 delta. 現在 `isDirectoryEntry()` 遇到 link 時再 `stat` 一次, 取代各處的 Dirent 判定; 遞迴掃描以 realpath 記錄走過的目錄, link 指回上層時每個實際目錄只走一次. 採用跟隨而不是拒絕, 因為「共用的 specs 目錄是合理用法」是既有的決定, 拒絕 link 會推翻它.
|
|
124
|
+
- change 目錄本身是 link 時, archive 照常完成並發出新的 WARNING `archive_change_linked`. archive 只搬移 link, 內容仍在 link 目標, git 不會在 `archive/` 下追蹤它.
|
|
125
|
+
- migrate 以 realpath 複製被 link 的 change、spec 與 archive 目錄的內容, 不複製 link 本身. 斷掉的 link 仍列在 `skipped[]`.
|
|
126
|
+
- dashboard 不列出實際路徑在 root 外的 change 或 spec (見「安全」), 所以可能比 CLI 少列. CLI 由使用者在終端機執行, 頁面則不能讀取 root 外的檔案.
|
|
127
|
+
- **task 計數與 GitHub 畫成 checkbox 的寫法一致 (breaking change)**: 下列 task 原本不被計入, `list` 顯示 change 已完成, archive gate 也放行. 根本原因是 counter 的規則比 CommonMark 窄. 用這些寫法的舊 change 可能改為在 archive gate 被擋下, 這正是 gate 原本宣稱的行為.
|
|
128
|
+
- ordered list 的 `1. [ ] ...` 與 `1) [ ] ...`: `TASK_PATTERN` 只認 `-`、`*`、`+`. 現在 marker 加入 `\d{1,9}[.)]`, `setTaskDone` 共用同一個 pattern. `[~]`、`[]`、`[ x]` 仍不算 task (與上游不同), 因為 GitHub 把它們顯示為文字.
|
|
129
|
+
- 含 U+2028 / U+2029 的 task 行: task 文字以 `.*` 讀取, 而 `.` 在這兩個字元停止. 現在改為 `[^\r\n]*`. apply 的 id 拆分 regex 同步修改, 否則這些行一被計入就會引入二次方回溯.
|
|
130
|
+
- bare CR 換行: `setTaskDone` 只以 `\n` 分行, parser 則在 bare CR 也斷行, 之後每一行都差一行, dashboard 勾 1.2 會勾到 1.3. 現在 `setTaskDone` 以 `/\r\n?|\n/` 分行, 並保留每行原本的換行字元. 沒有採用 AGENTS.md 的 `split(/\r?\n/)`: CommonMark 在 bare CR 斷行, 讀成一行時兩個 checkbox 都不算.
|
|
131
|
+
- **validate 與 archive 拒絕更多原本會通過的 spec (breaking change)**: 以下情況原本由 validate 放行, archive 則合併出讀者分不出差異或結構錯誤的 main spec, 或直接刪掉內容. 共同的根本原因是各讀取端對同一份 spec 的讀法不同. 現在 validate 回報 ERROR 或 archive 拒絕, 原本通過的 change 與 main spec 可能改為 exit 1.
|
|
132
|
+
- 只差大小寫或空白的 requirement 名稱視為同一個. ADDED `late fees` 加上 RENAMED 成 `LATE FEES` 原本讓合併後的 spec 有三個讀者分不出的 requirement. 現在所有重複與衝突檢查都經過 `findFoldedMatch` (轉小寫、合併空白), 只改大小寫的 rename 仍合法. main spec 已有這種成對名稱時會被回報, 並擋下下一次 archive.
|
|
133
|
+
- main spec 的 Purpose 只認 `## Purpose`. `parseSpec` 原本在任何層級尋找 Purpose, migrate、placeholder 檢查與 delta parser 則只認 level 2; `# Purpose` 還會把 `## Requirements` 讀進 overview. 錯誤改為指出標題與行號的 `SpecHeadingLevelError`. 沒有採用讓 migrate 認得每個層級, 因為每個讀取端與 template 都已寫 `## Purpose`.
|
|
134
|
+
- Requirements section 在 `#` 標題結束. merge 與 structure guard 原本只在下一個 `##` 結束, `MarkdownParser` 在 `#` 也結束, 所以 ADDED requirement 被附加到 `#` 標題之下, `show --json` 看不到它. 現在兩者共用 `endsRequirementsSection`, ADDED 寫在 `#` 標題之前, 位於 `#` 標題之後的 requirement 是 ERROR.
|
|
135
|
+
- main spec 殘留的 delta 標題 (`## REMOVED Scenarios`、`## ADDED Requirements ###`) 底下只有 bullet 時, 結構檢查原本沒有發現. 結構檢查與 migrate 改用 delta parser 的 `isDeltaSectionHeader`, 不再各自維護一份已經發生過不一致的 section 清單.
|
|
136
|
+
- 移除所有 requirement 時, 若 main spec 在標題、`## Purpose`、`## Requirements` 之外仍有內容 (`## Notes`、`# Appendix`), archive 拒絕刪檔並點名這些區段. 原本整個檔案連同 notes 一起被刪掉, 而同樣的 notes 寫在 requirement 區塊內時會被拒絕. 沒有採用 retire 加通知, 那會讓同一種遺失有兩套規則.
|
|
137
|
+
- 新 capability 的名稱: 與既有 main spec 只差大小寫是 ERROR, 原本在 Windows 與預設的 macOS 上會合併進既有 capability, 在區分大小寫的檔案系統上則建出第二份. Windows checkout 無法建立的名稱 (`con`、`nul`、禁用字元、結尾的點或空白) 也是 ERROR. 規則只套用在新 capability, 既有名稱照常運作.
|
|
138
|
+
- change 只要有 `specs/` 目錄, validate 就執行 delta pass. 原本只在 schema 要求 delta 時執行, 但 archive 照樣合併每個 `specs/<capability>/spec.md`, 缺少 Reason 的 REMOVED 因此通過 validate 後直接刪掉 requirement. `requireDeltas` 現在只決定「是否必須至少有一個 delta」.
|
|
139
|
+
- `archive --no-validate` 遇到格式錯誤的 RENAMED 區塊 (`FROM a / FROM b / TO x / TO y`) 時拒絕. 原本 `buildUpdatedSpec` 不讀 parser 的 `renamedIssues`, 照猜測把 b 改名成 x. 略過 validation 時, 這是那條路徑上唯一的檢查.
|
|
140
|
+
- **`validate --strict` 會拒絕的新 WARNING (breaking change)**: 下列檢查都以 WARNING 回報, 一般的 `validate` 與 archive 不受影響. 但 `--strict` 把 WARNING 當成失敗, 原本通過的 change 可能開始 exit 1.
|
|
141
|
+
- delta section 以外的內容、拼錯的 delta 標題、`specs/` 根目錄的 delta 檔 (見「新增」).
|
|
142
|
+
- `.tospec.yaml` 中 schema 不認識的頂層 key, 並以 Levenshtein 距離提示最接近的 key. Zod 會丟掉不認識的 key, 拼錯的 `decision:` 原本讓 ADR 連結靜默遺失. `x-` 開頭的 key 不受限制. 這些警告與 `markerIssues` 分開, 否則 `skip_specs: true` 旁邊任何不認識的 key 都會讓豁免失效.
|
|
143
|
+
- counter 看不到的 task 行 (`[~]`、`[]`、`[ x]`、`[x]done`) 與沒有任何 task 的非空 tracked 檔. 原本 `- [~] 1.2 deferred` 從所有 gate 消失. 括號內含 x 以外字母或數字的 (`[P1]`、`[v2]`) 視為標籤, 不回報. 沒有採用上游的放寬 counter, 那會計入沒有人能在 GitHub 上勾選的 box.
|
|
144
|
+
- 已損壞的 main spec. merge dry run 重建出有 ERROR 的 spec 時, validate 預先回報, 並區分錯在 main spec 還是 delta. 原本 `findArchiveBlockers` 只執行 merge, 不驗證結果, 所以 `--strict` 通過的 change 會在 archive 失敗.
|
|
145
|
+
- 既有 capability 的 delta 含非空 `## Purpose`. archive 不會套用它, 原本沒有任何提示.
|
|
146
|
+
- 新 capability 名稱不是 kebab-case. 規則支援 Unicode, `中文` 這類沒有大小寫的文字可以通過.
|
|
147
|
+
- **status code 對準實際的失敗 (breaking change)**: 下列失敗原本使用通用 code, 以 code 判斷的呼叫端需要調整.
|
|
148
|
+
- rule conflict 從 `init_error` / `update_error` / `rules_error` 改為 `rule_conflict`, 修正指引從 message 移到 `fix`. 三個命令回報同一個 code.
|
|
149
|
+
- `tospec` 或 layout 子目錄是檔案時, init 從 `init_error` 加原始 `EEXIST` 改為 `init_layout_path_not_directory` (含 `target` / `fix`), 並在任何提示與寫入之前停止. 其他命令遇到 `tospec/changes`、`specs`、`tickets`、`decisions` 是檔案時, 回報新的 `layout_path_not_directory`, 不再是通用 code 加原始 `ENOTDIR`. archive 與 `decision new` 在第一次寫入前檢查, 否則錯誤會在 specs 合併之後才出現.
|
|
150
|
+
- archive 寫入部分 spec 後失敗 (第二個 capability 寫入失敗、搬移遇到 ENOSPC / EBUSY) 時, 從 `archive_error` 改為 `archive_partially_applied`. message 列出已完成與失敗的步驟, fix 說不要重跑, 並列出剩下的手動步驟. 原本的泛用錯誤加 `archive: null` 看起來像什麼都沒改, 但 spec 已合併, 重跑會因 ADDED requirement 已存在而失敗. 第一次寫入就失敗時維持原錯誤, 因為重跑仍是正確建議. 沒有採用還原已寫入的 spec: 讓寫入失敗的原因 (磁碟滿、檔案鎖) 最可能也讓還原失敗.
|
|
151
|
+
- 搬移中途失敗留下的 `archive/<stamp>-<name>/` 半份複本會被清除, 只刪除這次複製建立的目的地. `archive_partially_applied` 的 fix 只在清除也失敗時要求手動刪除.
|
|
152
|
+
- `show -r` 的 `-r 0`、`-1`、`abc` 從 `show_error` 改為 `invalid_argument`.
|
|
153
|
+
- **`list` 與 `schemas` 依實際讀取結果回報 (breaking change)**: 兩個 batch 命令把讀不到的項目回報成正常, 或把正常的狀態回報成錯誤. 現在讀不到的項目走 partial failure (data 照常填入, exit 1), 不是錯誤的狀態則回報成功.
|
|
154
|
+
- change 的 schema.yaml 無法載入時, `list` 把它顯示為 `no-tasks` 並 exit 0, 同一個 change 在 `status --all` 卻是 `change_unreadable`. 根本原因是 `list` 載入 schema 時 `catch { continue; }`. 現在使用 `status` 的同一個 loader, 走既有的 unreadable 路徑.
|
|
155
|
+
- `tospec/changes` 是檔案時, `list` 回 `changes: []` 並 exit 0. 根本原因是 `listSubdirectories` 吞掉所有錯誤, 而 `status` / `validate` 用的列舉函式只吞 ENOENT. 兩組命令改用同一個函式, 只把 ENOENT 當成沒有 change.
|
|
156
|
+
- 有 `tospec/` 但沒有 `changes/` 時, `list` 原本以 `changes_dir_missing` exit 1, 把缺少的目錄當成安裝不完整. `decision new` 不需 init 就會建立 `tospec/decisions/`, 所以這是正常狀態. 現在回報沒有 change 並 exit 0, `changes_dir_missing` 只保留給完全沒有 `tospec/` 的情況.
|
|
157
|
+
- 覆寫 schema 壞掉時, `schemas` 刪掉上層的錯誤, 只列出可用的 package schema, 但 resolver 實際選的是壞掉的覆寫. 現在列表與解析用同一條規則, 回報 `schema_unreadable` 並 exit 1, fix 說明移除覆寫目錄可回退到下層. 沒有採用在 package entry 加 `shadowedBy`: 那會擴大 JSON 契約, package entry 也仍看起來可用.
|
|
158
|
+
- change 目錄下指向自己的 link 讓 `getLastModified` 拋出 ELOOP, 整個 change 被標為 unreadable. 現在略過這個項目. 沒有放寬共用的 `isMissingPathError`, 因為呼叫端把它解讀為「路徑不存在」, 而迴圈 link 是存在的.
|
|
159
|
+
- **原本接受、現在以 exit 1 拒絕的輸入 (breaking change)**: 下列輸入原本被接受, 但產生之後無法操作或不正確的結果. 拒絕都發生在寫入任何檔案之前.
|
|
160
|
+
- `new change` 不帶 `--schema` 時, config 指定的 schema 無法載入或是 `decision`, 原本照樣建立 change 並略過 ticket. 現在不論 schema 來自何處都執行同一組檢查; schema 來自 `config.yaml` 時, 訊息指出來源.
|
|
161
|
+
- `new change --decisions` 只接受 ADR 檔名 (`<yyyyMMdd_HHmmss>-<topic>.md`), 不接受含路徑的值. 重複值自動去重.
|
|
162
|
+
- `new change`、`decision new` 與 migrate 在所有平台拒絕 Windows 保留裝置名稱 (`con`、`aux`、`nul`、`com1-9` 等)、禁用字元與結尾的點或空白, 因為這種名稱在 Windows checkout 上無法建立. 只在建立時檢查, 既有同名 change 仍可操作.
|
|
163
|
+
- `status`、`instructions`、`archive` 拒絕保留名稱 `archive`. 原本 `archive` 目錄被當成一個所有 artifact 都未寫的 change. dashboard 的 `/api/changes/archive` 回 404.
|
|
164
|
+
- `show -r` 只接受正整數. `2abc`、`1.9` 原本被截斷後 exit 0.
|
|
165
|
+
- `config unset` 對檔案中不存在的未知 key 以 `config_error` 拒絕. 原本 exit 0, 打錯的 key 看起來像已移除. 檔案中已有的未知 key 仍可移除.
|
|
166
|
+
- migrate 遇到不合法的 active change 名稱時以 `migrate_change_name_invalid` 拒絕 (`migrate: null`). 原本原樣複製, 之後所有命令都無法操作它. 沒有採用自動轉成 kebab-case: 兩個名稱可能轉成同一個 id, 而 source 是唯讀的. 兩個 archive 目錄對應到同一個目標名稱 (不分大小寫) 時以 `migrate_archive_name_collision` 拒絕.
|
|
167
|
+
- **human 模式的診斷一律使用 `Error:` / `Warning:` / `Fix:`**: 每個輸出點各自撰寫, 同一個 `schema_tracks_unmatched` 在 `status` 印成 `⚠ <message>` 並丟掉 fix, 在 `schemas` 印成沒有前綴的一行. 多行訊息 (YAML code frame、specs-apply 的拒絕清單) 的後續行從第 0 欄印出, 看起來像另一則訊息. 根本原因是沒有任何規則讓這些輸出點遵守 failure 使用的形狀. 現在 `shared-output.ts` 的 `formatLabeled` 產生後續行縮排兩格的條目, 各輸出點維持原本的 stream. 沒有採用共用的 print helper, 因為各處的 stream 與跳脫方式不同, helper 必須把兩者都當參數, 只為了取代每處一行. `--json` 不受影響, 但比對 `⚠`、`WARNING:`、`Aborted.` 的 script 需要調整.
|
|
168
|
+
- `status --all` 對讀不到的 change 在 stdout 印 `Change: <name> (unreadable: <message>)`, 診斷在 stderr, 與 `list` 相同的拆法. 原本 stdout 什麼都不印, pipe 之後這一筆就消失.
|
|
169
|
+
- project config 的警告在所有命令都經過同一個 printer, 每個 process 只印一次 (原本 batch 命令印 N 次), `instructions apply` 也會印出. `config_unparseable` 的 `--json` message 不再以 `Warning:` 開頭.
|
|
170
|
+
- `show <change> --requirements-only` 的棄用通知在兩種模式都印到 stderr (原本只在 `--json` 印). 非 JSON 模式使用 JSON-only flag 時印出警告, `Ignoring flags` 改列 flag 名稱 (`--deltas-only`) 而不是內部 key.
|
|
171
|
+
- `archive --no-validate` 的 `⚠️ WARNING:` 改為 `Warning:`, 「Could not open a browser automatically」加上 `Warning:` 前綴.
|
|
172
|
+
- **零星變更**: `init --force` 原本就沒有作用, 從 help 與文件隱藏, 仍接受. `config reset --all` 與 `config edit` (檔案不存在時) 寫入 `{}` 而不是明確的 `profile: core`, `config list` 因此顯示 `(default)` 而不是 `(explicit)`; 第一次 `config set` 也不再寫入 `profile`.
|
|
173
|
+
|
|
174
|
+
### 修正
|
|
175
|
+
|
|
176
|
+
- **delta 文法的讀法與 CommonMark 一致**: 下列寫法在 GitHub 上與正規寫法看起來相同, tospec 卻讀成不同的東西, 而且 validate 通過、archive exit 0.
|
|
177
|
+
- REMOVED / RENAMED 的 `*` 與 `+` bullet: `LINE_PREFIX` 與 REMOVED bullet regex 只認 `-`, `* Late Fees` 沒被刪除也沒被改名. 現在三處都接受 `[-*+]`. marker 後仍必須有空白, 所以 `**Reason**:` 不是 bullet; `* * *` 這類 thematic break 被排除, 不會被記成 requirement 名稱.
|
|
178
|
+
- 標題結尾的 `#` 序列: CommonMark 把它當成裝飾, parser 卻保留在名稱與標題中. `### Requirement: Late Fees ###` 讀成 `Late Fees ###`, `## REMOVED Requirements ##` 對不上任何 delta 標題, migrate 認為正常的 `## Purpose ##` 被 validate 判定為缺少 Purpose. 現在 `stripClosingHashes` 套用在 requirement / scenario 名稱與 delta section 標題, `findSection` 與 structure guard 也比對去掉 `#` 的標題. `#` 前必須是空白, 所以 `Support C#` 保留 `#`. 改用線性掃描, 沒有採用上游的 `/[ \t]+#+[ \t]*$/`, 它會二次方回溯. 沒有在 `parseSections` 去掉 `#`: 名稱會被去兩次, 所有文件類型的標題文字也會改變.
|
|
179
|
+
- delta 的 requirement 區塊在 `#` 標題結束. 原本只在下一個 `##` 結束, ADDED 區塊內的 `# Note` 被複製進 main spec, 之後的 requirement 落到 `## Requirements` 之外, 而 validate 通過. 現在與 main spec 共用 `endsRequirementsSection`. 有內文的 `#` 標題以 `archive_delta_content_not_merged` 點名, 沒有合併進 main spec, 因為合併正是讓後面的 requirement 跑出 section 的原因.
|
|
180
|
+
- `show --deltas-only` 改用 archive 的 `parseDeltaSpec`. 原本以要求 `###` 後有空白的 section walker 讀取, `###Requirement: X` 什麼都找不到, 回退到 proposal 文字並回報一個 MODIFIED, archive 卻把同一個檔案讀成 ADDED. proposal 文字現在只在沒有任何 spec 檔含 delta section 時才作為回退.
|
|
181
|
+
- **delta 檔的來源只有一套規則**: `show <change> --json` 在 Windows 上把 `specs/billing/SPEC.md` 列為 ADDED delta, 也列出 `specs/.hidden/spec.md`, validate 卻不讀它們. `specs/auth/spec.md/` 是目錄時被當成 delta 來源, merge 之後才以 EISDIR 失敗. 不要求 delta 的 schema 下, validate 把沒有 delta 標題的 `specs/spec.md` 當成筆記, archive 的 `archive_spec_merge_empty` backstop 卻拒絕它, 並叫使用者回去跑 validate. 根本原因是 archive 的每條讀取路徑、`show` 與 validate 各自決定哪些檔案算 delta, 大小寫交給檔案系統決定. 現在 `findDeltaSpecSources` 由 merge 與 `show` 共用: 略過 dot 目錄, 要求 readdir 中有完全相同拼寫的 `spec.md`, 並以 `fs.stat` 要求是一般檔案 (stat 會跟隨 link, 所以 symlink 的 `spec.md` 仍算). root 檔案的判定集中到 `findRootSpecDelta`. 沒有採用不分大小寫的過濾: 那會是第三套規則, 在區分大小寫的檔案系統上還會列出 merge 會丟掉的 `SPEC.md`.
|
|
182
|
+
- `--no-validate` 下與 capability delta 並存的 `specs/spec.md` 改以 `archive_root_delta_ignored` 回報. 原本 backstop 只在完全沒有 delta 時執行, 這個檔案沒有留下任何紀錄.
|
|
183
|
+
- **validate 與 archive 的訊息指向真正的原因**:
|
|
184
|
+
- validate 在結構錯誤之後仍回報 merge precondition. 原本 dry run 遇到第一個結構錯誤就停止, MODIFIED 少 Scenario 與 ADDED 名稱衝突要修兩輪才看得到. archive 的預設路徑不變, 不會寫出部分結果.
|
|
185
|
+
- RENAMED 加 `## REMOVED Scenarios` 使用舊名稱時, 訊息寫出應使用的新名稱 (archive 先套用 RENAMED). 比對規則不變.
|
|
186
|
+
- `archive_spec_update_failed` 錯在 main spec 時 (requirement 在 `## Requirements` 之外、含 delta header、retire 被拒絕), fix 改為 `Fix tospec/specs/<cap>/spec.md and rerun`. 原本一律叫使用者改 delta, 而改 delta 沒有用. 以 `MainSpecMergeError` 型別區分, 沒有比對訊息文字, 否則 fix 會綁死在措辭上. `archive_spec_validation_failed` 的 message 以續行附上每個 ERROR 與 issue path.
|
|
187
|
+
- validate 的 metadata 錯誤原本一律附上 "Fix the schema name in .tospec.yaml", 只有 `created: bad` 或 `schema.yaml` 本身壞掉時也一樣. 根本原因是欄位錯誤、YAML 無法解析與未知 schema 都拋同一個 `ChangeMetadataError`. 現在未知 schema 拋子類別 `UnknownChangeSchemaError`, `SchemaLoadError` 提示修 schema 檔, 名稱來自 `config.yaml` 時指向 `config.yaml`; `list` 與 `status` 的 `change_unreadable` fix 也依來源指向正確的檔案. human 模式的 Next steps 補上 `.tospec.yaml` 與「沒有 delta」兩個步驟, 原本清單看起來完整, 卻漏掉剛印出的錯誤.
|
|
188
|
+
- fix 訊息帶入實際的 change 名稱, 不再寫死 `<name>`; 空檔稱為 "empty" 而不是 template. `template_file_missing` 在下層沒有同名 schema 時改為建議補檔或修 `template:`, 不再叫使用者複製不存在的 packaged schema. change 相關命令的 schema 清單不再列出 `decision`. "Did you mean" 只列距離在門檻內的名稱, 沒有相近名稱時不印建議.
|
|
189
|
+
- **無效的 `skip_specs` 不再讓整份 `.tospec.yaml` 讀取失敗**: `skip_specs: "yes"` 時, validate 叫使用者去改沒有問題的 schema 名稱, 並在其他檢查之前返回; `list` 與 dashboard 把 change 標成 unreadable; `schema: issue` 被當成專案預設的 sdd. 根本原因是 `readChangeMetadata` 以整份文件做 Zod 驗證, 一個非 boolean 值讓其他欄位一起失敗. 現在 reader 在 parse 之前丟掉不可用的 `skip_specs` (視為未設定), 回報仍由 `readSkipSpecsMarker` 負責. 修 reader 一次修好所有呼叫端; 沒有採用只在 validate 重讀一次, 那樣 `list`、`show`、dashboard、archive 仍讀成壞掉.
|
|
190
|
+
- **migrate 重建 spec 時不再丟掉或錯置內容**: `normalizeMainSpec` 重建 delta 形式或沒有標題的 OpenSpec main spec 時, 只保留 Purpose 與 requirement 區塊, migrate exit 0 且不回報遺失. 被丟掉的有 `## Notes` 等其他 section、第一個區塊之前的文字與前言、Purpose 下的子標題、`#` 標題及其下的文字; `## purpose` (小寫) 被當成沒有 Purpose 而換成 TBD stub. 現在以共用的 `sectionHeaderKey` 讀標題, 其他 section 依原順序寫在 requirements 之後, `#` 標題自成一段並保留在來源中的位置 (即使沒有內文). 採用保留而不是回報: 每個 main spec 讀取端都接受 `## Requirements` 之後的 section, 只回報則仍要使用者手動補回.
|
|
191
|
+
- 判斷 delta 標題改用 delta parser 的 `isDeltaSectionHeader`. 原本 `## Added Requirements` 或 `## ADDED Requirements ##` 的 spec 被當成格式正確並原樣複製, REMOVED 項目留在裡面, 而 migrate 建議執行的 `validate --all` 拒絕這份 spec.
|
|
192
|
+
- `--json` summary 新增 `skipped[]`, 列出沒有搬移的項目 (頂層的其他檔案、`specs/` 與 `changes/` 下不是目錄的項目、沒有 `spec.md` 的 spec 目錄). human 模式印 `Warning: not migrated, copy by hand if still needed: ...`. 選擇 summary 欄位而不是 `status[]`, 因為 migrate 已用 summary 回報要人工檢查的項目.
|
|
193
|
+
- 沒有日期前綴的 archive 目錄補上 stamp (取 `created`, 否則取今天), 並列在新的 `stampedArchives`. 原本原樣保留, dashboard 把名稱拆錯, 並把它排成最近的歸檔. 這個 stamp 是建立日而不是歸檔日, 所以明確列出供使用者檢查.
|
|
194
|
+
- **init / update 的清理範圍**:
|
|
195
|
+
- 每次 `update` 遞迴刪除 `.agents/commands` 與 `.cursor/commands` 整個目錄, 使用者自己的 command 一起消失, 沒有任何提示. 根本原因是 init 的 `removeLegacyCommands` 只刪舊版 tospec 寫過的路徑, update 卻另寫了一份清除邏輯. 現在兩者共用同一份路徑清單, update 也回報移除的數量. Cursor 的 command id 改為凍結清單, 否則 workflow 日後改名時, 舊檔會離開清除範圍.
|
|
196
|
+
- `init --tools none` (或只選 `agents`) 刪除 `.agents/skills` 的正本, 卻保留 `.claude/skills` 的副本, Claude Code 繼續載入 profile 已排除的 skill. 現在 init 也清理已安裝但未選取的 tool 的副本 (只刪除, 不寫入), 並以 `init_removed_deselected_skills` 列出數量.
|
|
197
|
+
- 含 UTF-8 BOM 的 rule 檔被判定為使用者手改, 回報 conflict. 現在比對時忽略 BOM, 內容未變時不改寫檔案.
|
|
198
|
+
- update 的新 tool 提示與 old core 提示只印一次, 改為 `Warning:` / `Fix:`, `--json` 新增 `update_new_tool_detected` 與 `update_old_core_profile` (原本 `--json` 下完全沒有). 相依 tool 在 update plan 顯示 `claude (dependent on agents)`, 而不是 `0.24.0 → 0.24.0`.
|
|
199
|
+
- **全域 config 壞掉時不再被預設值覆寫或誤讀**: global config 多一個逗號就被讀成預設值, 下一次 `config set` / `unset` / `profile` 把預設值存回去, 所有設定都被清掉. 欄位型別錯誤則更糟: `{"profile":"custom","workflows":"explore"}` 讓 update 對字串呼叫 `.filter`, init 把字串展開成字元. 根本原因是讀取只檢查 parse 結果是 object, 不經過 `config set` 用的 Zod schema, 寫入又以讀到的預設值為基礎.
|
|
200
|
+
- 無法解析的檔案拒絕寫入 (`global_config_unreadable`, 指向 `config edit`). `config reset --all` 例外, 因為取代檔案就是它的用途, 否則壞掉的 config 無法從 CLI 復原.
|
|
201
|
+
- 任何欄位不符合 schema 時, 忽略整份檔案並使用預設值, stderr 每個 process 只警告一次 (原本一次 init 印 4 次). 沒有採用只丟棄無效欄位: 上例會變成沒有 workflow 的 custom profile, update 會刪除全部 skill, 而多安裝的 skill 可以恢復. 不觸及無效欄位的寫入以 `global_config_invalid` 拒絕, 因為讀取會忽略它, 寫入會回報成功卻沒有效果; 修正無效欄位的 `set` / `unset` 照常寫入.
|
|
202
|
+
- custom profile 下 `config set workflows ""` / `config unset workflows` 發出 `config_profile_needs_workflows`. `config set workflows` 去除重複值. 相關 fix 改為不會觸發另一個警告的兩行指引.
|
|
203
|
+
- `config list` 讀到內容為 `null` 的檔案時不再 crash.
|
|
204
|
+
- **`config edit` 可以使用帶參數的 `EDITOR`**: `EDITOR="code --wait"` 以 ENOENT 失敗, 因為整個值被當成一個程式名稱. Windows 上 `code` 是只有 cmd.exe 能執行的 `code.cmd`, 不經過 shell 時即使沒有參數也會失敗. 現在 `splitCommandLine` 依引號把值拆成 argv (反斜線不是跳脫字元, Windows 路徑保持原樣), 再以新的 runtime 相依套件 `cross-spawn@^7` 執行; 啟動失敗或 exit code 非零時印 `Error:` 並 exit 1. 沒有採用 `shell: true`, 那會一併執行變數中的其他內容; 也沒有自己實作 cmd.exe 的引號規則, 那屬於 Node 本身也必須修補的 BatBadBut 類型漏洞.
|
|
205
|
+
- **CRLF 檔案的判定**:
|
|
206
|
+
- CRLF 的 ticket 讀不到 frontmatter 最後一個 key 的正確值. `splitFrontmatter` 在 `\n---` 截斷, 最後一行留著 `\r`, `change: abc` 讀成 `abc\r`. 以 `change:` 結尾的 ticket 因此在 status 找不到 (`ticketPath: null`), archive 也不搬移它, dashboard 的 `ref` 則帶著 `\r`. 現在以 `split(/\r?\n/)` 切行, body 從原文切出以保留換行.
|
|
207
|
+
- CRLF 的 template 原樣副本不被判定為 stub. 比對只去除 BOM, 沒有正規化換行, 而 Windows 編輯器與 `core.autocrlf` 都會產生 CRLF. 現在 artifact 與 template 兩邊都經過 `normalizeDocument`, status、validate 與 apply 一起生效.
|
|
208
|
+
- **重用已歸檔 change 的名稱**:
|
|
209
|
+
- 重建同名 change 時, 重用的 ticket 沿用舊的 `type` / `schema` / `ref`, 封存後 `ref` 指向不存在的 artifact. 現在依目前的 schema 重寫這三個 key, body 與 `title` / `date` / `change` 不變. 採用重寫而不是警告, 因為 ticket 必須對應目前的 change.
|
|
210
|
+
- sync 與 verify 的 diff base 取自 `git log -- tospec/changes/<name>/` 的第一個 commit, 同名的舊 change 讓 base 落在數個月前, scope-creep 檢查讀到之後所有無關的 commit. 現在從最新的同名歸檔目錄被歸檔的 commit 之後開始找. 每個指令仍是單一的唯讀 git 呼叫, 不會跳出權限提示.
|
|
211
|
+
- **dashboard**:
|
|
212
|
+
- stale marker 的 pid 被 OS 重用時, `dashboard stop` 終止無關的 process, `start` 拒絕啟動, `status` 回報一個不回應的 hub. Windows 上 hub 被 hard kill (`stop` 本身、Task Manager、關機) 時不會執行 cleanup, marker 會留下, 而判斷只檢查 pid 是否存在. 現在 `/api/health` 回傳 hub 的 pid, `start` / `stop` / `status` 只承認 URL 以相同 pid 回應的 marker, 其餘刪除並說明. 比對 pid 而不是接受任何回應, 也能排除之後綁定同一連接埠的另一個 hub. 卡住超過 3 秒的真 hub 也會被當成 stale: 之後 `start` 會因連接埠被占用而失敗, 這可以恢復; 終止被重用的 pid 則無法恢復.
|
|
213
|
+
- layout 路徑是檔案時, route 回 409 `{error, code, fix}`, 頁面顯示 fix. 原本是 500 或 0 個 change.
|
|
214
|
+
- 含錯誤 escape 的 asset 路徑 (`/assets/%zz`) 回 400, 原本是 500. 單次 activity 回報超過 200 筆時保留最新的 event, 原本保留最舊的.
|
|
215
|
+
- **CLI 的 stdout**:
|
|
216
|
+
- `init <不存在的路徑> --json` 在 JSON 之前印出 `Directory "..." doesn't exist, it will be created.`, stdout 無法解析, 而 exit 0. 這行在建立 `InitCommand` 之前直接 `console.log`, `say()` 的 JSON 抑制管不到它. 現在 `--json` 下不印, 與 `say()` 相同.
|
|
217
|
+
- 讀取端提早關閉時 (`| head`), 下一次寫入產生未處理的 `EPIPE` 並 crash. 現在 `runCli()` 對 stdout 與 stderr 註冊 listener, `EPIPE` 之後的輸出丟棄, 命令照常完成並保留 exit code. 沒有採用 `EPIPE` 時立即 exit, 那會讓 init、update、archive 停在寫到一半的狀態.
|
|
218
|
+
- **skill: sync 與 verify**:
|
|
219
|
+
- sync 與 verify 對每個 delta Requirement 用同一條規則: 沒有實作就是 CODE-BUG. 正確刪除的 REMOVED 行為因此讓 sync 失敗, archive 的 "fix now" 分支還會叫 `tospec-apply` 把行為加回去. 現在 REMOVED 只在行為仍存在時才是 CODE-BUG, 並先從 main spec (或 `git log -p`) 取回區塊, 依內文描述的行為搜尋, 而不是只搜尋標題. 哪裡都找不到時記為新的 `UNJUDGED`, 而不是 MATCH, 因為「找不到」正是通過的結果. RENAMED 以 TO 名稱判定.
|
|
220
|
+
- 含 `UNJUDGED` 的 report 維持 FAIL, 但不是 CODE-BUG. 單獨執行 sync 時依原因分流, 不再一律送回 apply. 解法抽成 archive 與 sync 共用的 fragment, 以免兩份分歧.
|
|
221
|
+
- sync 把每個 SPEC-UPDATED 都改寫成 MODIFIED, 但 ADDED 的 Requirement 在 main spec 沒有目標, archive 因此以 `archive_spec_update_failed` 失敗. 現在在 Requirement 原本所在的 section 就地改寫; 只有不在任何 section 的 Requirement 與行為偏移的 RENAMED (以 TO 名稱) 才新增 MODIFIED.
|
|
222
|
+
- sync 與 verify 沒有指定 diff 命令與基準, agent 自行組出 `cd <root> && git ...`、`$(...)` 這類會觸發權限詢問的命令, 基準是 root commit 時 `git diff <hash>~1` 還會失敗. 現在共用 `CHANGE_DIFF` fragment 與「每個 git 命令單獨執行」的規則. 沒有採用在 allowedTools 授予 `Bash(git diff:*)`: prefix rule 在 read-only 檢查之前比對, 會放行 `git diff --output=<path>` 這種寫檔.
|
|
223
|
+
- `skip_specs` change 沒有 delta 時, sync 改以 tracks 檔與程式碼比對, 不寫 delta, 也不拿掉 flag. 原本這個分支只寫給 issue fix, agent 可能為了有東西可比對而自行寫 delta 或拿掉 flag.
|
|
224
|
+
- verify 的 Standards 軸拿到 apply 收到的 `context` 與 `rules`, 原本專案自訂的 apply rules 從未被 review 檢查. 主 agent 在兩份報告都到齊後才彙整, 不再把缺席的軸當成沒有發現問題. issue change 的 delta spec 也交給 Spec 軸, 每個軸列出沒有檢查的項目.
|
|
225
|
+
- **skill: apply 與 planning workflow**:
|
|
226
|
+
- TDD 變體的 red 必須實際執行, 並確認失敗原因是被 assert 的行為; 因 compile、import 或 setup 錯誤而紅的測試不算. 兩個 apply 變體都列出 expected value 與程式碼算法相同的恆真 assertion.
|
|
227
|
+
- apply 的 issue 分支在勾選 task 之前 grep `[DEBUG-` 並刪除診斷 log. 原本沒有任何步驟執行這次清理, log 隨修正一起 commit.
|
|
228
|
+
- apply 的 Output 依 `apply_specs_missing` 是否已處理決定交接說法. 原本無條件說 change 已可交給 `tospec-archive`, 與 CLI 和 archive 的判斷相反.
|
|
229
|
+
- propose 與 issue 在命名 capability 前先執行 `tospec list --specs --json`, sdd 的 proposal instruction 要求以長期存在的功能領域命名. 原本 propose 從不讀 `tospec/specs/`, 會在既有的 `auth` 旁邊寫出 `add-login-endpoint`, archive 產生第二份重疊的 main spec. 規則寫在 schema instruction 而不是 template, 因為 stub detection 逐 byte 比對 template.
|
|
230
|
+
- propose 與 explore 在 `tospec/decisions/index.md` 存在時讀取它, 只開啟相關的 accepted ADR. 有衝突時建議以 `tospec-decision` 記錄新決策, 不靜默推翻舊的.
|
|
231
|
+
- issue schema 的 task instruction 原本要求 3-5 個假設, 與 0.22.0 改過的 skill 矛盾. 現在兩邊都是依證據列出可證偽的假設.
|
|
232
|
+
- `tospec-update` 的 step 4 只擬稿, step 5 才寫入. 原本 step 4 寫 "Apply the requested edit", 照順序執行的 agent 會先寫入再詢問.
|
|
233
|
+
- `tospec-archive` 對 `--json` 結果的描述與 CLI 一致: 沒有警告的成功不帶 `status`, 四個 code 沒有 `fix`, 失敗是 severity 為 error 的那一筆而不是 `status[0]`. 遇到 `archive_partially_applied` 時, agent 要把剩下的步驟交給使用者, 而不是重試.
|
|
234
|
+
- 本版的 skill 變更都要重跑 `tospec update` 才會寫進專案.
|
|
235
|
+
- **零星修正**:
|
|
236
|
+
- validate 不再把西班牙文的 "Todo el sistema..." 當成 Purpose placeholder. 全大寫的 TBD / TODO 單獨出現才算; 其他大小寫要接標點、行尾或文字結尾, 以免把其他語言的單字當成 placeholder.
|
|
237
|
+
- decision index 的 title 或 summary 含 `\|` 時, status 會更新, 舊格式升級會補 status 欄, `--reindex` 排序正確.
|
|
238
|
+
- `new change --description` 與 `decision new --title` 的多行值合併為一行, 不再注入 heading 或 `---`. 重用 ticket 時, Summary 由 `sectionHeaderKey` 判定: 原本 `##Summary` (CommonMark 不算 heading) 算 Summary, `## Summary ##` 卻不算.
|
|
239
|
+
- `show` 的 change 標題去掉結尾的 `#`, 也不再把 fenced code 裡的 `# x` 當成 H1. dashboard 的 ticket Summary 與 `decision` 的 ADR 標題使用同一條規則.
|
|
240
|
+
- 頂層是 YAML list 的 `tospec/config.yaml` 回報 `config_not_a_mapping`, 原本被靜默忽略.
|
|
241
|
+
- `list --type` 沒有符合項目時印 `No active <type> changes found.`, 原本只印空標題. `list --specs` 與 `dashboard status` 名稱含中文時欄位對齊, 寬度以新的 runtime 相依套件 `fast-string-width` 計算 (原本已經由 `@inquirer/core` 存在於安裝樹中).
|
|
242
|
+
|
|
243
|
+
### 其他
|
|
244
|
+
|
|
245
|
+
- **相依套件**: 新增 runtime 相依套件 `cross-spawn@^7` (`config edit` 執行 `EDITOR`) 與 `fast-string-width` (CJK 欄寬).
|
|
246
|
+
- **README**: 補上 `--tools` 可用逗號列出多個、規則檔包含 `decision.md`、何時不跳出提示 (stdin 不是終端機、設定 `CI`、`TOSPEC_INTERACTIVE=0`)、capability 的命名規則. 也說明改寫既有檔案時保留換行, 一律寫成不含 BOM 的 UTF-8; archive 改寫 main spec 時移除 BOM 的行為維持不變.
|
|
247
|
+
- **repo 內的文件** (不在 npm 套件內): `docs/agent-contract.md` 列出全部六個會部分失敗的命令與 `failed[]` 的欄位, `docs/custom-schema.md` 更正 `apply.tracks` 的語意, `docs/how-to-publish.md` 改為手動 bump 與 annotated tag. skills guide 與 flow 頁面同步本版的 skill 變更, 並移除已不存在的 `/tosx:<id>` command 與已退役的 grill skill.
|
|
248
|
+
- **測試**: 151 個檔案, 2735 個 test. dashboard 的 stop / start 測試改為在測試 process 內以連接埠 0 啟動真的 hub: 原本寫入沒有 server 的 marker, 只因 stale marker 的缺陷才通過. metrics server 的連接埠遞增測試不再假設 `held + 1` 是空的, 原本平行執行時偶發失敗.
|
|
249
|
+
|
|
9
250
|
## [0.24.0] - 2026-10-07
|
|
10
251
|
|
|
11
252
|
本版本的主題是**把 dashboard 從「每個專案一個 server」改成「所有專案共用一個 hub」**. 原本同時在多個專案驅動 agent 的人, 要為每個專案各開一個 server 與一個瀏覽器分頁, 而頁面只看得到檔案, 看不到 session 正在做什麼. 現在 hub 不讀任何專案檔案, 改由 Claude Code 的 `tospec-dashboard` mod 推送 snapshot 與 session 活動. 頁面重畫成跨專案的路線圖, 並可以回答 session 提出的問題. 命令介面隨之改成 `dashboard start|stop|status`.
|
|
@@ -523,6 +764,8 @@ Dashboard 進化為可背景常駐、多專案並存的服務, 並補上 TDD 導
|
|
|
523
764
|
|
|
524
765
|
- 新增 `prepack` script 與 npm publish 的準備設定, 完備套件發行流程.
|
|
525
766
|
|
|
767
|
+
[0.26.0]: https://github.com/seanmars/tospec/compare/v0.25.0...v0.26.0
|
|
768
|
+
[0.25.0]: https://github.com/seanmars/tospec/compare/v0.24.0...v0.25.0
|
|
526
769
|
[0.24.0]: https://github.com/seanmars/tospec/compare/v0.23.0...v0.24.0
|
|
527
770
|
[0.23.0]: https://github.com/seanmars/tospec/compare/v0.22.0...v0.23.0
|
|
528
771
|
[0.22.0]: https://github.com/seanmars/tospec/compare/v0.21.0...v0.22.0
|
package/README.md
CHANGED
|
@@ -36,7 +36,7 @@ tospec validate add-dark-mode
|
|
|
36
36
|
tospec archive add-dark-mode
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
-
`--tools` 可填 `claude`, `agents
|
|
39
|
+
`--tools` 可填 `claude`, `agents` (可用逗號列出多個), `all` 或 `none`. Skill 一律先寫進 `.agents/skills/` 這份 canonical 副本, 再複製到各工具目錄, 所以 `claude` 隱含 `agents`. Codex 直接讀 `.agents/`, 不會另外產生 `.codex/`. `none` 只建 `tospec/` 目錄, 不碰任何 agent 內容.
|
|
40
40
|
|
|
41
41
|
`.claude/skills/` 是複本不是連結, 因為 junction 與 symlink 進 git 的記錄方式不同, 會讓追蹤內容取決於貢獻者的作業系統. 改了 `.agents/skills/` 之後要跑 `tospec update` 才會同步過去.
|
|
42
42
|
|
|
@@ -83,23 +83,27 @@ tospec archive add-dark-mode
|
|
|
83
83
|
|
|
84
84
|
多數指令支援 `--json`, 給 AI 工具讀. 封包格式與 exit code 見 [Agent 契約](./docs/agent-contract.md).
|
|
85
85
|
|
|
86
|
+
stdin 不是 terminal, 有設環境變數 `CI`, 或設了環境變數 `TOSPEC_INTERACTIVE=0` 時, tospec 不跳出互動提示. 需要確認的指令 (例如 `archive`, `config reset --all`) 改為 exit 1, 並提示要加的選項 (例如 `--yes`).
|
|
87
|
+
|
|
88
|
+
tospec 改寫既有檔案時 (`archive` 合併主 specs 與更新 ticket, `new change` 重用 ticket, `decision` 更新索引, dashboard 勾 task), 保留原本的換行 (CRLF 或 LF), 但一律寫成不含 BOM 的 UTF-8. 讀取時, 檔頭的 BOM 視為不存在.
|
|
89
|
+
|
|
86
90
|
### 專案管理
|
|
87
91
|
|
|
88
92
|
| 指令 | 作用 | 選項 |
|
|
89
93
|
|------|------|------|
|
|
90
|
-
| `tospec init [path]` | 初始化專案, 建立 `tospec/`, Skill 與 workflow 規則 | `--tools`, `--
|
|
94
|
+
| `tospec init [path]` | 初始化專案, 建立 `tospec/`, Skill 與 workflow 規則 | `--tools`, `--profile <name>`, `--json` |
|
|
91
95
|
| `tospec update [path]` | 重新產生 Skill 並同步到各工具目錄 | `--force`, `--json` |
|
|
92
96
|
| `tospec rules [path]` | 只刷新 workflow 規則檔, 不動 Skill 與設定 | `--json` |
|
|
93
97
|
| `tospec migrate [openspec-dir]` | 把 OpenSpec 專案轉成 tospec 格式 (預設讀 `./openspec`) | `-f, --force`, `--json` |
|
|
94
98
|
| `tospec config <sub>` | 全域設定: `path`, `list`, `get`, `set`, `unset`, `reset`, `edit`, `profile [preset]` | 各子命令多支援 `--json` |
|
|
95
99
|
|
|
96
|
-
workflow 規則檔 (`.agents/rules/tospec
|
|
100
|
+
workflow 規則檔 (`.agents/rules/tospec/` 下的 `single-source-of-truth.md` 與 `decision.md`, 以及 `.claude/` 下的複本) 由套件產生, 內含版本標記與內容雜湊. 一旦偵測到手改, `init` / `update` / `rules` 都會在寫入任何檔案前整次失敗, `--force` 也不例外. 想保留修改, 先備份再刪掉該檔重跑.
|
|
97
101
|
|
|
98
102
|
### 建立與查詢
|
|
99
103
|
|
|
100
104
|
| 指令 | 作用 | 選項 |
|
|
101
105
|
|------|------|------|
|
|
102
|
-
| `tospec new change <name>` | 建立 change 目錄與 ticket | `--schema <sdd
|
|
106
|
+
| `tospec new change <name>` | 建立 change 目錄與 ticket | `--schema <name>` (預設 `sdd`), `--description`, `--goal`, `--decisions <files>`, `--json` |
|
|
103
107
|
| `tospec list` | 列出進行中的 change | `--specs`, `--changes`, `--sort <recent\|name>`, `--type <requirement\|issue>`, `--json` |
|
|
104
108
|
| `tospec show [item-name]` | 顯示 change 或 spec | `--type <change\|spec>`, `--diff`, `--requirements`, `--no-scenarios`, `-r <id>`, `--no-interactive`, `--json` |
|
|
105
109
|
| `tospec status` | 顯示各文件狀態 | `--change <id>`, `--all`, `--schema <name>`, `--json` |
|
|
@@ -115,10 +119,10 @@ workflow 規則檔 (`.agents/rules/tospec/single-source-of-truth.md` 與 `.claud
|
|
|
115
119
|
|
|
116
120
|
| 指令 | 作用 | 選項 |
|
|
117
121
|
|------|------|------|
|
|
118
|
-
| `tospec validate [item-name]` | 檢查必要段落與 delta spec 格式 | `--all`, `--changes`, `--specs`, `--type
|
|
122
|
+
| `tospec validate [item-name]` | 檢查必要段落與 delta spec 格式 | `--all`, `--changes`, `--specs`, `--type <change\|spec>`, `--strict`, `--concurrency <n>`, `--no-interactive`, `--json` |
|
|
119
123
|
| `tospec archive [change-name]` | 重新驗證, 合併 delta specs, 把 change 移到 archive | `-y`, `--skip-specs`, `--no-validate`, `--require-sync`, `--json` |
|
|
120
124
|
|
|
121
|
-
delta spec 固定放在 `specs/<capability>/spec.md`, 恰好一層, 更深的目錄不會被合併. 完全沒有行為變更的 change (工具, CI, 純文件) 在 `.tospec.yaml` 設 `skip_specs: true` 就好, 不要為了過驗證捏造 requirement.
|
|
125
|
+
delta spec 固定放在 `specs/<capability>/spec.md`, 恰好一層, 更深的目錄不會被合併. 既有 capability 的資料夾沿用 `tospec/specs/` 裡的名稱, 大小寫也要一樣; 只差大小寫是錯誤, 因為不分大小寫的檔案系統會合併, 分大小寫的會另建一份. 新的 capability 用 kebab-case (允許 `中文` 這類沒有大小寫的文字), 不符合是 WARNING. Windows 不能使用的名稱 (`con`, `nul` 這類保留名稱, `<>:"/\|?*`, 結尾的點或空白) 是錯誤. 完全沒有行為變更的 change (工具, CI, 純文件) 在 `.tospec.yaml` 設 `skip_specs: true` 就好, 不要為了過驗證捏造 requirement.
|
|
122
126
|
|
|
123
127
|
requirement 內文慣例上要有 SHALL 或 MUST, 缺了是 WARNING; `--strict` 才會把它當錯誤.
|
|
124
128
|
|
|
@@ -265,8 +265,9 @@ export function staleDaysOf(data) {
|
|
|
265
265
|
// --- time ------------------------------------------------------------------
|
|
266
266
|
|
|
267
267
|
/**
|
|
268
|
-
* `20260713_123649` (archive and decision stamps)
|
|
269
|
-
*
|
|
268
|
+
* `20260713_123649` (archive and decision stamps), `2026-07-13T12:36:49` or
|
|
269
|
+
* `2026-07-13` (change metadata `created`, new and older changes) as a local
|
|
270
|
+
* Date; anything else through Date, or null.
|
|
270
271
|
*/
|
|
271
272
|
export function parseLooseDate(value) {
|
|
272
273
|
if (!value) return null;
|
|
@@ -276,9 +277,14 @@ export function parseLooseDate(value) {
|
|
|
276
277
|
return new Date(+y, +mo - 1, +d, +h, +mi, +s);
|
|
277
278
|
}
|
|
278
279
|
// A bare ISO date parses as UTC midnight, which lands on the previous day
|
|
279
|
-
// west of Greenwich; read it as the local calendar date it names.
|
|
280
|
-
|
|
281
|
-
|
|
280
|
+
// west of Greenwich; read it as the local calendar date it names. The
|
|
281
|
+
// zoneless datetime is read here too rather than left to Date, so neither
|
|
282
|
+
// spelling depends on how the engine treats a value without an offset.
|
|
283
|
+
const iso = /^(\d{4})-(\d{2})-(\d{2})(?:T(\d{2}):(\d{2}):(\d{2}))?$/.exec(value);
|
|
284
|
+
if (iso) {
|
|
285
|
+
const [, y, mo, d, h = '0', mi = '0', s = '0'] = iso;
|
|
286
|
+
return new Date(+y, +mo - 1, +d, +h, +mi, +s);
|
|
287
|
+
}
|
|
282
288
|
const parsed = new Date(value);
|
|
283
289
|
return Number.isNaN(parsed.getTime()) ? null : parsed;
|
|
284
290
|
}
|
|
@@ -10,7 +10,7 @@ const SLOT_KEY = 'tospec-line-slots';
|
|
|
10
10
|
export const store = {
|
|
11
11
|
hubConnected: true,
|
|
12
12
|
projectsLoaded: false,
|
|
13
|
-
/** False when the hub is bound beyond loopback: the page may watch
|
|
13
|
+
/** False when the hub is bound beyond loopback: the page may watch, not answer sessions or tick tasks. */
|
|
14
14
|
interactive: false,
|
|
15
15
|
/** /api/projects, sorted by name so rows do not jump when a push re-sorts the hub's list. */
|
|
16
16
|
projects: [],
|
|
@@ -51,7 +51,9 @@ export async function api(path, options) {
|
|
|
51
51
|
let detail = 'HTTP ' + res.status;
|
|
52
52
|
try {
|
|
53
53
|
const body = await res.json();
|
|
54
|
-
|
|
54
|
+
// `fix` rides along on a project-side refusal (a file where the layout
|
|
55
|
+
// expects a directory); without it the page names the problem only.
|
|
56
|
+
if (body && body.error) detail = body.fix ? body.error + ' ' + body.fix : body.error;
|
|
55
57
|
} catch {
|
|
56
58
|
// No JSON body; keep the status line.
|
|
57
59
|
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
import { el, note, marker, paintLine, icon } from '../lib/dom.js';
|
|
5
5
|
import { STATIONS, SIGNAL_LABEL, stationOf, stationIndex, stationLabel, signalOf, lineSignal, gapOf, unreadableInfo, staleDaysOf, relTime, formatDate, parseLooseDate } from '../lib/derive.js';
|
|
6
|
-
import { dataOf, slotOf, projectByKey, postTask, loadProject, loadDetail } from '../lib/store.js';
|
|
6
|
+
import { store, dataOf, slotOf, projectByKey, postTask, loadProject, loadDetail } from '../lib/store.js';
|
|
7
7
|
import { routes } from '../lib/routes.js';
|
|
8
8
|
import { renderMarkdown, openFiles } from './docs.js';
|
|
9
9
|
|
|
@@ -140,11 +140,13 @@ function taskSection(project, change) {
|
|
|
140
140
|
section.append(note('這個 change 還沒有 task.'));
|
|
141
141
|
return section;
|
|
142
142
|
}
|
|
143
|
-
// The server enforces
|
|
143
|
+
// The server enforces every lock too; the page only mirrors them so a click
|
|
144
144
|
// is not offered that would be refused.
|
|
145
145
|
const locked = change.phase === 'archiving';
|
|
146
|
+
const readOnly = !store.interactive;
|
|
146
147
|
const offline = !project.online;
|
|
147
148
|
if (locked) section.append(el('p', { class: 'lock-note' }, [icon('lock', 14), 'sync 已通過, 正在等 archive; task 已凍結.']));
|
|
149
|
+
else if (readOnly) section.append(el('p', { class: 'lock-note' }, [icon('lock', 14), '這個 dashboard 對外開放 (--allow-remote), 只能檢視; 請在專案中勾選 task.']));
|
|
148
150
|
else if (offline) section.append(el('p', { class: 'lock-note' }, [icon('lock', 14), '沒有 session 連線, 勾選要由 session 寫回檔案, 現在只能閱讀.']));
|
|
149
151
|
|
|
150
152
|
const files = [...new Set(change.tasks.map((t) => t.file))];
|
|
@@ -152,7 +154,7 @@ function taskSection(project, change) {
|
|
|
152
154
|
if (files.length > 1) section.append(el('p', { class: 'task-file mono', text: file }));
|
|
153
155
|
const list = el('ul', { class: 'task-list' });
|
|
154
156
|
for (const task of change.tasks) {
|
|
155
|
-
if (task.file === file) list.append(taskItem(project.key, task, locked || offline));
|
|
157
|
+
if (task.file === file) list.append(taskItem(project.key, task, locked || readOnly || offline));
|
|
156
158
|
}
|
|
157
159
|
section.append(list);
|
|
158
160
|
}
|
package/dist/cli/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,OAAO,EAAgD,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,OAAO,EAAgD,MAAM,WAAW,CAAC;AAkIlF,QAAA,MAAM,OAAO,SAAgB,CAAC;AA0gB9B,OAAO,EAAE,OAAO,EAAE,CAAC;AAyCnB;;;;GAIG;AACH,wBAAsB,MAAM,CAAC,IAAI,WAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAsC/D"}
|