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
@@ -73,8 +73,45 @@ export function copyExecutable(src, dst, dryRun) {
73
73
  }
74
74
  return dst;
75
75
  }
76
- /** Additive, deduped by command; never removes existing entries, so claude-mem stays intact. */
77
- export function mergeClaudeHook(settingsPath, eventName, entry, dryRun) {
76
+ /** Shell operators that chain, pipe or redirect: after the owner key a foreign command runs too. */
77
+ const CHAINED = /[;&|<>`\n\r]|\$\(/;
78
+ /** "owned": the key alone or with flags only; a hook without a string command is "foreign". */
79
+ function ownership(hook, ownerKey) {
80
+ const command = hook.command;
81
+ if (ownerKey === undefined || typeof command !== "string")
82
+ return "foreign";
83
+ if (command === ownerKey)
84
+ return "owned";
85
+ if (!command.startsWith(`${ownerKey} `))
86
+ return "foreign";
87
+ return CHAINED.test(command.slice(ownerKey.length)) ? "chained" : "owned";
88
+ }
89
+ function isOwnedEntry(e, ownerKey) {
90
+ return e.hooks.length > 0 && e.hooks.every((h) => ownership(h, ownerKey) === "owned");
91
+ }
92
+ function runsInEntry(e, ownerKey) {
93
+ return e.hooks.some((h) => ownership(h, ownerKey) !== "foreign");
94
+ }
95
+ function runsCommand(e, command) {
96
+ return e.hooks.some((h) => h.command === command);
97
+ }
98
+ /**
99
+ * Merges one hook entry into a settings event, deduped by command.
100
+ *
101
+ * Without `ownerKey` the merge is purely additive. With it, entries whose every command
102
+ * runs the owned script with flags only are FreeHarness's own: the first is replaced in
103
+ * place when its command or `onFailure` differs and the rest are dropped, so a flag change
104
+ * never leaves a stale copy running beside the new one and an older install gains the
105
+ * fail-closed field. Every other entry, claude-mem's included,
106
+ * stays as is, even one that mixes the owned script with a foreign command or chains one
107
+ * to it; that one is only reported.
108
+ * @param entry Entry to merge; its first command is the dedupe key.
109
+ * @param dryRun Computes the result without writing.
110
+ * @param ownerKey Command prefix that runs the owned script, without flags.
111
+ * @returns What the merge appended, replaced or left running in a shared entry; `collapsed` when
112
+ * several owned copies became one, so the note names a collapse rather than a reset.
113
+ */
114
+ export function mergeClaudeHook(settingsPath, eventName, entry, dryRun, ownerKey) {
78
115
  let settings = {};
79
116
  if (fs.existsSync(settingsPath)) {
80
117
  settings = JSON.parse(fs.readFileSync(settingsPath, "utf8"));
@@ -83,14 +120,40 @@ export function mergeClaudeHook(settingsPath, eventName, entry, dryRun) {
83
120
  settings.hooks[eventName] ??= [];
84
121
  const arr = settings.hooks[eventName];
85
122
  const ourCmd = entry.hooks[0].command;
86
- const exists = arr.some((e) => e.hooks.some((h) => h.command === ourCmd));
87
- if (!exists)
123
+ const ourOnFailure = entry.hooks[0].onFailure;
124
+ const owned = (e) => isOwnedEntry(e, ownerKey);
125
+ const ownedAt = arr.flatMap((e, i) => (owned(e) ? [i] : []));
126
+ const ownedHooks = ownedAt.flatMap((i) => arr[i].hooks);
127
+ const current = ownedAt.length === 1 && ownedHooks.length === 1 && ownedHooks[0].command === ourCmd && ownedHooks[0].onFailure === ourOnFailure;
128
+ const shared = arr.some((e) => runsInEntry(e, ownerKey) && !owned(e));
129
+ let result;
130
+ if (ownedAt.length > 0 && !current) {
131
+ const stale = ownedHooks.find((h) => h.command !== ourCmd)?.command;
132
+ // A current copy already ran fail-closed, so dropping a stale one beside it resets nothing.
133
+ const hasCurrent = ownedHooks.some((h) => h.command === ourCmd && h.onFailure === ourOnFailure);
134
+ const onFailureChanged = !hasCurrent && ownedHooks.some((h) => h.onFailure !== ourOnFailure);
135
+ settings.hooks[eventName] = arr.flatMap((e, i) => (i === ownedAt[0] ? [entry] : owned(e) ? [] : [e]));
136
+ result = {
137
+ outcome: "replaced",
138
+ ...(typeof stale === "string" ? { replacedCommand: stale } : {}),
139
+ ...(onFailureChanged ? { onFailureChanged: true } : {}),
140
+ ...(ownedHooks.length > 1 ? { collapsed: true } : {}),
141
+ };
142
+ }
143
+ else if (!arr.some((e) => runsCommand(e, ourCmd))) {
88
144
  arr.push(entry);
89
- if (!dryRun && !exists) {
145
+ result = { outcome: "added" };
146
+ }
147
+ else {
148
+ result = { outcome: "present" };
149
+ }
150
+ if (!dryRun && result.outcome !== "present") {
90
151
  ensureDir(path.dirname(settingsPath), false);
91
152
  fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), "utf8");
92
153
  }
93
- return { added: !exists };
154
+ // A shared entry already running the current command, with no entry of our own, is the only copy.
155
+ const hasSharedCopy = shared && (result.outcome === "added" || ownedAt.length > 0);
156
+ return { ...result, hasSharedCopy };
94
157
  }
95
158
  /** A key is written only when absent, so a value the user (or another tool) set is never overwritten; `$comment` keys are never copied. */
96
159
  export function mergeSettingsKeys(settingsPath, entries, dryRun) {
@@ -39,9 +39,10 @@ export interface ProfileRecord {
39
39
  * Resolves the manifest path for a scope.
40
40
  * @param scope user | project.
41
41
  * @param projectRoot Project root (project scope).
42
+ * @param home Home whose `.claude` holds the user-scope manifest.
42
43
  * @returns Absolute manifest path.
43
44
  */
44
- export declare function manifestPath(scope: Scope, projectRoot: string): string;
45
+ export declare function manifestPath(scope: Scope, projectRoot: string, home?: string): string;
45
46
  /**
46
47
  * Reads a manifest when present.
47
48
  * @param file Manifest path.
@@ -10,10 +10,11 @@ import { spawnSync } from "node:child_process";
10
10
  * Resolves the manifest path for a scope.
11
11
  * @param scope user | project.
12
12
  * @param projectRoot Project root (project scope).
13
+ * @param home Home whose `.claude` holds the user-scope manifest.
13
14
  * @returns Absolute manifest path.
14
15
  */
15
- export function manifestPath(scope, projectRoot) {
16
- const claudeDir = scope === "user" ? path.join(os.homedir(), ".claude") : path.join(projectRoot, ".claude");
16
+ export function manifestPath(scope, projectRoot, home = os.homedir()) {
17
+ const claudeDir = scope === "user" ? path.join(home, ".claude") : path.join(projectRoot, ".claude");
17
18
  return path.join(claudeDir, "freeharness", "install-manifest.json");
18
19
  }
19
20
  /**
@@ -0,0 +1,18 @@
1
+ /** One installed value moved from a previously shipped price to the current one. */
2
+ export interface PriceCorrection {
3
+ row: string;
4
+ field: string;
5
+ from: number;
6
+ to: number;
7
+ }
8
+ /**
9
+ * Adds shipped rows missing from the installed table, in shipped order, and corrects a value
10
+ * only while it still equals one the shipped history lists, so an owner edit is never touched.
11
+ */
12
+ export declare function mergePriceTable(installed: string, shipped: string): {
13
+ text: string;
14
+ added: string[];
15
+ corrected: PriceCorrection[];
16
+ } | {
17
+ warning: string;
18
+ };
@@ -0,0 +1,64 @@
1
+ function isPlainObject(value) {
2
+ return typeof value === "object" && value !== null && !Array.isArray(value);
3
+ }
4
+ /** True when `history[row][field]` is an array listing `value` as a number; malformed entries never match. */
5
+ function wasShipped(history, row, field, value) {
6
+ if (!isPlainObject(history) || !Object.prototype.hasOwnProperty.call(history, row))
7
+ return false;
8
+ const fields = history[row];
9
+ if (!isPlainObject(fields) || !Object.prototype.hasOwnProperty.call(fields, field))
10
+ return false;
11
+ const values = fields[field];
12
+ return Array.isArray(values) && values.some((v) => typeof v === "number" && v === value);
13
+ }
14
+ /**
15
+ * Adds shipped rows missing from the installed table, in shipped order, and corrects a value
16
+ * only while it still equals one the shipped history lists, so an owner edit is never touched.
17
+ */
18
+ export function mergePriceTable(installed, shipped) {
19
+ let installedObj;
20
+ try {
21
+ installedObj = JSON.parse(installed);
22
+ }
23
+ catch {
24
+ return { warning: "installed price table is not valid JSON" };
25
+ }
26
+ if (!isPlainObject(installedObj))
27
+ return { warning: "installed price table is not a JSON object" };
28
+ if (!isPlainObject(installedObj.prices))
29
+ return { warning: "installed price table has no `prices` object" };
30
+ const shippedObj = JSON.parse(shipped);
31
+ const shippedPrices = shippedObj.prices;
32
+ const history = shippedObj.shipped_history;
33
+ const mergedPrices = { ...installedObj.prices };
34
+ const corrected = [];
35
+ for (const row of Object.keys(mergedPrices)) {
36
+ const current = mergedPrices[row];
37
+ const shippedRow = Object.prototype.hasOwnProperty.call(shippedPrices, row) ? shippedPrices[row] : undefined;
38
+ if (!isPlainObject(current) || !isPlainObject(shippedRow))
39
+ continue;
40
+ let next;
41
+ for (const field of Object.keys(current)) {
42
+ const from = current[field];
43
+ const to = shippedRow[field];
44
+ if (typeof from !== "number" || typeof to !== "number" || from === to)
45
+ continue;
46
+ if (!wasShipped(history, row, field, from))
47
+ continue;
48
+ next ??= { ...current };
49
+ next[field] = to;
50
+ corrected.push({ row, field, from, to });
51
+ }
52
+ if (next)
53
+ mergedPrices[row] = next;
54
+ }
55
+ const added = [];
56
+ for (const key of Object.keys(shippedPrices)) {
57
+ if (Object.prototype.hasOwnProperty.call(mergedPrices, key))
58
+ continue;
59
+ mergedPrices[key] = shippedPrices[key];
60
+ added.push(key);
61
+ }
62
+ const merged = { ...installedObj, prices: mergedPrices };
63
+ return { text: `${JSON.stringify(merged, null, 2)}\n`, added, corrected };
64
+ }
@@ -3,11 +3,20 @@ export type Tier = "strong" | "standard" | "fast";
3
3
  /** OpenCode ids are provider-specific: emitted as env placeholders, not concrete models. */
4
4
  export declare const TIER_MODEL: Record<HarnessTarget, Record<Tier, string>>;
5
5
  export declare function mapTier(target: HarnessTarget, tier: Tier): string;
6
+ /** Reached only by escalation. */
7
+ export type EscalationTier = "frontier";
8
+ export declare const ESCALATION_MODEL: Record<HarnessTarget, string>;
9
+ /** Frontier escalates via `ESCALATION_MODEL`; other tiers via `mapTier`. */
10
+ export declare function mapModel(target: HarnessTarget, tier: Tier | EscalationTier): string;
11
+ /** Neutral effort vocabulary; adapters decide emission. */
12
+ export declare const EFFORT_LEVELS: readonly ["low", "medium", "high", "xhigh", "max"];
13
+ export type EffortLevel = (typeof EFFORT_LEVELS)[number];
14
+ export declare function isEffortLevel(v: unknown): v is EffortLevel;
6
15
  /** Body text of the leading frontmatter block, or undefined without one. */
7
16
  export declare function frontmatterBody(content: string): string | undefined;
8
17
  /** Rebuilds a file's frontmatter fence around `fn`'s rewrite of the body; content unchanged without a fence or when `fn` returns the same body. */
9
18
  export declare function replaceFrontmatterBody(content: string, fn: (body: string) => string): string;
10
- /** Rewrites the frontmatter `model:` tier to its target concrete id (`[ \t]*`, never `\s*`: `\r` is a JS line terminator). */
19
+ /** Maps the frontmatter `model:` tier and, for Claude Code, `cache_ttl:` (`[ \t]*`, never `\s*`: `\r` is a JS line terminator). */
11
20
  export declare function rewriteAgentModel(content: string, target: HarnessTarget): string;
12
21
  /** True when the frontmatter block declares the key at column 0. */
13
22
  export declare function frontmatterHasKey(content: string, key: string): boolean;
@@ -13,6 +13,20 @@ const TIERS = new Set(["strong", "standard", "fast"]);
13
13
  export function mapTier(target, tier) {
14
14
  return TIER_MODEL[target][tier];
15
15
  }
16
+ export const ESCALATION_MODEL = {
17
+ "claude-code": "fable",
18
+ plugin: "fable",
19
+ opencode: "${OPENCODE_FRONTIER_MODEL}",
20
+ };
21
+ /** Frontier escalates via `ESCALATION_MODEL`; other tiers via `mapTier`. */
22
+ export function mapModel(target, tier) {
23
+ return tier === "frontier" ? ESCALATION_MODEL[target] : mapTier(target, tier);
24
+ }
25
+ /** Neutral effort vocabulary; adapters decide emission. */
26
+ export const EFFORT_LEVELS = ["low", "medium", "high", "xhigh", "max"];
27
+ export function isEffortLevel(v) {
28
+ return typeof v === "string" && EFFORT_LEVELS.includes(v);
29
+ }
16
30
  // Frontmatter block, CRLF-tolerant, closing fence may lack a trailing newline: preserves a
17
31
  // file's own line endings for every caller below.
18
32
  const FRONTMATTER_RE = /^---(\r?\n)([\s\S]*?)(\r?\n)---(\r?\n|$)/;
@@ -39,9 +53,17 @@ export function replaceFrontmatterBody(content, fn) {
39
53
  const fenceEndIndex = m.index + m.whole.length;
40
54
  return content.slice(0, m.index) + `---${m.openNl}${body}${m.closeNl}---${m.afterNl}` + content.slice(fenceEndIndex);
41
55
  }
42
- /** Rewrites the frontmatter `model:` tier to its target concrete id (`[ \t]*`, never `\s*`: `\r` is a JS line terminator). */
56
+ // Claude Code reads the TTL only as nested `experimental.cacheTtl`; other adapters strip it.
57
+ const CACHE_TTL_TARGETS = new Set(["claude-code", "plugin"]);
58
+ /** Maps the frontmatter `model:` tier and, for Claude Code, `cache_ttl:` (`[ \t]*`, never `\s*`: `\r` is a JS line terminator). */
43
59
  export function rewriteAgentModel(content, target) {
44
- return replaceFrontmatterBody(content, (body) => body.replace(/^(model:[ \t]*)(\w+)[ \t]*$/m, (line, prefix, value) => TIERS.has(value) ? `${prefix}${mapTier(target, value)}` : line));
60
+ return replaceFrontmatterBody(content, (body) => {
61
+ const mapped = body.replace(/^(model:[ \t]*)(\w+)[ \t]*$/m, (line, prefix, value) => TIERS.has(value) ? `${prefix}${mapTier(target, value)}` : line);
62
+ if (!CACHE_TTL_TARGETS.has(target))
63
+ return mapped;
64
+ const nl = mapped.includes("\r\n") ? "\r\n" : "\n";
65
+ return mapped.replace(/^cache_ttl:[ \t]*(\S+)[ \t]*(\r?)$/m, (_line, value, cr) => `experimental:${nl} cacheTtl: ${value}${cr}`);
66
+ });
45
67
  }
46
68
  // Column-0 key of one frontmatter line, or undefined.
47
69
  function frontmatterKey(line) {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@freeharness/core",
3
- "version": "0.4.1",
3
+ "version": "0.4.2",
4
4
  "private": true,
5
5
  "description": "FreeHarness pure domain: capability registry, install planner, model tiers, install context, and ports. No I/O, zero runtime dependencies.",
6
6
  "type": "module",
@@ -4,8 +4,8 @@ upstream:
4
4
  name: "@fission-ai/openspec"
5
5
  repo: https://github.com/Fission-AI/OpenSpec
6
6
  fork: https://github.com/anegrelli/OpenSpec
7
- baseline_version: "1.13.0"
8
- baseline_commit: "9d4e5974"
7
+ baseline_version: "1.15.0"
8
+ baseline_commit: "6d04a1b5"
9
9
 
10
10
  vendored:
11
11
  assets:
@@ -83,6 +83,8 @@ patches:
83
83
  - "task-progress.ts disposition kinds: `countTasksFromContent` resolves a task only for the documented kinds (superseded, moved, external); any other kind leaves the task open, matching the converge hook and the ERROR `validate --strict` reports for it"
84
84
  - "formatTaskStatus (task-progress.ts): accepts the optional disposition-aware counts, reporting a change complete when `open` is zero and naming the dispositioned count while tasks are still open; a caller passing only `total`/`completed` keeps the upstream reading"
85
85
  - "shim disposition rules (validate/status/archive, FreeHarness addition): validate.ts adds an ERROR for a disposition under a checked task, an unknown kind, an unresolved `superseded`/`moved` target, or an empty `external` reference; status.ts exposes `done`/`open`/`dispositioned` in `--json`, with the tasks resolved (never open) exactly when `open` is zero; archive.ts excludes a dispositioned task from the unchecked-task blocker and prints the dispositioned tasks grouped by kind after the other archive output"
86
+ - "spec-discovery.ts (divergence from the #1870 port of src/utils/spec-discovery.ts, see ports_1_13_2): findUnreadDeltaFiles and findUnreadDeltaFilesSync drive one generator walker (walkUnreadDeltaFiles) that yields each readdir/stat/read call, so the async and sync twins share one set of walk rules and return the same list; the results are unchanged"
87
+ - "specs-apply.ts comment: the RENAMED-vs-REMOVED conflict comment in buildUpdatedSpec is restated as a present-tense counterfactual (a missing REMOVED target is a no-op, so without the explicit rejection the REMOVED silently does nothing); code unchanged"
86
88
 
87
89
  skipped:
88
90
  - "telemetry (PostHog); store/worksets; shell completions; version-check/self-update"
@@ -167,7 +169,282 @@ ports_1_13_ignored:
167
169
  - pr: "#1782"
168
170
  note: "retire_capabilities — capability retirement (retire_capabilities marker + retireSpec deletion path) was not reconstructed in P3 inc.4c's archive shim; retireSpec/finalizeRetiredSpec are vendored in specs-apply.ts but unused by the shim orchestration"
169
171
 
172
+ # vendor-sync-2026-10 ports from upstream 9d4e5974 (1.13.0) -> db230978 (1.13.2, MIT).
173
+ # Each entry names the upstream file(s), the PR, and the vendored target;
174
+ # `sync adopt sdd` never touches these (tracked assets only).
175
+ ports_1_13_2:
176
+ - upstream: src/core/templates/workflows/explore.ts, src/core/templates/workflows/update-change.ts
177
+ pr: "#1731"
178
+ target: vendor/openspec/generation/templates/workflows/{explore,update-change}.ts
179
+ note: "explore and update-change describe an `openspec list --json` entry by name and task status only; the schema field this text once cited is gone from that output"
180
+ - upstream: src/core/templates/workflows/archive-change.ts
181
+ pr: "#1795"
182
+ target: vendor/openspec/generation/templates/workflows/archive-change.ts
183
+ note: "archive-change counts incomplete tasks from the selected change's `totalTasks`/`completedTasks` in `openspec list --json` rather than reading tasks.md directly, so a schema's custom-named or glob task files are counted; a missing or duplicate change entry, invalid JSON, or an inconsistent count stops the workflow before syncing or archiving, and a zero `totalTasks` skips the warning instead of a missing tasks.md doing so"
184
+ - upstream: src/core/templates/workflows/update-change.ts
185
+ pr: "#1733"
186
+ target: vendor/openspec/generation/templates/workflows/update-change.ts
187
+ note: "update-change treats a `skipped` artifact as untouched rather than missing, defers only a `ready`/`blocked` artifact with empty `existingOutputPaths` to the continue workflow, and adds a confirmed, re-verified procedure (fresh instructions, a path inside changeRoot that does not already exist, a create that fails on collision) for adding one concrete file under a glob artifact that already has other files"
188
+ - upstream: schemas/spec-driven/schema.yaml
189
+ pr: "#1955"
190
+ target: "canonical/openspec/schema.spec-driven.yaml (override) + assets/schema.upstream.yaml"
191
+ note: "the tasks instruction requires each task group to land the tests and documentation its own work calls for, reserving a final group for integration checks only, so a group's deferred verification never cascades failures back through the groups after it; FreeHarness addition: the final group may also hold the whole-change gates (full suite, simulate, matrix coverage, baseline bumps)"
192
+ - upstream: src/core/parsers/requirement-blocks.ts, src/core/specs-apply.ts, src/core/validation/validator.ts
193
+ pr: "#1809"
194
+ target: vendor/openspec/validation/parsers/requirement-blocks.ts, vendor/openspec/apply/specs-apply.ts, vendor/openspec/validation/validator.ts
195
+ note: "diffScenarioNames reports both the scenario names a MODIFIED block drops and the ones it introduces, plus both counts, alongside the existing scenario-loss guard; findMissingCurrentScenarios becomes the guard's missing-half read of the same comparison, so archive and validate cannot drift on what they report any more than on what they refuse"
196
+ - upstream: src/core/templates/workflows/{apply-change,archive-change,explore,project-root,propose,sync-specs,update-change}.ts, src/commands/workflow/new-change.ts
197
+ pr: "#1787"
198
+ target: vendor/openspec/generation/templates/workflows/{apply-change,archive-change,explore,project-root,propose,sync-specs,update-change}.ts, src/shim/new-change.ts
199
+ note: "the six emitted workflows carry the shared project-check text (project-root.ts): before the first write, confirm `openspec list --json` reports a root; with none, an auto-selected workflow drops OpenSpec silently while an explicit OpenSpec request stops and asks, and neither branch runs `openspec init` or hand-creates openspec/ as a side effect; `openspec new change` prints a dimmed notice naming the directory when it fell back to an implicit root"
200
+ - upstream: schemas/spec-driven/schema.yaml, src/core/templates/workflows/archive-change.ts, src/utils/task-progress.ts
201
+ pr: "#1773"
202
+ target: canonical/openspec/schema.spec-driven.yaml (override), vendor/openspec/generation/templates/workflows/archive-change.ts, vendor/openspec/engine/task-progress.ts, vendor/openspec/validation/task-lines.ts
203
+ note: "a checkbox is done only when its bracket holds `x`/`X` (any spacing, any case); every other marker, including `- [~]`, `- [-]` and an empty `- []`, counts as open, and a line with no checkbox is not tracked at all"
204
+ - upstream: src/core/templates/workflows/explore.ts
205
+ pr: "#1788"
206
+ target: vendor/openspec/generation/templates/workflows/explore.ts
207
+ note: "explore names the propose workflow at the implement-request handoff, the capture-done handoff, the discovery-end offer, the summary next step, and the don't-implement guardrail; upstream resolves each against the installed workflow set, while explore.ts hardcodes the shipped-workflow wording (`/opsx:propose`, `/opsx:apply`) because FreeHarness always emits both workflows, so no CLI-only fallback is rendered"
208
+ - upstream: src/core/command-generation/generator.ts, src/core/shared/skill-generation.ts, src/core/templates/optional-workflow.ts, src/core/templates/workflows/{apply-change,archive-change,propose,update-change}.ts
209
+ pr: "#1775"
210
+ target: vendor/openspec/generation/templates/workflows/{apply-change,update-change}.ts
211
+ note: "adapted: upstream templates author an `[[opsx:if-workflow <id>]]...[[opsx:else]]...[[opsx:end]]` conditional that generation resolves against the installed workflow set; FreeHarness vendors no resolver and no optional-workflow.ts. The templates are hand-edited with each conditional already resolved for the fixed six-workflow set: continue and new are not emitted, so their handoffs render the CLI fallback, and no emitted text names a workflow FreeHarness does not ship"
212
+ - upstream: src/core/parsers/requirement-blocks.ts, src/core/specs-apply.ts, src/core/validation/validator.ts
213
+ pr: "#1804"
214
+ target: vendor/openspec/validation/parsers/requirement-blocks.ts, vendor/openspec/apply/specs-apply.ts, vendor/openspec/validation/validator.ts
215
+ note: "a well-formed requirement block written outside every delta section is reported as a WARNING naming where it sits, instead of being silently ignored by the section reader"
216
+ - upstream: src/core/templates/workflows/update-change.ts
217
+ pr: "#1840"
218
+ target: vendor/openspec/generation/templates/workflows/update-change.ts
219
+ note: "update-change drafts the requested edit in the conversation during step 4 and performs every artifact write in step 5; no earlier step edits a file, and step 5 shows the drafted edit alongside each proposed revision"
220
+ - upstream: src/utils/task-progress.ts
221
+ pr: "#1862"
222
+ target: vendor/openspec/engine/task-progress.ts, vendor/openspec/validation/task-lines.ts
223
+ note: "the task-line pattern accepts `-`, `*`, `+` and an ordered `1.`/`1)` marker (up to nine digits) ahead of the checkbox, so an unchecked task under a marker other than `-`/`*` is counted instead of read as untracked"
224
+ - upstream: "(FreeHarness, not upstream)"
225
+ pr: "diverges from #1862, #1773"
226
+ target: vendor/openspec/engine/task-progress.ts, vendor/openspec/validation/task-lines.ts, canonical/hooks/freeharness-converge.mjs, canonical/hooks/freeharness-session-start
227
+ note: "the task-line pattern is rewritten in a linear form (`\\[(?:\\s*([^\\]\\s])\\s*\\](?![([])|\\](?![([])|\\s+\\])`): upstream's `\\s*([^\\]\\s]?)\\s*` puts two whitespace runs around an optional token and backtracks quadratically, so one `- [` line of spaces stalls every reader for seconds to minutes. Matches are unchanged except that group 1 is undefined for a blank box, which every done check reads as `(match[1] ?? '').toLowerCase() === 'x'`. The session-start hook also skips lines longer than 10,000 characters before matching"
228
+ - upstream: src/core/templates/workflows/archive-change.ts, src/core/templates/workflows/sync-specs.ts
229
+ pr: "#1701"
230
+ target: vendor/openspec/generation/templates/workflows/{archive-change,sync-specs}.ts
231
+ note: "sync and archive create a new capability's main spec from ADDED requirements only; a delta with a MODIFIED or RENAMED requirement and no existing main spec is reported sync-blocked instead of inventing a prior version, a REMOVED-only delta with no `retire_capabilities` marker is reported blocked (matching archive's 'Spec must have at least one requirement' refusal), and a REMOVED-only delta under that marker is treated as already retired"
232
+ - upstream: src/core/templates/workflows/explore.ts
233
+ pr: "#1832"
234
+ target: vendor/openspec/generation/templates/workflows/explore.ts
235
+ note: "when the user's own request asks explore to capture the exploration as a change, that request is itself the write confirmation the mode otherwise requires, scoped to the change and artifacts the request names; a yes to explore's own offer confirms only the scope that offer named"
236
+ - upstream: src/core/list.ts
237
+ pr: "#1866"
238
+ target: src/shim/list.ts
239
+ note: "a change directory entry that no longer resolves while list dates it (removed mid-listing, a symlink with a missing or looping target) is skipped instead of aborting the whole listing"
240
+ - upstream: src/commands/workflow/instructions.ts, src/core/archive.ts, src/core/validation/validator.ts, src/utils/spec-discovery.ts
241
+ pr: "#1870"
242
+ target: src/shim/instructions.ts, src/shim/archive.ts, vendor/openspec/validation/validator.ts, vendor/openspec/validation/spec-discovery.ts
243
+ note: "a delta file under a change's specs/ that carries delta sections but is not a capability's spec.md (findUnreadDeltaFiles) counts toward archive's has-deltas check, so a change whose only delta content sits at a path the merge never reads is refused instead of archived with nothing merged"
244
+ - upstream: src/core/parsers/markdown-parser.ts, src/core/parsers/requirement-text.ts, src/core/validation/validator.ts
245
+ pr: "#1858"
246
+ target: vendor/openspec/validation/parsers/{markdown-parser,requirement-text}.ts, vendor/openspec/validation/validator.ts
247
+ note: "a scenario header (`#### Scenario: ...`) with no body under it is reported as an error instead of accepted as an empty scenario"
248
+ - upstream: src/core/parsers/change-parser.ts
249
+ pr: "#1856"
250
+ target: vendor/openspec/validation/parsers/change-parser.ts
251
+ note: "the change proposal's What-Changes deltas are read through parseDeltaSpec, the same reader `openspec archive` uses, instead of ChangeParser's own simpler pass, so a proposal and the change it describes cannot disagree on what a delta contains; a REMOVED entry written as a bare name is read as a header-only block"
252
+ - upstream: src/core/parsers/requirement-blocks.ts, src/core/parsers/spec-structure.ts
253
+ pr: "#1860"
254
+ target: vendor/openspec/validation/parsers/requirement-blocks.ts, vendor/openspec/validation/parsers/spec-structure.ts
255
+ note: "normalizeRequirementName drops a trailing run of `#`s preceded by whitespace (an ATX heading's optional closing hashes), so `### Requirement: Foo ###` reads as `Foo`; a name genuinely ending in `#`, like `C#`, is unaffected because the run must be whitespace-preceded; findMainSpecStructureIssues' duplicate-heading check now normalizes through the same function instead of a bare `.trim()`, so a closed heading no longer reads as a distinct duplicate name"
256
+ - upstream: src/core/parsers/requirement-blocks.ts, src/core/specs-apply.ts
257
+ pr: "#1864"
258
+ target: vendor/openspec/validation/parsers/requirement-blocks.ts, vendor/openspec/apply/specs-apply.ts
259
+ note: "foldRequirementName's case- and whitespace-insensitive typo check also covers ADDED headers, not only REMOVED and the RENAMED/REMOVED cross-section conflict, so two ADDED requirement names differing only in case or interior whitespace are refused as a likely typo"
260
+ - upstream: src/core/parsers/requirement-blocks.ts, src/core/specs-apply.ts, src/core/validation/validator.ts
261
+ pr: "#1806"
262
+ target: vendor/openspec/validation/parsers/requirement-blocks.ts, vendor/openspec/apply/specs-apply.ts, vendor/openspec/validation/validator.ts
263
+ note: "a `FROM:`/`TO:` line in `## RENAMED Requirements` that never forms a pair (a second FROM overwriting an unpaired first, a TO with no pending FROM, a trailing FROM at section end) is recorded as an UnpairedRename instead of silently dropped, so validate reports it and buildUpdatedSpec refuses the fold instead of guessing a pairing"
264
+ - upstream: src/core/specs-apply.ts, src/commands/workflow/instructions.ts
265
+ pr: "#1835"
266
+ target: vendor/openspec/apply/specs-apply.ts, src/shim/instructions.ts
267
+ note: "resolveTrustedSpecPath warns when a linked capability spec resolves outside the project root instead of writing there silently; maskHtmlComments becomes a single linear scan instead of a quadratic regex re-scan from every comment opener; printInstructionsText escapes the artifact envelope's tag attributes and inner text (change name, description, rules, dependency description) so a hostile repository's file or directory names cannot forge a sibling tag in the agent-facing instructions output. The commit's dependency-advisory hunks (vitest/fflate version bumps) are excluded: no dependency change is in scope"
268
+ - upstream: src/commands/workflow/status.ts, src/core/change-status-policy.ts
269
+ pr: "#1786"
270
+ target: src/shim/status.ts, vendor/openspec/engine/change-status-policy.ts
271
+ note: "resolveNextStep returns one next step in both a ready-to-run `command` (with any store flag) and a `sentence` form, so the JSON `nextSteps` array and the text surface's printed command name the same next action instead of risking a mismatch between two independent builders"
272
+ - upstream: src/commands/change.ts, src/commands/validate.ts, src/commands/workflow/shared.ts, src/commands/workflow/status.ts, src/core/archive.ts, src/core/list.ts, src/utils/nested-change.ts
273
+ pr: "#1849"
274
+ target: src/shim/{status,list,archive,validate}.ts
275
+ note: "a change laid out under a namespace folder (`changes/<area>/<name>/` instead of directly under `changes/`) is detected from the default schema's root markers (.openspec.yaml, proposal.md, ...) and reported by name and validate, list, status and archive instead of being silently mishandled"
276
+ - upstream: src/core/validation/task-checkboxes.ts, src/core/validation/validator.ts
277
+ pr: "#1774"
278
+ target: src/shim/{shared,status,instructions,archive}.ts
279
+ note: "adapted: when every file the schema tracks as tasks lists items but carries no checkbox (bare `-`/`*`/`+`/ordered bullets with no `[ ]`/`[x]`), findMissingTaskCheckboxWarning returns one warning naming the file and the line of its first bare list item; `status` (JSON warnings and text) and `instructions apply` surface it, and `archive` adds it as an [INFO] blocker, since the archive-blocker gate otherwise reads the zero total as already complete; `validate` does not report it"
280
+ - upstream: src/commands/workflow/instructions.ts, src/commands/workflow/shared.ts
281
+ pr: "#1732"
282
+ target: src/shim/instructions.ts
283
+ note: "generateApplyInstructions resolves every concrete file a glob tracking path matches (resolveArtifactOutputs) instead of one literal path, and reports an unreadable tracking file by path and reason instead of treating it as absent; a change whose tracking evidence is entirely unreadable reports blocked instead of falsely reporting all_done, and unavailableTrackingFiles is carried in the JSON payload alongside taskTrackingConfigured"
284
+ - upstream: src/core/artifact-graph/outputs.ts
285
+ pr: "#1885"
286
+ target: vendor/openspec/engine/outputs.ts
287
+ note: "isGlobPattern recognizes a brace expansion (`{a,b}`, `{1..3}`) and an extglob (`@(...)`, `!(...)`) as a glob in addition to the existing `*`/`?`/`[` check, so an artifact output declared with either is resolved through the glob path instead of treated as one literal filename"
288
+ - upstream: src/core/validation/purpose-placeholder.ts
289
+ pr: "#1912"
290
+ target: vendor/openspec/validation/purpose-placeholder.ts
291
+ note: "a shouted `TBD`/`TODO` opening the Purpose is always the placeholder marker; any other case (`todo`, `Tbd`) is the marker only when punctuation or line end follows it, so lowercase prose that happens to start with those letters in another language (Spanish/Portuguese 'Todo') is not misread as an unwritten Purpose while `todo - write this later` still is"
292
+ - upstream: src/core/specs-apply.ts
293
+ pr: "#1958"
294
+ target: vendor/openspec/apply/specs-apply.ts
295
+ note: "writeUpdatedSpec reads the target spec.md's existing line-ending convention (matchLineEnding/detectLineEnding) before an archive fold rewrites it and writes the rebuilt content back with that same convention instead of always LF, so a spec file already using CRLF keeps every unchanged line unchanged; a spec that does not exist yet stays LF. Scoped to this file: the commit's other hunks (src/utils/file-system.ts's shell-completion marker-block writer, scripts/pack-version-check.mjs) touch no kept path"
296
+
297
+ # vendor-sync-2026-10: kept-path commits between 9d4e5974 and db230978 reviewed and
298
+ # intentionally not ported.
299
+ ports_1_13_2_ignored:
300
+ - pr: "#1962"
301
+ note: "removed-requirement reporting in verify — touches only verify-change.ts; workflow not emitted, text arrives through the adopted asset snapshot"
302
+ - pr: "#1926"
303
+ note: "archive EPERM fallback on Windows rename — no Windows target"
304
+ - pr: "#1769"
305
+ note: "archive lock release on Windows — no Windows target"
306
+ - pr: "#1940"
307
+ note: "approval-threshold wording — touches only ff-change.ts and onboard.ts, neither emitted; workflow not emitted, text arrives through the adopted asset snapshot"
308
+ - pr: "#1777"
309
+ note: "top-level heading in generated artifacts — no markdown lint runs in the gate, the artifacts are read by agents, and the heading adds a line to every artifact of every change; the ported parsers still accept an artifact that has one"
310
+ - pr: "#1852"
311
+ note: "trigger phrases in skill descriptions — the six descriptions are already in the model's skill list on every turn; the slash command is the explicit invocation path"
312
+ - pr: "#1829"
313
+ note: "bulk-archive existing-target check — touches only bulk-archive-change.ts; workflow not emitted, text arrives through the adopted asset snapshot"
314
+
315
+ # vendor-sync-2026-11 ports from upstream db230978 (1.13.2) -> 6d04a1b5 (1.15.0, MIT).
316
+ # One entry per kept-path commit, limited to the hunks that reach emitted workflow text
317
+ # or the store-free shim; `sync adopt sdd` never touches these (tracked assets only).
318
+ ports_1_15_0:
319
+ - commit: "e7a9512d"
320
+ pr: "#1994"
321
+ upstream: src/commands/workflow/instructions.ts, src/commands/workflow/shared.ts, src/core/templates/workflows/apply-change.ts
322
+ target: src/shim/instructions.ts, vendor/openspec/generation/templates/workflows/apply-change.ts
323
+ note: "apply tasks carry their source path and line; our text printer naming path and line is a FreeHarness divergence from upstream's printer"
324
+ - commit: "1bce87f8"
325
+ pr: "#2054"
326
+ upstream: src/core/parsers/requirement-blocks.ts, src/core/specs-apply.ts, src/core/validation/validator.ts
327
+ target: vendor/openspec/validation/parsers/requirement-blocks.ts, vendor/openspec/apply/specs-apply.ts, vendor/openspec/validation/validator.ts, src/shim/archive.ts
328
+ note: "archive warns on delta text it will not carry; the warnings surface through the archive shim"
329
+ - commit: "276d0150"
330
+ pr: "#2104"
331
+ upstream: src/core/templates/workflows/explore.ts, src/core/templates/workflows/propose.ts
332
+ target: vendor/openspec/generation/templates/workflows/{explore,propose}.ts
333
+ note: "glob artifact paths kept inside the change directory; propose and explore halves only, continue-change and ff-change are not emitted"
334
+ - commit: "3fea2a96"
335
+ pr: "#2083"
336
+ upstream: src/core/templates/workflows/update-change.ts
337
+ target: vendor/openspec/generation/templates/workflows/update-change.ts
338
+ note: "glob new-file flow unblocked; update half only, the verify-change half is not emitted"
339
+ - commit: "9cb5ba8c"
340
+ pr: "#2086"
341
+ upstream: src/core/templates/workflows/explore.ts
342
+ target: vendor/openspec/generation/templates/workflows/explore.ts
343
+ note: "explore capture table scoped to the default schema"
344
+ - commit: "0ff63dba"
345
+ pr: "#1759"
346
+ upstream: src/core/templates/workflows/archive-change.ts
347
+ target: vendor/openspec/generation/templates/workflows/archive-change.ts
348
+ note: "archive wording requiring a successful spec sync; archive-change half only, bulk-archive-change is not emitted"
349
+ - commit: "832e2cf8"
350
+ pr: "#2120"
351
+ upstream: src/commands/workflow/new-change.ts
352
+ target: src/shim/new-change.ts
353
+ note: "empty --schema falls back to the default schema; no project config read, since the shim is config-free"
354
+ - commit: "18729828"
355
+ pr: "#1978"
356
+ upstream: src/core/validation/constants.ts
357
+ target: vendor/openspec/validation/constants.ts
358
+ note: "validation constants message only; the schema sentence on the 500-character limit is not adopted (owner decision 2026-10-29)"
359
+ - commit: "7056a580"
360
+ pr: "#1791"
361
+ upstream: src/commands/workflow/instructions.ts, src/core/templates/workflows/apply-change.ts
362
+ target: src/shim/instructions.ts, vendor/openspec/generation/templates/workflows/apply-change.ts
363
+ note: "instruction text only; the schema Workflow follow-up section stays skipped"
364
+ - commit: "e70dcc7c"
365
+ pr: "#1997"
366
+ upstream: schemas/spec-driven/schema.yaml, schemas/spec-driven/templates/proposal.md
367
+ target: canonical/openspec/schema.spec-driven.yaml, canonical/openspec/templates/proposal.md
368
+ note: "capability naming guidance in the proposal instruction and template, wording adapted to the schema override"
369
+ - commit: "852a0738"
370
+ pr: "#2037"
371
+ upstream: schemas/spec-driven/schema.yaml
372
+ target: canonical/openspec/schema.spec-driven.yaml
373
+ note: "spec files written one at a time, wording adapted to the schema override; the continue-change half is not emitted and not ported"
374
+
375
+ # vendor-sync-2026-11: kept-path commits between db230978 and 6d04a1b5 reviewed and
376
+ # intentionally not ported.
377
+ ports_1_15_0_ignored:
378
+ - commit: "760584ba"
379
+ pr: "#2020"
380
+ note: "held: validate --strict would fail on requirements over the length limit, which many of our requirements exceed"
381
+ - commit: "88692b3b"
382
+ pr: "#1925"
383
+ note: "held: warns on unknown .openspec.yaml keys, which our depends_on and supersedes keys would trigger"
384
+ - commit: "e0a2f00f"
385
+ pr: "#2063"
386
+ note: "held: exempts x- extension keys from the unknown-key warning of #1925, which is itself held; it is taken together with #1925 once the warning can coexist with our depends_on and supersedes keys"
387
+ - commit: "6b156199"
388
+ pr: "#2114"
389
+ note: "not emitted: touches only the verify workflow"
390
+ - commit: "43d23ccb"
391
+ pr: "#2028"
392
+ note: "not emitted: touches only the verify workflow"
393
+ - commit: "31c6d1c4"
394
+ pr: "#2137"
395
+ note: "out of slice: standalone binaries for installs without Node.js"
396
+ - commit: "15b3b50f"
397
+ pr: "#2116"
398
+ note: "out of slice: lazy loading of ora in the upstream CLI"
399
+ - commit: "bfa670ed"
400
+ pr: "#2025"
401
+ note: "out of slice: lazy command loading in the upstream CLI"
402
+ - commit: "5251f2ef"
403
+ pr: "#2108"
404
+ note: "out of slice: zero-width TTY spinner, the shim has no spinner"
405
+ - commit: "87a69a2b"
406
+ pr: "#2101"
407
+ note: "symlinked layouts: spec.md symlinks are unsupported upstream per #2135"
408
+ - commit: "90c95c2a"
409
+ pr: "#2052"
410
+ note: "symlinked layouts: symlinked changes and specs directories are unsupported upstream per #2135"
411
+ - commit: "e33245d2"
412
+ pr: "#2103"
413
+ note: "new glob dependency: tinyglobby replaces fast-glob upstream; the shim depends only on yaml and zod and its glob matcher never expands braces; FreeHarness refuses brace-expansion output patterns at schema load instead of expanding them"
414
+ - commit: "7acf2820"
415
+ pr: "#2061"
416
+ note: "new glob dependency: brace nesting limit in the upstream glob expansion, which the shim does not use"
417
+ - commit: "ba0f5087"
418
+ pr: "#1792"
419
+ note: "none: retirement cleanup reporting, the shim archive has no retire path"
420
+ - commit: "cf2859a5"
421
+ pr: "#2014"
422
+ note: "none: store-backed edit roots, the shim is store-free"
423
+ - commit: "77281941"
424
+ pr: "#1977"
425
+ note: "none: the hunk routes the archive sync handoff through optionalWorkflow, which our template drops; sync is always emitted, so the archive skill keeps naming /opsx:sync and the hunk is not taken"
426
+ - commit: "d28fb49c"
427
+ pr: "#1972"
428
+ note: "none: show JSON requirement names, no show command is vendored"
429
+ - commit: "c2304240"
430
+ pr: "#2109"
431
+ note: "out of slice: update --dry-run; FreeHarness update has its own two-pass gate"
432
+ - commit: "c879d13d"
433
+ pr: "#883"
434
+ note: "out of slice: code studio tool adapter; adapters other than claude and opencode are trimmed"
435
+ - commit: "d1642cb5"
436
+ pr: "#1352"
437
+ note: "out of slice: easycode tool adapter; adapters other than claude and opencode are trimmed"
438
+ - commit: "3de7c72c"
439
+ pr: "#1211"
440
+ note: "out of slice: atomcode tool adapter; adapters other than claude and opencode are trimmed"
441
+ - commit: "3c3e6e3d"
442
+ pr: "#1961"
443
+ note: "out of slice: GigaCode tool adapter; adapters other than claude and opencode are trimmed"
444
+
170
445
  roadmap:
446
+ - "vendor-sync-2026-10 (sdd) adopt: DONE — `sync adopt sdd` adopted the tracked assets (assets/schema.upstream.yaml, assets/skills) at db230978; baseline bumped here and in sources.yaml to db230978 / 1.13.2; post-adopt `sync check sdd` reports up to date (task 5.2)"
447
+ - "vendor-sync-2026-10 (sdd) triage: DONE — 36 upstream commits 9d4e5974..db230978 on kept paths triaged into ports_1_13_2 (29) and ports_1_13_2_ignored (7) above; #1733 and #1940 resolved against the emitted templates (ported, ignored respectively); #1835 limited to its specs-apply.ts/instructions.ts hunks, dependency-advisory hunks excluded; #1958 limited to its specs-apply.ts hunk, Windows-only/non-kept-path hunks excluded"
171
448
  - "vendor-sync-2026-09 (sdd): DONE — 1.13 engine/template ports (ports_1_13 above), tracked assets adopted at 9d4e5974, sources.yaml bumped to 9d4e5974 / 1.13.0"
172
449
  - "P3 inc.3: DONE — vendored change-validation slice (parsers + schemas + Validator) + src/validate-change.ts"
173
450
  - "P3 inc.4: DONE — store-free `openspec` CLI shim (src/bin/openspec.ts + src/shim/*) over the vendored engine: status/instructions/list/schemas/context/validate/new-change fully working"