freeharness 0.4.1 → 0.4.2

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.
Files changed (194) hide show
  1. package/README.md +8 -7
  2. package/canonical/HARVEST.yaml +25 -14
  3. package/canonical/PROVENANCE.yaml +1 -1
  4. package/canonical/agents/README.md +77 -36
  5. package/canonical/agents/agentic-architect.md +2 -1
  6. package/canonical/agents/database-reviewer.md +6 -8
  7. package/canonical/agents/explorer-ai-engineer-analyst.md +2 -6
  8. package/canonical/agents/implementer.md +30 -0
  9. package/canonical/agents/pr-reviewer.md +9 -11
  10. package/canonical/agents/python-reviewer.md +7 -9
  11. package/canonical/agents/report-formatter.md +7 -3
  12. package/canonical/agents/security-reviewer.md +7 -8
  13. package/canonical/agents/test-writer.md +5 -2
  14. package/canonical/hooks/cost-prices.json +11 -9
  15. package/canonical/hooks/freeharness-config-guard.mjs +8072 -87
  16. package/canonical/hooks/freeharness-context-monitor.mjs +88 -54
  17. package/canonical/hooks/freeharness-converge.mjs +78 -23
  18. package/canonical/hooks/freeharness-cost-tracker.mjs +1129 -218
  19. package/canonical/hooks/freeharness-memory-guard.mjs +20 -23
  20. package/canonical/hooks/freeharness-precompact.mjs +217 -87
  21. package/canonical/hooks/freeharness-quota-tap.mjs +173 -0
  22. package/canonical/hooks/freeharness-reload-context.mjs +197 -0
  23. package/canonical/hooks/freeharness-repeat-tool.mjs +1 -1
  24. package/canonical/hooks/freeharness-sdd-context.mjs +970 -0
  25. package/canonical/hooks/freeharness-session-start +297 -59
  26. package/canonical/hooks/hooks.claude-code.json +1 -1
  27. package/canonical/hooks/hooks.config-guard.claude-code.json +3 -2
  28. package/canonical/hooks/hooks.context-monitor.claude-code.json +1 -1
  29. package/canonical/hooks/hooks.converge.claude-code.json +1 -1
  30. package/canonical/hooks/hooks.memory-guard.claude-code.json +3 -2
  31. package/canonical/hooks/hooks.reload-context.claude-code.json +15 -0
  32. package/canonical/openspec/schema.spec-driven.yaml +29 -8
  33. package/canonical/openspec/templates/follow-ups.md +3 -2
  34. package/canonical/openspec/templates/proposal.md +1 -1
  35. package/canonical/openspec/templates/tasks.md +8 -0
  36. package/canonical/rules/CLAUDE.md +5 -4
  37. package/canonical/rules/routing.md +10 -9
  38. package/canonical/skills/agent-evaluation/SKILL.md +17 -6
  39. package/canonical/skills/agent-evaluation/evals/cases.yaml +38 -23
  40. package/canonical/skills/agentic-architecture/SKILL.md +4 -0
  41. package/canonical/skills/agentic-architecture/evals/cases.yaml +27 -0
  42. package/canonical/skills/code-review/SKILL.md +9 -2
  43. package/canonical/skills/code-review/evals/cases.yaml +1118 -27
  44. package/canonical/skills/git-workflow/SKILL.md +2 -0
  45. package/canonical/skills/local-code/SKILL.md +2 -2
  46. package/canonical/skills/local-code/evals/cases.yaml +1 -1
  47. package/canonical/skills/report-formatting/SKILL.md +10 -2
  48. package/canonical/skills/report-formatting/reference/ARCHITECTURE.md +3 -3
  49. package/canonical/skills/report-formatting/reference/HELPERS.md +10 -5
  50. package/canonical/skills/report-formatting/reference/RELEASE.md +1 -2
  51. package/canonical/skills/report-formatting/reference/TEMPLATES.md +3 -2
  52. package/canonical/skills/report-formatting/reference/authoring.md +25 -37
  53. package/canonical/skills/report-formatting/scripts/build-architecture.js +1 -1
  54. package/canonical/skills/report-formatting/scripts/build-md-claro.js +5 -8
  55. package/canonical/skills/report-formatting/scripts/check-env.js +0 -4
  56. package/canonical/skills/report-formatting/scripts/docmodel.js +3 -11
  57. package/canonical/skills/report-formatting/scripts/example-build.js +8 -10
  58. package/canonical/skills/report-formatting/scripts/example_build_ArchitectureReview_docx.js +8 -10
  59. package/canonical/skills/report-formatting/scripts/example_build_ExecutiveReport_docx.js +8 -10
  60. package/canonical/skills/report-formatting/scripts/theme-claro.js +39 -10
  61. package/canonical/skills/subagent-driven-development/SKILL.md +35 -28
  62. package/canonical/skills/subagent-driven-development/evals/cases.yaml +3496 -19
  63. package/canonical/skills/subagent-driven-development/reference/brief.md +185 -35
  64. package/canonical/skills/subagent-driven-development/reference/handoff.md +82 -2
  65. package/canonical/skills/subagent-driven-development/reference/loop.md +57 -0
  66. package/canonical/skills/subagent-driven-development/reference/trees.md +40 -0
  67. package/canonical/skills/subagent-driven-development/reference/workspace.md +31 -0
  68. package/canonical/skills/subagent-driven-development/scripts/validate-handoff.mjs +4771 -326
  69. package/dist/adapters/harness/claude-code.d.ts +19 -3
  70. package/dist/adapters/harness/claude-code.js +204 -39
  71. package/dist/adapters/harness/opencode.js +16 -2
  72. package/dist/adapters/harness/plugin.js +28 -17
  73. package/dist/adapters/memory/doctor.d.ts +174 -29
  74. package/dist/adapters/memory/doctor.js +758 -134
  75. package/dist/adapters/memory/migrate.d.ts +95 -0
  76. package/dist/adapters/memory/migrate.js +497 -0
  77. package/dist/adapters/memory/paths.d.ts +22 -0
  78. package/dist/adapters/memory/paths.js +85 -0
  79. package/dist/adapters/memory/vendored.d.ts +56 -29
  80. package/dist/adapters/memory/vendored.js +191 -154
  81. package/dist/adapters/sync/diff.js +4 -1
  82. package/dist/cli/cleanup-prompt.d.ts +35 -0
  83. package/dist/cli/cleanup-prompt.js +68 -0
  84. package/dist/cli/cleanup.d.ts +22 -24
  85. package/dist/cli/cleanup.js +182 -68
  86. package/dist/cli/cost-quota.d.ts +158 -0
  87. package/dist/cli/cost-quota.js +645 -0
  88. package/dist/cli/cost.d.ts +408 -46
  89. package/dist/cli/cost.js +1731 -57
  90. package/dist/cli/doctor.d.ts +52 -13
  91. package/dist/cli/doctor.js +193 -44
  92. package/dist/cli/hard-cleanup.d.ts +93 -0
  93. package/dist/cli/hard-cleanup.js +552 -0
  94. package/dist/cli/install-facts.d.ts +28 -0
  95. package/dist/cli/install-facts.js +66 -0
  96. package/dist/cli/json-key-edit.d.ts +18 -0
  97. package/dist/cli/json-key-edit.js +118 -0
  98. package/dist/cli/memory-import.d.ts +74 -0
  99. package/dist/cli/memory-import.js +469 -0
  100. package/dist/cli/plugin-validate.d.ts +12 -0
  101. package/dist/cli/plugin-validate.js +43 -0
  102. package/dist/cli/uninstall.d.ts +1 -1
  103. package/dist/cli/uninstall.js +2 -2
  104. package/dist/cli/upstream-launchers.d.ts +49 -0
  105. package/dist/cli/upstream-launchers.js +287 -0
  106. package/dist/index.js +247 -70
  107. package/dist/infra/fsutil.d.ts +31 -3
  108. package/dist/infra/fsutil.js +69 -6
  109. package/dist/infra/manifest.d.ts +2 -1
  110. package/dist/infra/manifest.js +3 -2
  111. package/dist/infra/prices.d.ts +18 -0
  112. package/dist/infra/prices.js +64 -0
  113. package/node_modules/@freeharness/core/dist/tiers.d.ts +10 -1
  114. package/node_modules/@freeharness/core/dist/tiers.js +24 -2
  115. package/node_modules/@freeharness/core/package.json +1 -1
  116. package/node_modules/@freeharness/sdd/PROVENANCE.yaml +279 -2
  117. package/node_modules/@freeharness/sdd/dist/src/bin/openspec.js +34 -8
  118. package/node_modules/@freeharness/sdd/dist/src/shim/archive.d.ts +7 -6
  119. package/node_modules/@freeharness/sdd/dist/src/shim/archive.js +72 -64
  120. package/node_modules/@freeharness/sdd/dist/src/shim/follow-up.d.ts +48 -16
  121. package/node_modules/@freeharness/sdd/dist/src/shim/follow-up.js +168 -70
  122. package/node_modules/@freeharness/sdd/dist/src/shim/instructions.d.ts +36 -6
  123. package/node_modules/@freeharness/sdd/dist/src/shim/instructions.js +110 -49
  124. package/node_modules/@freeharness/sdd/dist/src/shim/list.d.ts +1 -1
  125. package/node_modules/@freeharness/sdd/dist/src/shim/list.js +57 -14
  126. package/node_modules/@freeharness/sdd/dist/src/shim/nested-change.d.ts +7 -0
  127. package/node_modules/@freeharness/sdd/dist/src/shim/nested-change.js +118 -0
  128. package/node_modules/@freeharness/sdd/dist/src/shim/new-change.js +3 -1
  129. package/node_modules/@freeharness/sdd/dist/src/shim/proposal-capabilities.d.ts +12 -0
  130. package/node_modules/@freeharness/sdd/dist/src/shim/proposal-capabilities.js +94 -0
  131. package/node_modules/@freeharness/sdd/dist/src/shim/shared.d.ts +17 -0
  132. package/node_modules/@freeharness/sdd/dist/src/shim/shared.js +182 -1
  133. package/node_modules/@freeharness/sdd/dist/src/shim/status.d.ts +2 -2
  134. package/node_modules/@freeharness/sdd/dist/src/shim/status.js +34 -5
  135. package/node_modules/@freeharness/sdd/dist/src/shim/validate.d.ts +9 -0
  136. package/node_modules/@freeharness/sdd/dist/src/shim/validate.js +49 -41
  137. package/node_modules/@freeharness/sdd/dist/src/shim/warnings.d.ts +21 -0
  138. package/node_modules/@freeharness/sdd/dist/src/shim/warnings.js +66 -0
  139. package/node_modules/@freeharness/sdd/dist/vendor/openspec/apply/line-endings.d.ts +4 -0
  140. package/node_modules/@freeharness/sdd/dist/vendor/openspec/apply/line-endings.js +17 -0
  141. package/node_modules/@freeharness/sdd/dist/vendor/openspec/apply/specs-apply.js +95 -22
  142. package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/change-status-policy.d.ts +6 -0
  143. package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/change-status-policy.js +14 -6
  144. package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/index.d.ts +1 -1
  145. package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/index.js +1 -1
  146. package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/outputs.d.ts +3 -3
  147. package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/outputs.js +24 -4
  148. package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/task-progress.js +4 -7
  149. package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/types.js +13 -1
  150. package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/apply-change.js +21 -6
  151. package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/archive-change.js +80 -19
  152. package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/explore.js +42 -22
  153. package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/project-root.d.ts +32 -0
  154. package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/project-root.js +39 -0
  155. package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/propose.js +16 -9
  156. package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/sync-specs.js +36 -1
  157. package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/update-change.js +65 -30
  158. package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/constants.d.ts +1 -1
  159. package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/constants.js +1 -1
  160. package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/parsers/change-parser.d.ts +8 -13
  161. package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/parsers/change-parser.js +79 -102
  162. package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/parsers/markdown-parser.js +4 -9
  163. package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/parsers/requirement-blocks.d.ts +50 -10
  164. package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/parsers/requirement-blocks.js +165 -37
  165. package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/parsers/requirement-text.d.ts +24 -3
  166. package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/parsers/requirement-text.js +55 -8
  167. package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/parsers/spec-structure.js +4 -1
  168. package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/purpose-placeholder.js +19 -9
  169. package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/spec-discovery.d.ts +10 -0
  170. package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/spec-discovery.js +115 -0
  171. package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/task-lines.d.ts +3 -3
  172. package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/task-lines.js +20 -20
  173. package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/validator.d.ts +6 -0
  174. package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/validator.js +75 -14
  175. package/node_modules/@freeharness/sdd/package.json +1 -1
  176. package/node_modules/@freeharness/sdd/vendor/openspec/assets/schema.upstream.yaml +47 -7
  177. package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-apply-change/SKILL.md +25 -7
  178. package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-archive-change/SKILL.md +50 -11
  179. package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-bulk-archive-change/SKILL.md +48 -10
  180. package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-continue-change/SKILL.md +27 -7
  181. package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-explore/SKILL.md +25 -12
  182. package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-ff-change/SKILL.md +17 -5
  183. package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-new-change/SKILL.md +12 -1
  184. package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-onboard/SKILL.md +47 -33
  185. package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-propose/SKILL.md +17 -5
  186. package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-sync-specs/SKILL.md +29 -1
  187. package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-update-change/SKILL.md +31 -13
  188. package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-verify-change/SKILL.md +75 -27
  189. package/package.json +6 -4
  190. package/provenance/canonical.PROVENANCE.yaml +1 -1
  191. package/provenance/memory.PROVENANCE.yaml +458 -6
  192. package/provenance/sdd.PROVENANCE.yaml +279 -2
  193. package/canonical/hooks/freeharness-auto-memory.mjs +0 -158
  194. package/canonical/hooks/hooks.auto-memory.claude-code.json +0 -16
@@ -11,8 +11,9 @@
11
11
  // Root resolution is store-free: walk up from the cwd to the nearest openspec/
12
12
  // directory (src/root-resolver.ts). Its PARENT is the project root passed to the
13
13
  // engine. `context` may run without a root (to report no_openspec_root); `new
14
- // change` falls back to the cwd as an implicit root; every other command
15
- // requires a root.
14
+ // change` falls back to the cwd as an implicit root; `list` also accepts a null
15
+ // root (it reports `root: null` and exits 1 instead of throwing, so a caller can
16
+ // probe for a project without a write); every other command requires a root.
16
17
  import * as path from 'node:path';
17
18
  import { findOpenspecDir } from '../root-resolver.js';
18
19
  import { statusHandler } from '../shim/status.js';
@@ -23,9 +24,21 @@ import { contextHandler } from '../shim/context.js';
23
24
  import { validateHandler } from '../shim/validate.js';
24
25
  import { newChangeHandler } from '../shim/new-change.js';
25
26
  import { archiveHandler } from '../shim/archive.js';
26
- import { followUpAddHandler, followUpClaimHandler, followUpListHandler } from '../shim/follow-up.js';
27
+ import { FOLLOW_UP_TYPES, followUpAddHandler, followUpClaimHandler, followUpListHandler } from '../shim/follow-up.js';
27
28
  /** Flags that consume the following token as their value. */
28
- const VALUE_FLAGS = new Set(['--change', '--schema', '--description', '--sort', '--store', '--type', '--from', '--by']);
29
+ const VALUE_FLAGS = new Set([
30
+ '--change',
31
+ '--schema',
32
+ '--description',
33
+ '--sort',
34
+ '--store',
35
+ '--type',
36
+ '--from',
37
+ '--by',
38
+ '--expected',
39
+ '--current',
40
+ '--impact',
41
+ ]);
29
42
  function parseArgs(argv) {
30
43
  const positionals = [];
31
44
  const flags = {};
@@ -89,7 +102,9 @@ async function main() {
89
102
  case 'schemas':
90
103
  return schemasHandler(projectRootFrom(cwd) ?? '', { json });
91
104
  case 'list':
92
- return await listHandler(requireRoot(cwd), {
105
+ // Root-optional: a caller probing for a project (the /opsx pre-write check)
106
+ // reads `root` from a valid JSON document instead of parsing a thrown error.
107
+ return await listHandler(projectRootFrom(cwd), {
93
108
  json,
94
109
  sort: flags['--sort'] ?? 'recent',
95
110
  });
@@ -122,7 +137,16 @@ async function main() {
122
137
  case 'follow-up': {
123
138
  const root = requireRoot(cwd);
124
139
  const sub = positionals[0];
125
- const options = { json, from: flags['--from'], by: flags['--by'] };
140
+ const value = (flag) => flags[flag];
141
+ const options = {
142
+ json,
143
+ from: value('--from'),
144
+ by: value('--by'),
145
+ type: value('--type'),
146
+ expected: value('--expected'),
147
+ current: value('--current'),
148
+ impact: value('--impact'),
149
+ };
126
150
  if (sub === 'add')
127
151
  return followUpAddHandler(root, positionals[1], positionals[2], options);
128
152
  if (sub === 'list')
@@ -150,8 +174,10 @@ function printUsage() {
150
174
  ' schemas [--json]',
151
175
  ' context [--json]',
152
176
  ' archive <id> [--yes] [--force] [--json]',
153
- ' follow-up add <change-id> "<text>" [--from <task>] [--json]',
154
- ' follow-up list [--json]',
177
+ " follow-up add <change-id> '<text>' --type <type> --expected '<text>' --current '<text>' --impact '<text>' [--from <task>]",
178
+ ' [--json]',
179
+ ` <type>: ${FOLLOW_UP_TYPES.join('|')}`,
180
+ ' follow-up list [--type <type>|untyped] [--json]',
155
181
  ' follow-up claim <change-id> <index> --by <change-id> [--json]',
156
182
  ].join('\n'));
157
183
  }
@@ -1,7 +1,9 @@
1
1
  import type { ValidationIssue } from '../../vendor/openspec/validation/types.js';
2
2
  import { type UnresolvedDeltaHeader } from '../../vendor/openspec/validation/delta-resolution.js';
3
+ import { type AnnotatedTask } from '../../vendor/openspec/validation/task-lines.js';
3
4
  import { type ChangeMetadata } from '../../vendor/openspec/engine/change-metadata.js';
4
5
  import { type FollowUpItem } from './follow-up.js';
6
+ import { type ShimWarning } from './warnings.js';
5
7
  /** One `Supersedes:` link resolved against openspec/changes/archive/. */
6
8
  export interface SupersessionLink {
7
9
  /** The superseded (archived) change id. */
@@ -24,7 +26,7 @@ export declare function checkSupersession(changeName: string, changeDir: string,
24
26
  * Lines that describe what archive would refuse or what is still open. The
25
27
  * unresolved headers are listed here as well so one refusal names everything.
26
28
  */
27
- export declare function listArchiveBlockers(changeDir: string, issues: ValidationIssue[], unresolved?: UnresolvedDeltaHeader[]): string[];
29
+ export declare function listArchiveBlockers(changeDir: string, projectRoot: string, tasks: AnnotatedTask[], issues: ValidationIssue[], unresolved?: UnresolvedDeltaHeader[]): string[];
28
30
  /** One unchecked task resolved outside this change. */
29
31
  export interface ArchivedDisposition {
30
32
  kind: string;
@@ -58,8 +60,10 @@ export interface ArchiveChangeResult {
58
60
  skipped: UnresolvedDeltaHeader[];
59
61
  /** Unchecked tasks resolved outside this change, grouped for display by kind. */
60
62
  dispositions: ArchivedDisposition[];
61
- /** The archived change's unclaimed follow-ups, printed as a notice (never a blocker). */
63
+ /** The archived change's unclaimed follow-ups; reported as a notice, never a blocker. */
62
64
  followUps: FollowUpItem[];
65
+ /** Delta text the fold left out of the main specs; the archive still completes. */
66
+ warnings: ShimWarning[];
63
67
  }
64
68
  /**
65
69
  * Archive a change: validate it, fold its delta specs into openspec/specs/, then
@@ -74,8 +78,5 @@ export interface ArchiveOptions {
74
78
  yes?: boolean;
75
79
  force?: boolean;
76
80
  }
77
- /**
78
- * CLI wrapper for `openspec archive <id>`. Returns the process exit code and
79
- * emits either the single-document `--json` result or human-readable lines.
80
- */
81
+ /** CLI wrapper for `openspec archive <id>`: a `--json` document or plain lines; returns the exit code. */
81
82
  export declare function archiveHandler(projectRoot: string, changeName: string | undefined, options: ArchiveOptions): Promise<number>;
@@ -7,7 +7,7 @@
7
7
  // an invalid change and (2) folds every delta before moving the change, never a
8
8
  // move-without-fold that would leave openspec/specs/ stale.
9
9
  //
10
- // V4-P4 additions:
10
+ // Checks and links around the fold:
11
11
  // - Archive blockers (#1710 findArchiveBlockers, reported INFO by the validator)
12
12
  // and unchecked tasks are listed before anything is written; without `--yes`
13
13
  // they refuse the archive (upstream prompts; this shim is non-interactive).
@@ -29,13 +29,15 @@ import * as fssync from 'node:fs';
29
29
  import * as path from 'node:path';
30
30
  import { Validator } from '../../vendor/openspec/validation/validator.js';
31
31
  import { findSpecUpdates, buildUpdatedSpec, writeUpdatedSpec, } from '../../vendor/openspec/apply/specs-apply.js';
32
- import { parseDeltaSpec } from '../../vendor/openspec/validation/parsers/requirement-blocks.js';
32
+ import { describeUnappliedContent, findUnappliedContent, parseDeltaSpec, } from '../../vendor/openspec/validation/parsers/requirement-blocks.js';
33
33
  import { discoverSpecFiles } from '../../vendor/openspec/validation/spec-discovery.js';
34
34
  import { findUnresolvedDeltaHeaders, } from '../../vendor/openspec/validation/delta-resolution.js';
35
35
  import { parseAnnotatedTaskLines } from '../../vendor/openspec/validation/task-lines.js';
36
36
  import { readChangeMetadata } from '../../vendor/openspec/engine/change-metadata.js';
37
- import { changesDirFor, resolveChangeId, taskLabel, validateChangeExists } from './shared.js';
38
- import { readFollowUps } from './follow-up.js';
37
+ import { changesDirFor, findMissingTaskCheckboxWarning, resolveChangeId, taskLabel, validateChangeExists, } from './shared.js';
38
+ import { formatFollowUpGroups, readFollowUps } from './follow-up.js';
39
+ import { dispositionReason } from './validate.js';
40
+ import { unappliedDeltaContentWarning } from './warnings.js';
39
41
  const SUPERSEDES_LINE = /^\s*Supersedes:\s*(.+?)\s*$/gm;
40
42
  const SUPERSEDED_BY_PREFIX = 'Superseded-by:';
41
43
  /**
@@ -147,11 +149,18 @@ async function writeSupersededBy(link, changeName) {
147
149
  const separator = existing.length === 0 ? '' : existing.endsWith('\n') ? '\n' : '\n\n';
148
150
  await fs.writeFile(designPath, `${existing}${separator}${line}\n`, 'utf-8');
149
151
  }
152
+ /** The change's tasks.md parsed once for archive's task checks; empty when absent. */
153
+ function readTasks(changeDir) {
154
+ const tasksPath = path.join(changeDir, 'tasks.md');
155
+ if (!fssync.existsSync(tasksPath))
156
+ return [];
157
+ return parseAnnotatedTaskLines(fssync.readFileSync(tasksPath, 'utf-8'));
158
+ }
150
159
  /**
151
160
  * Lines that describe what archive would refuse or what is still open. The
152
161
  * unresolved headers are listed here as well so one refusal names everything.
153
162
  */
154
- export function listArchiveBlockers(changeDir, issues, unresolved = []) {
163
+ export function listArchiveBlockers(changeDir, projectRoot, tasks, issues, unresolved = []) {
155
164
  const blockers = [];
156
165
  for (const issue of issues) {
157
166
  if (issue.level === 'INFO' && /^Archive would refuse this delta/.test(issue.message)) {
@@ -160,37 +169,22 @@ export function listArchiveBlockers(changeDir, issues, unresolved = []) {
160
169
  }
161
170
  for (const u of unresolved)
162
171
  blockers.push(`[WARNING] ${u.entryPath}: ${u.message}`);
163
- const tasksPath = path.join(changeDir, 'tasks.md');
164
- if (fssync.existsSync(tasksPath)) {
165
- // A dispositioned task is resolved outside this change (see
166
- // dispositionedTasks below), so it is never a blocker; only a plain
167
- // unchecked task refuses the archive.
168
- const open = parseAnnotatedTaskLines(fssync.readFileSync(tasksPath, 'utf-8')).filter((t) => !t.done && !t.disposition);
169
- if (open.length > 0) {
170
- blockers.push(`[INFO] tasks.md: ${open.length} unchecked task(s); first: ${open[0].description || '(no description)'}`);
171
- }
172
+ // A dispositioned task is resolved outside this change (see
173
+ // dispositionedTasks below), so it is never a blocker; only a plain
174
+ // unchecked task refuses the archive.
175
+ const open = tasks.filter((t) => !t.done && !t.disposition);
176
+ if (open.length > 0) {
177
+ blockers.push(`[INFO] tasks.md: ${open.length} unchecked task(s); first: ${open[0].description || '(no description)'}`);
172
178
  }
173
- return blockers;
174
- }
175
- const DISPOSITION_KINDS = new Set(['superseded', 'moved', 'external']);
176
- /**
177
- * Why a task's disposition does not resolve it, or null when it does: a kind
178
- * outside the three documented ones, an `external` line without a reference,
179
- * or a `superseded`/`moved` target `resolveChangeId` cannot place in flight or
180
- * archived. Re-checked here (not merely trusted from an earlier `validate`
181
- * run) the same way `listDependencyBlockers` below re-checks `depends_on`.
182
- */
183
- function dispositionBlockerReason(projectRoot, disposition) {
184
- const { kind, target } = disposition;
185
- if (!DISPOSITION_KINDS.has(kind)) {
186
- return `disposition kind '${kind}' is none of superseded, moved, external`;
187
- }
188
- if (kind === 'external') {
189
- return target.trim() === '' ? 'an external disposition needs a reference' : null;
179
+ // A tasks file written as plain bullets carries zero checkboxes, so the
180
+ // scan above finds no open task and archive would otherwise proceed with
181
+ // unfinished work unflagged (#1774).
182
+ const checkboxWarning = findMissingTaskCheckboxWarning(changeDir, projectRoot);
183
+ if (checkboxWarning) {
184
+ // The warning already opens with `<path>:<line>`, so it takes no path prefix.
185
+ blockers.push(`[INFO] ${checkboxWarning}`);
190
186
  }
191
- return resolveChangeId(projectRoot, target) === null
192
- ? `${kind} disposition names unresolved change '${target}'`
193
- : null;
187
+ return blockers;
194
188
  }
195
189
  /**
196
190
  * The change's dispositioned tasks (unchecked, carrying a `Disposition:`
@@ -199,11 +193,7 @@ function dispositionBlockerReason(projectRoot, disposition) {
199
193
  * disposition does not resolve is still listed here for display, but also
200
194
  * comes back as a blocker from `listDispositionBlockers`.
201
195
  */
202
- function dispositionedTasks(changeDir) {
203
- const tasksPath = path.join(changeDir, 'tasks.md');
204
- if (!fssync.existsSync(tasksPath))
205
- return [];
206
- const tasks = parseAnnotatedTaskLines(fssync.readFileSync(tasksPath, 'utf-8'));
196
+ function dispositionedTasks(tasks) {
207
197
  const out = [];
208
198
  for (const task of tasks) {
209
199
  if (task.done || !task.disposition)
@@ -221,18 +211,16 @@ function dispositionedTasks(changeDir) {
221
211
  * unknown kind, an unresolved `superseded`/`moved` target, or an empty
222
212
  * `external` reference), naming the task and the reason. Such a task stays a
223
213
  * blocker exactly like a plain unchecked task; a resolving disposition never
224
- * appears here.
214
+ * appears here. The reasons are `validate`'s, re-checked here (not merely
215
+ * trusted from an earlier `validate` run) the same way
216
+ * `listDependencyBlockers` below re-checks `depends_on`.
225
217
  */
226
- function listDispositionBlockers(projectRoot, changeDir) {
227
- const tasksPath = path.join(changeDir, 'tasks.md');
228
- if (!fssync.existsSync(tasksPath))
229
- return [];
230
- const tasks = parseAnnotatedTaskLines(fssync.readFileSync(tasksPath, 'utf-8'));
218
+ function listDispositionBlockers(projectRoot, tasks) {
231
219
  const blockers = [];
232
220
  for (const task of tasks) {
233
221
  if (task.done || !task.disposition)
234
222
  continue;
235
- const reason = dispositionBlockerReason(projectRoot, task.disposition);
223
+ const reason = dispositionReason(projectRoot, task.disposition);
236
224
  if (reason) {
237
225
  blockers.push(`[INFO] tasks.md: task ${taskLabel(task)} disposition does not resolve it (${reason})`);
238
226
  }
@@ -281,6 +269,18 @@ async function forcedFoldBlockers(changeDir, specsDir, changeName) {
281
269
  }
282
270
  return out;
283
271
  }
272
+ /**
273
+ * The fold's warnings about delta text it leaves behind, told apart from its
274
+ * other warnings by the span description they open with, so the orphan and
275
+ * `--force` notices keep their own handling.
276
+ */
277
+ async function unappliedContentWarnings(update, foldWarnings) {
278
+ const content = await fs.readFile(update.source, 'utf-8');
279
+ const prefixes = findUnappliedContent(content).map((lost) => `${update.id} - ${describeUnappliedContent(lost)} (line ${lost.line})`);
280
+ return foldWarnings
281
+ .filter((message) => prefixes.some((prefix) => message.startsWith(prefix)))
282
+ .map((message) => unappliedDeltaContentWarning(update.id, message));
283
+ }
284
284
  /**
285
285
  * Archive a change: validate it, fold its delta specs into openspec/specs/, then
286
286
  * move openspec/changes/<name>/ to openspec/changes/archive/<name>/.
@@ -329,18 +329,19 @@ export async function archiveChange(changeName, { projectRoot, yes, force }) {
329
329
  // unchecked tasks. Listed together; without --yes they refuse the archive
330
330
  // with nothing written (upstream asks; this shim cannot).
331
331
  const unresolved = await findUnresolvedDeltaHeaders(changeDir, specsDir, changesDir);
332
+ const tasks = readTasks(changeDir);
332
333
  const dependencyBlockers = listDependencyBlockers(projectRoot, metadata);
333
- const dispositionBlockers = listDispositionBlockers(projectRoot, changeDir);
334
+ const dispositionBlockers = listDispositionBlockers(projectRoot, tasks);
334
335
  const blockers = force
335
336
  ? [
336
337
  ...(await forcedFoldBlockers(changeDir, specsDir, name)),
337
- ...listArchiveBlockers(changeDir, report.issues.filter((i) => !/^Archive would refuse this delta/.test(i.message))),
338
+ ...listArchiveBlockers(changeDir, projectRoot, tasks, report.issues.filter((i) => !/^Archive would refuse this delta/.test(i.message))),
338
339
  ...metadataBlockers,
339
340
  ...dependencyBlockers,
340
341
  ...dispositionBlockers,
341
342
  ]
342
343
  : [
343
- ...listArchiveBlockers(changeDir, report.issues, unresolved),
344
+ ...listArchiveBlockers(changeDir, projectRoot, tasks, report.issues, unresolved),
344
345
  ...metadataBlockers,
345
346
  ...dependencyBlockers,
346
347
  ...dispositionBlockers,
@@ -360,9 +361,9 @@ export async function archiveChange(changeName, { projectRoot, yes, force }) {
360
361
  }
361
362
  // 1d. Supersession links (design.md `Supersedes:`), checked before any write.
362
363
  const supersedes = await checkSupersession(name, changeDir, changesDir, metadata);
363
- // 1e. Dispositioned tasks, read while the change still sits at changeDir (the
364
- // move below relocates it).
365
- const dispositions = dispositionedTasks(changeDir);
364
+ // 1e. Dispositioned tasks, from the parse taken while the change still sat at
365
+ // changeDir (the move below relocates it).
366
+ const dispositions = dispositionedTasks(tasks);
366
367
  // 2. Fold the change's delta specs into openspec/specs/. buildUpdatedSpec does
367
368
  // the ADDED/MODIFIED/REMOVED/RENAMED merge; writeUpdatedSpec writes it in
368
369
  // place. A fold that resolves to zero operations is an already-synced
@@ -379,9 +380,11 @@ export async function archiveChange(changeName, { projectRoot, yes, force }) {
379
380
  const updates = await findSpecUpdates(changeDir, specsDir);
380
381
  const plan = [];
381
382
  const skipUnresolved = force === true && unresolved.length > 0;
383
+ const warnings = [];
382
384
  for (const update of updates) {
383
385
  const built = await buildUpdatedSpec(update, name, { silent: true, skipUnresolved });
384
386
  plan.push({ update, rebuilt: built.rebuilt, counts: built.counts });
387
+ warnings.push(...(await unappliedContentWarnings(update, built.warnings)));
385
388
  }
386
389
  const applied = [];
387
390
  for (const { update, rebuilt, counts } of plan) {
@@ -403,17 +406,20 @@ export async function archiveChange(changeName, { projectRoot, yes, force }) {
403
406
  // 4. Reverse links: the superseded archived record names its successor.
404
407
  for (const link of supersedes)
405
408
  await writeSupersededBy(link, name);
406
- // 5. Open follow-ups, read from the archived location (the file moved with the
407
- // directory in step 3): printed as a notice, never a blocker. An item is open
408
- // while its checkbox is unchecked, the rule `follow-up list`, `follow-up claim`
409
- // and the session-start pointer read it by, so the four never disagree.
409
+ // 5. Open follow-ups, read where step 3 moved them: a notice, never a blocker. Open
410
+ // means unchecked, as for `follow-up list`, `claim` and the session-start pointer.
410
411
  const followUps = readFollowUps(archivedTo).filter((item) => !item.done);
411
- return { applied, archivedTo, supersedes, skipped: skipUnresolved ? unresolved : [], dispositions, followUps };
412
+ return {
413
+ applied,
414
+ archivedTo,
415
+ supersedes,
416
+ skipped: skipUnresolved ? unresolved : [],
417
+ dispositions,
418
+ followUps,
419
+ warnings,
420
+ };
412
421
  }
413
- /**
414
- * CLI wrapper for `openspec archive <id>`. Returns the process exit code and
415
- * emits either the single-document `--json` result or human-readable lines.
416
- */
422
+ /** CLI wrapper for `openspec archive <id>`: a `--json` document or plain lines; returns the exit code. */
417
423
  export async function archiveHandler(projectRoot, changeName, options) {
418
424
  try {
419
425
  const result = await archiveChange(changeName, {
@@ -431,6 +437,7 @@ export async function archiveHandler(projectRoot, changeName, options) {
431
437
  skipped: result.skipped.map((u) => ({ operation: u.operation, header: u.header, spec: u.specId, ...(u.addedBy ? { addedBy: u.addedBy } : {}) })),
432
438
  dispositions: result.dispositions,
433
439
  followUps: result.followUps,
440
+ warnings: result.warnings,
434
441
  }, null, 2));
435
442
  }
436
443
  else {
@@ -446,6 +453,8 @@ export async function archiveHandler(projectRoot, changeName, options) {
446
453
  for (const u of result.skipped) {
447
454
  console.log(`Warning (--force): skipped ${u.operation} "${u.header}" in specs/${u.entryPath} — ${u.message}`);
448
455
  }
456
+ for (const warning of result.warnings)
457
+ console.log(`Warning: ${warning.message}`);
449
458
  if (result.dispositions.length > 0) {
450
459
  console.log('Dispositions:');
451
460
  const byKind = new Map();
@@ -461,9 +470,8 @@ export async function archiveHandler(projectRoot, changeName, options) {
461
470
  }
462
471
  if (result.followUps.length > 0) {
463
472
  console.log(`follow-ups open: ${result.followUps.length}`);
464
- for (const item of result.followUps) {
465
- const suffix = item.from ? ` (from ${item.from})` : '';
466
- console.log(` ${item.index}. ${item.text}${suffix}`);
473
+ for (const line of formatFollowUpGroups(result.followUps, { heading: ' ', item: ' ' }, (item) => `${item.index}. `)) {
474
+ console.log(line);
467
475
  }
468
476
  }
469
477
  console.log(`Archived change to ${result.archivedTo}`);
@@ -1,3 +1,9 @@
1
+ export declare const TEMPLATE: string;
2
+ /** The known item types, in display order; `untyped` follows them. */
3
+ export declare const FOLLOW_UP_TYPES: readonly ["defect", "hardening", "refactor", "test-gap", "docs", "design", "owner-step", "upstream"];
4
+ /** A known type, or `untyped` for an item line without a known `[<type>]` tag. */
5
+ export type FollowUpType = (typeof FOLLOW_UP_TYPES)[number] | 'untyped';
6
+ export declare const DISPLAY_ORDER: readonly FollowUpType[];
1
7
  /** One follow-up item, its 1-based position among item lines in file order. */
2
8
  export interface FollowUpItem {
3
9
  index: number;
@@ -7,32 +13,43 @@ export interface FollowUpItem {
7
13
  done: boolean;
8
14
  /** The claiming change id, null for an item checked by hand with no `-> claimed by` suffix. */
9
15
  claimedBy: string | null;
16
+ type: FollowUpType;
17
+ /** Context lines under the item; an absent line reads as "". */
18
+ expected: string;
19
+ current: string;
20
+ impact: string;
10
21
  }
11
22
  /**
12
- * Reads every item line of a change's follow-ups.md, ignoring any other line
13
- * (heading, blank, comment). Index is the 1-based position among item lines in
14
- * file order. Absent file -> empty list.
23
+ * Reads every item of a change's follow-ups.md with its type and context.
24
+ * Absent file -> empty list.
15
25
  */
16
26
  export declare function readFollowUps(dir: string): FollowUpItem[];
27
+ /** A new item as `follow-up add` receives it; every field is validated before any write. */
28
+ export interface NewFollowUp {
29
+ text: string;
30
+ type: string;
31
+ expected: string;
32
+ current: string;
33
+ impact: string;
34
+ from?: string;
35
+ }
17
36
  export interface AddFollowUpResult {
18
37
  change: string;
19
38
  index: number;
20
39
  }
21
40
  /**
22
- * Appends an unclaimed item to a change's follow-ups.md, creating the file from
23
- * the template when absent. Throws when the change resolves neither in flight
24
- * nor archived.
41
+ * Appends an unclaimed item and its three context lines to a change's
42
+ * follow-ups.md in one write, creating the file from the template when absent.
43
+ * Throws, writing nothing, on an invalid field or when the change resolves
44
+ * neither in flight nor archived.
25
45
  */
26
- export declare function addFollowUp(projectRoot: string, changeId: string, text: string, from?: string): AddFollowUpResult;
27
- export interface ListedFollowUp {
46
+ export declare function addFollowUp(projectRoot: string, changeId: string, item: NewFollowUp): AddFollowUpResult;
47
+ export type ListedFollowUp = Omit<FollowUpItem, 'done' | 'claimedBy'> & {
28
48
  change: string;
29
- index: number;
30
- text: string;
31
- from: string | null;
32
49
  state: 'open';
33
- }
34
- /** Every open (unchecked) item of every in-flight and archived change. */
35
- export declare function listFollowUps(projectRoot: string): ListedFollowUp[];
50
+ };
51
+ /** Every open (unchecked) item of every in-flight and archived change, optionally of one type. */
52
+ export declare function listFollowUps(projectRoot: string, type?: FollowUpType): ListedFollowUp[];
36
53
  export interface ClaimFollowUpResult {
37
54
  change: string;
38
55
  index: number;
@@ -48,10 +65,25 @@ export interface FollowUpCommandOptions {
48
65
  json?: boolean;
49
66
  from?: string;
50
67
  by?: string;
68
+ type?: string;
69
+ expected?: string;
70
+ current?: string;
71
+ impact?: string;
51
72
  }
52
- /** CLI wrapper for `openspec follow-up add <change-id> "<text>" [--from <task>]`. */
73
+ /**
74
+ * Lines grouped by type, with C0, DEL, C1, U+2028 and U+2029 removed from each,
75
+ * since a hand-edited record never passed add's validation; `--json` prints raw values.
76
+ */
77
+ export declare function formatFollowUpGroups<T extends Omit<FollowUpItem, 'done' | 'claimedBy'>>(items: readonly T[], indent: {
78
+ heading: string;
79
+ item: string;
80
+ }, label: (item: T) => string): string[];
81
+ /**
82
+ * CLI wrapper for `openspec follow-up add <change-id> '<text>' --type <type>
83
+ * --expected '<text>' --current '<text>' --impact '<text>' [--from <task>]`.
84
+ */
53
85
  export declare function followUpAddHandler(projectRoot: string, changeId: string | undefined, text: string | undefined, options: FollowUpCommandOptions): number;
54
- /** CLI wrapper for `openspec follow-up list [--json]`. */
86
+ /** CLI wrapper for `openspec follow-up list [--type <type>] [--json]`. */
55
87
  export declare function followUpListHandler(projectRoot: string, options: FollowUpCommandOptions): number;
56
88
  /** CLI wrapper for `openspec follow-up claim <change-id> <index> --by <change-id>`. */
57
89
  export declare function followUpClaimHandler(projectRoot: string, changeId: string | undefined, indexArg: string | undefined, options: FollowUpCommandOptions): number;