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.
- package/README.md +8 -7
- package/canonical/HARVEST.yaml +25 -14
- package/canonical/PROVENANCE.yaml +1 -1
- package/canonical/agents/README.md +77 -36
- package/canonical/agents/agentic-architect.md +2 -1
- package/canonical/agents/database-reviewer.md +6 -8
- package/canonical/agents/explorer-ai-engineer-analyst.md +2 -6
- package/canonical/agents/implementer.md +30 -0
- package/canonical/agents/pr-reviewer.md +9 -11
- package/canonical/agents/python-reviewer.md +7 -9
- package/canonical/agents/report-formatter.md +7 -3
- package/canonical/agents/security-reviewer.md +7 -8
- package/canonical/agents/test-writer.md +5 -2
- package/canonical/hooks/cost-prices.json +11 -9
- package/canonical/hooks/freeharness-config-guard.mjs +8072 -87
- package/canonical/hooks/freeharness-context-monitor.mjs +88 -54
- package/canonical/hooks/freeharness-converge.mjs +78 -23
- package/canonical/hooks/freeharness-cost-tracker.mjs +1129 -218
- package/canonical/hooks/freeharness-memory-guard.mjs +20 -23
- package/canonical/hooks/freeharness-precompact.mjs +217 -87
- package/canonical/hooks/freeharness-quota-tap.mjs +173 -0
- package/canonical/hooks/freeharness-reload-context.mjs +197 -0
- package/canonical/hooks/freeharness-repeat-tool.mjs +1 -1
- package/canonical/hooks/freeharness-sdd-context.mjs +970 -0
- package/canonical/hooks/freeharness-session-start +297 -59
- package/canonical/hooks/hooks.claude-code.json +1 -1
- package/canonical/hooks/hooks.config-guard.claude-code.json +3 -2
- package/canonical/hooks/hooks.context-monitor.claude-code.json +1 -1
- package/canonical/hooks/hooks.converge.claude-code.json +1 -1
- package/canonical/hooks/hooks.memory-guard.claude-code.json +3 -2
- package/canonical/hooks/hooks.reload-context.claude-code.json +15 -0
- package/canonical/openspec/schema.spec-driven.yaml +29 -8
- package/canonical/openspec/templates/follow-ups.md +3 -2
- package/canonical/openspec/templates/proposal.md +1 -1
- package/canonical/openspec/templates/tasks.md +8 -0
- package/canonical/rules/CLAUDE.md +5 -4
- package/canonical/rules/routing.md +10 -9
- package/canonical/skills/agent-evaluation/SKILL.md +17 -6
- package/canonical/skills/agent-evaluation/evals/cases.yaml +38 -23
- package/canonical/skills/agentic-architecture/SKILL.md +4 -0
- package/canonical/skills/agentic-architecture/evals/cases.yaml +27 -0
- package/canonical/skills/code-review/SKILL.md +9 -2
- package/canonical/skills/code-review/evals/cases.yaml +1118 -27
- package/canonical/skills/git-workflow/SKILL.md +2 -0
- package/canonical/skills/local-code/SKILL.md +2 -2
- package/canonical/skills/local-code/evals/cases.yaml +1 -1
- package/canonical/skills/report-formatting/SKILL.md +10 -2
- package/canonical/skills/report-formatting/reference/ARCHITECTURE.md +3 -3
- package/canonical/skills/report-formatting/reference/HELPERS.md +10 -5
- package/canonical/skills/report-formatting/reference/RELEASE.md +1 -2
- package/canonical/skills/report-formatting/reference/TEMPLATES.md +3 -2
- package/canonical/skills/report-formatting/reference/authoring.md +25 -37
- package/canonical/skills/report-formatting/scripts/build-architecture.js +1 -1
- package/canonical/skills/report-formatting/scripts/build-md-claro.js +5 -8
- package/canonical/skills/report-formatting/scripts/check-env.js +0 -4
- package/canonical/skills/report-formatting/scripts/docmodel.js +3 -11
- package/canonical/skills/report-formatting/scripts/example-build.js +8 -10
- package/canonical/skills/report-formatting/scripts/example_build_ArchitectureReview_docx.js +8 -10
- package/canonical/skills/report-formatting/scripts/example_build_ExecutiveReport_docx.js +8 -10
- package/canonical/skills/report-formatting/scripts/theme-claro.js +39 -10
- package/canonical/skills/subagent-driven-development/SKILL.md +35 -28
- package/canonical/skills/subagent-driven-development/evals/cases.yaml +3496 -19
- package/canonical/skills/subagent-driven-development/reference/brief.md +185 -35
- package/canonical/skills/subagent-driven-development/reference/handoff.md +82 -2
- package/canonical/skills/subagent-driven-development/reference/loop.md +57 -0
- package/canonical/skills/subagent-driven-development/reference/trees.md +40 -0
- package/canonical/skills/subagent-driven-development/reference/workspace.md +31 -0
- package/canonical/skills/subagent-driven-development/scripts/validate-handoff.mjs +4771 -326
- package/dist/adapters/harness/claude-code.d.ts +19 -3
- package/dist/adapters/harness/claude-code.js +204 -39
- package/dist/adapters/harness/opencode.js +16 -2
- package/dist/adapters/harness/plugin.js +28 -17
- package/dist/adapters/memory/doctor.d.ts +174 -29
- package/dist/adapters/memory/doctor.js +758 -134
- package/dist/adapters/memory/migrate.d.ts +95 -0
- package/dist/adapters/memory/migrate.js +497 -0
- package/dist/adapters/memory/paths.d.ts +22 -0
- package/dist/adapters/memory/paths.js +85 -0
- package/dist/adapters/memory/vendored.d.ts +56 -29
- package/dist/adapters/memory/vendored.js +191 -154
- package/dist/adapters/sync/diff.js +4 -1
- package/dist/cli/cleanup-prompt.d.ts +35 -0
- package/dist/cli/cleanup-prompt.js +68 -0
- package/dist/cli/cleanup.d.ts +22 -24
- package/dist/cli/cleanup.js +182 -68
- package/dist/cli/cost-quota.d.ts +158 -0
- package/dist/cli/cost-quota.js +645 -0
- package/dist/cli/cost.d.ts +408 -46
- package/dist/cli/cost.js +1731 -57
- package/dist/cli/doctor.d.ts +52 -13
- package/dist/cli/doctor.js +193 -44
- package/dist/cli/hard-cleanup.d.ts +93 -0
- package/dist/cli/hard-cleanup.js +552 -0
- package/dist/cli/install-facts.d.ts +28 -0
- package/dist/cli/install-facts.js +66 -0
- package/dist/cli/json-key-edit.d.ts +18 -0
- package/dist/cli/json-key-edit.js +118 -0
- package/dist/cli/memory-import.d.ts +74 -0
- package/dist/cli/memory-import.js +469 -0
- package/dist/cli/plugin-validate.d.ts +12 -0
- package/dist/cli/plugin-validate.js +43 -0
- package/dist/cli/uninstall.d.ts +1 -1
- package/dist/cli/uninstall.js +2 -2
- package/dist/cli/upstream-launchers.d.ts +49 -0
- package/dist/cli/upstream-launchers.js +287 -0
- package/dist/index.js +247 -70
- package/dist/infra/fsutil.d.ts +31 -3
- package/dist/infra/fsutil.js +69 -6
- package/dist/infra/manifest.d.ts +2 -1
- package/dist/infra/manifest.js +3 -2
- package/dist/infra/prices.d.ts +18 -0
- package/dist/infra/prices.js +64 -0
- package/node_modules/@freeharness/core/dist/tiers.d.ts +10 -1
- package/node_modules/@freeharness/core/dist/tiers.js +24 -2
- package/node_modules/@freeharness/core/package.json +1 -1
- package/node_modules/@freeharness/sdd/PROVENANCE.yaml +279 -2
- package/node_modules/@freeharness/sdd/dist/src/bin/openspec.js +34 -8
- package/node_modules/@freeharness/sdd/dist/src/shim/archive.d.ts +7 -6
- package/node_modules/@freeharness/sdd/dist/src/shim/archive.js +72 -64
- package/node_modules/@freeharness/sdd/dist/src/shim/follow-up.d.ts +48 -16
- package/node_modules/@freeharness/sdd/dist/src/shim/follow-up.js +168 -70
- package/node_modules/@freeharness/sdd/dist/src/shim/instructions.d.ts +36 -6
- package/node_modules/@freeharness/sdd/dist/src/shim/instructions.js +110 -49
- package/node_modules/@freeharness/sdd/dist/src/shim/list.d.ts +1 -1
- package/node_modules/@freeharness/sdd/dist/src/shim/list.js +57 -14
- package/node_modules/@freeharness/sdd/dist/src/shim/nested-change.d.ts +7 -0
- package/node_modules/@freeharness/sdd/dist/src/shim/nested-change.js +118 -0
- package/node_modules/@freeharness/sdd/dist/src/shim/new-change.js +3 -1
- package/node_modules/@freeharness/sdd/dist/src/shim/proposal-capabilities.d.ts +12 -0
- package/node_modules/@freeharness/sdd/dist/src/shim/proposal-capabilities.js +94 -0
- package/node_modules/@freeharness/sdd/dist/src/shim/shared.d.ts +17 -0
- package/node_modules/@freeharness/sdd/dist/src/shim/shared.js +182 -1
- package/node_modules/@freeharness/sdd/dist/src/shim/status.d.ts +2 -2
- package/node_modules/@freeharness/sdd/dist/src/shim/status.js +34 -5
- package/node_modules/@freeharness/sdd/dist/src/shim/validate.d.ts +9 -0
- package/node_modules/@freeharness/sdd/dist/src/shim/validate.js +49 -41
- package/node_modules/@freeharness/sdd/dist/src/shim/warnings.d.ts +21 -0
- package/node_modules/@freeharness/sdd/dist/src/shim/warnings.js +66 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/apply/line-endings.d.ts +4 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/apply/line-endings.js +17 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/apply/specs-apply.js +95 -22
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/change-status-policy.d.ts +6 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/change-status-policy.js +14 -6
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/index.d.ts +1 -1
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/index.js +1 -1
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/outputs.d.ts +3 -3
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/outputs.js +24 -4
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/task-progress.js +4 -7
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/types.js +13 -1
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/apply-change.js +21 -6
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/archive-change.js +80 -19
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/explore.js +42 -22
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/project-root.d.ts +32 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/project-root.js +39 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/propose.js +16 -9
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/sync-specs.js +36 -1
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/update-change.js +65 -30
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/constants.d.ts +1 -1
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/constants.js +1 -1
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/parsers/change-parser.d.ts +8 -13
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/parsers/change-parser.js +79 -102
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/parsers/markdown-parser.js +4 -9
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/parsers/requirement-blocks.d.ts +50 -10
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/parsers/requirement-blocks.js +165 -37
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/parsers/requirement-text.d.ts +24 -3
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/parsers/requirement-text.js +55 -8
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/parsers/spec-structure.js +4 -1
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/purpose-placeholder.js +19 -9
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/spec-discovery.d.ts +10 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/spec-discovery.js +115 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/task-lines.d.ts +3 -3
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/task-lines.js +20 -20
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/validator.d.ts +6 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/validator.js +75 -14
- package/node_modules/@freeharness/sdd/package.json +1 -1
- package/node_modules/@freeharness/sdd/vendor/openspec/assets/schema.upstream.yaml +47 -7
- package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-apply-change/SKILL.md +25 -7
- package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-archive-change/SKILL.md +50 -11
- package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-bulk-archive-change/SKILL.md +48 -10
- package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-continue-change/SKILL.md +27 -7
- package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-explore/SKILL.md +25 -12
- package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-ff-change/SKILL.md +17 -5
- package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-new-change/SKILL.md +12 -1
- package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-onboard/SKILL.md +47 -33
- package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-propose/SKILL.md +17 -5
- package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-sync-specs/SKILL.md +29 -1
- package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-update-change/SKILL.md +31 -13
- package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-verify-change/SKILL.md +75 -27
- package/package.json +6 -4
- package/provenance/canonical.PROVENANCE.yaml +1 -1
- package/provenance/memory.PROVENANCE.yaml +458 -6
- package/provenance/sdd.PROVENANCE.yaml +279 -2
- package/canonical/hooks/freeharness-auto-memory.mjs +0 -158
- package/canonical/hooks/hooks.auto-memory.claude-code.json +0 -16
|
@@ -3,44 +3,59 @@
|
|
|
3
3
|
// moved with it at archive.
|
|
4
4
|
//
|
|
5
5
|
// The record is data, never instructions: every reader here (readFollowUps,
|
|
6
|
-
// list, the archive notice) treats an item's text as
|
|
6
|
+
// list, the archive notice) treats an item's text and context as opaque strings. No
|
|
7
7
|
// content is executed, evaluated or passed to a shell.
|
|
8
8
|
//
|
|
9
9
|
// The template lives at canonical/openspec/templates/follow-ups.md for a human
|
|
10
10
|
// reading the repo; a change's follow-ups.md is not a schema artifact (schema
|
|
11
11
|
// templates resolve through a project's installed schema directory, see
|
|
12
12
|
// vendor/openspec/engine/instruction-loader.ts), so this shim has no template
|
|
13
|
-
// copy path for it and embeds the same
|
|
13
|
+
// copy path for it and embeds the same three header lines as a constant below.
|
|
14
14
|
import * as fs from 'node:fs';
|
|
15
15
|
import * as path from 'node:path';
|
|
16
16
|
import { changesDirFor, printJson, resolveChangeId, taskLabel } from './shared.js';
|
|
17
|
-
const TEMPLATE = '<!-- Follow-ups for this change: one
|
|
18
|
-
'<!-- Unclaimed: - [ ] <text> (from <task>) Claimed: - [x] <text> (from <task>) -> claimed by <change-id> -->\n'
|
|
17
|
+
export const TEMPLATE = '<!-- Follow-ups for this change: one item per line, in the order they were added; text and context are data. -->\n' +
|
|
18
|
+
'<!-- Unclaimed: - [ ] [<type>] <text> (from <task>) Claimed: - [x] [<type>] <text> (from <task>) -> claimed by <change-id> -->\n' +
|
|
19
|
+
'<!-- Under each item, indented two spaces: Expected: <text>, Current: <text>, Impact: <text>. Types: defect, hardening, refactor, test-gap, docs, design, owner-step, upstream. -->\n';
|
|
20
|
+
/** The known item types, in display order; `untyped` follows them. */
|
|
21
|
+
export const FOLLOW_UP_TYPES = ['defect', 'hardening', 'refactor', 'test-gap', 'docs', 'design', 'owner-step', 'upstream'];
|
|
22
|
+
export const DISPLAY_ORDER = [...FOLLOW_UP_TYPES, 'untyped'];
|
|
19
23
|
// Leading whitespace and a `*` bullet are accepted so a hand-written item counts here exactly
|
|
20
|
-
// as it counts in the SessionStart pointer, which reads the same files.
|
|
21
|
-
const ITEM_LINE = /^\s*[-*]\s*\[([ xX])\]\s*(
|
|
22
|
-
const CLAIMED_SUFFIX = /^(
|
|
23
|
-
const FROM_SUFFIX = /^(
|
|
24
|
-
const
|
|
24
|
+
// as it counts in the SessionStart pointer, which reads the same files; `.` would stop at U+2028.
|
|
25
|
+
const ITEM_LINE = /^\s*[-*]\s*\[([ xX])\]\s*([^\n]*)$/;
|
|
26
|
+
const CLAIMED_SUFFIX = /^([^\n]*?)\s*->\s*claimed by\s+(\S+)\s*$/;
|
|
27
|
+
const FROM_SUFFIX = /^([^\n]*?)\s*\(from\s+([^)]+)\)\s*$/;
|
|
28
|
+
const TYPE_TAG = /^\[([a-z-]+)\]\s*([^\n]*)$/;
|
|
29
|
+
// Indentation is what separates a context line from prose; an unindented `Expected:` is not one.
|
|
30
|
+
const CONTEXT_LINE = /^[ \t]+(Expected|Current|Impact):[ \t]?([^\n]*)$/;
|
|
31
|
+
// Each can split a line for some reader or drive a terminal: add refuses them, printers strip them.
|
|
32
|
+
const CONTROL_CHAR = /[\x00-\x1f\x7f-\x9f\u{2028}\u{2029}]/u;
|
|
33
|
+
const CONTROL_CHARS = new RegExp(CONTROL_CHAR.source, 'gu');
|
|
34
|
+
function isKnownType(value) {
|
|
35
|
+
return FOLLOW_UP_TYPES.includes(value);
|
|
36
|
+
}
|
|
25
37
|
/**
|
|
26
|
-
* Rejects a
|
|
27
|
-
* line break) or carry an unprintable byte,
|
|
28
|
-
* line can never be parsed back as two
|
|
38
|
+
* Rejects a blank value (it reads back as absent), or one that would split the
|
|
39
|
+
* written line in two (a line break) or carry an unprintable byte, so a written
|
|
40
|
+
* item or context line can never be parsed back as two lines. `label` names the
|
|
41
|
+
* value in the error.
|
|
29
42
|
*/
|
|
30
|
-
function
|
|
31
|
-
if (
|
|
32
|
-
throw new Error(
|
|
33
|
-
if (CONTROL_CHAR.test(
|
|
34
|
-
throw new Error(
|
|
43
|
+
function validateLine(label, value) {
|
|
44
|
+
if (value.trim().length === 0)
|
|
45
|
+
throw new Error(`${label} cannot be empty.`);
|
|
46
|
+
if (CONTROL_CHAR.test(value)) {
|
|
47
|
+
throw new Error(`${label} cannot contain a line break or control character.`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function validateType(value) {
|
|
51
|
+
if (!isKnownType(value)) {
|
|
52
|
+
throw new Error(`--type '${value}' is not one of: ${FOLLOW_UP_TYPES.join(', ')}.`);
|
|
35
53
|
}
|
|
36
54
|
}
|
|
37
55
|
/**
|
|
38
|
-
* Rejects a `--from` value
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
* when the whole value is such a label, with nothing before or after. Reusing
|
|
42
|
-
* that recognizer (rather than a private copy of its pattern) means the two
|
|
43
|
-
* never drift, and a line break or other control character never round-trips.
|
|
56
|
+
* Rejects a `--from` value unless `taskLabel` (shared.ts) returns it whole, with
|
|
57
|
+
* nothing before or after the `N.N[a]` label; reusing that recognizer keeps the
|
|
58
|
+
* two from drifting, and a control character never round-trips through it.
|
|
44
59
|
*/
|
|
45
60
|
function validateFrom(from) {
|
|
46
61
|
if (taskLabel({ description: from, line: 0 }) !== from) {
|
|
@@ -86,27 +101,40 @@ function listInFlightChangeIds(projectRoot) {
|
|
|
86
101
|
}
|
|
87
102
|
}
|
|
88
103
|
function parseBody(body) {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
104
|
+
let text = body;
|
|
105
|
+
let from = null;
|
|
106
|
+
const fromMatch = text.match(FROM_SUFFIX);
|
|
107
|
+
if (fromMatch) {
|
|
108
|
+
text = fromMatch[1];
|
|
109
|
+
from = fromMatch[2];
|
|
110
|
+
}
|
|
111
|
+
// An unknown tag stays part of the text so an item written before the types existed loses nothing.
|
|
112
|
+
const tag = text.match(TYPE_TAG);
|
|
113
|
+
if (tag && isKnownType(tag[1]))
|
|
114
|
+
return { text: tag[2], from, type: tag[1] };
|
|
115
|
+
return { text, from, type: 'untyped' };
|
|
93
116
|
}
|
|
94
117
|
/**
|
|
95
|
-
* Reads every item
|
|
96
|
-
*
|
|
97
|
-
*
|
|
118
|
+
* Reads every item of a follow-ups.md body. Index is the 1-based position among
|
|
119
|
+
* item lines in file order; a context line attaches to the nearest item line
|
|
120
|
+
* above it, and any other line (heading, blank, comment, an indented line
|
|
121
|
+
* before the first item) is ignored.
|
|
98
122
|
*/
|
|
99
|
-
|
|
100
|
-
const file = followUpsPath(dir);
|
|
101
|
-
if (!fs.existsSync(file))
|
|
102
|
-
return [];
|
|
103
|
-
const lines = fs.readFileSync(file, 'utf-8').split('\n');
|
|
123
|
+
function parseFollowUps(content) {
|
|
104
124
|
const items = [];
|
|
105
125
|
let index = 0;
|
|
106
|
-
for (const line of
|
|
126
|
+
for (const line of content.split(/\r?\n/)) {
|
|
107
127
|
const match = line.match(ITEM_LINE);
|
|
108
|
-
if (!match)
|
|
128
|
+
if (!match) {
|
|
129
|
+
const context = line.match(CONTEXT_LINE);
|
|
130
|
+
const owner = items[items.length - 1];
|
|
131
|
+
if (context && owner) {
|
|
132
|
+
const key = context[1].toLowerCase();
|
|
133
|
+
if (owner[key] === '')
|
|
134
|
+
owner[key] = context[2].trimEnd();
|
|
135
|
+
}
|
|
109
136
|
continue;
|
|
137
|
+
}
|
|
110
138
|
index += 1;
|
|
111
139
|
const checked = match[1].toLowerCase() === 'x';
|
|
112
140
|
let body = match[2];
|
|
@@ -118,39 +146,61 @@ export function readFollowUps(dir) {
|
|
|
118
146
|
claimedBy = claim[2];
|
|
119
147
|
}
|
|
120
148
|
}
|
|
121
|
-
const { text, from } = parseBody(body);
|
|
122
|
-
items.push({ index, text, from, done: checked, claimedBy });
|
|
149
|
+
const { text, from, type } = parseBody(body);
|
|
150
|
+
items.push({ index, text, from, done: checked, claimedBy, type, expected: '', current: '', impact: '' });
|
|
123
151
|
}
|
|
124
152
|
return items;
|
|
125
153
|
}
|
|
126
154
|
/**
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
|
|
155
|
+
* Reads every item of a change's follow-ups.md with its type and context.
|
|
156
|
+
* Absent file -> empty list.
|
|
157
|
+
*/
|
|
158
|
+
export function readFollowUps(dir) {
|
|
159
|
+
const file = followUpsPath(dir);
|
|
160
|
+
if (!fs.existsSync(file))
|
|
161
|
+
return [];
|
|
162
|
+
return parseFollowUps(fs.readFileSync(file, 'utf-8'));
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Appends an unclaimed item and its three context lines to a change's
|
|
166
|
+
* follow-ups.md in one write, creating the file from the template when absent.
|
|
167
|
+
* Throws, writing nothing, on an invalid field or when the change resolves
|
|
168
|
+
* neither in flight nor archived.
|
|
130
169
|
*/
|
|
131
|
-
export function addFollowUp(projectRoot, changeId,
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
170
|
+
export function addFollowUp(projectRoot, changeId, item) {
|
|
171
|
+
validateLine('follow-up text', item.text);
|
|
172
|
+
validateType(item.type);
|
|
173
|
+
validateLine('--expected', item.expected);
|
|
174
|
+
validateLine('--current', item.current);
|
|
175
|
+
validateLine('--impact', item.impact);
|
|
176
|
+
if (item.from !== undefined)
|
|
177
|
+
validateFrom(item.from);
|
|
178
|
+
const text = item.text.trim();
|
|
179
|
+
const from = item.from ?? null;
|
|
180
|
+
const itemLine = `- [ ] [${item.type}] ${text}${from ? ` (from ${from})` : ''}`;
|
|
181
|
+
const [written] = parseFollowUps(itemLine);
|
|
182
|
+
if (written.text !== text || written.type !== item.type || written.from !== from) {
|
|
183
|
+
throw new Error(`follow-up text '${text}' would not read back as written: it ends in a (from ...) suffix or a tag that would not read back; reword it, or pass a task label as --from.`);
|
|
184
|
+
}
|
|
135
185
|
const resolution = resolveChangeId(projectRoot, changeId);
|
|
136
186
|
if (!resolution) {
|
|
137
187
|
throw new Error(`Change '${changeId}' not found (neither in flight nor archived).`);
|
|
138
188
|
}
|
|
139
|
-
const
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
const suffix = from ? ` (from ${from})` : '';
|
|
146
|
-
const line = `- [ ] ${text}${suffix}`;
|
|
147
|
-
const current = fs.readFileSync(file, 'utf-8');
|
|
148
|
-
const separator = current.length === 0 || current.endsWith('\n') ? '' : '\n';
|
|
149
|
-
fs.writeFileSync(file, `${current}${separator}${line}\n`, 'utf-8');
|
|
189
|
+
const file = followUpsPath(changeDirForResolution(projectRoot, changeId, resolution));
|
|
190
|
+
const existing = fs.existsSync(file) ? fs.readFileSync(file, 'utf-8') : TEMPLATE;
|
|
191
|
+
const index = parseFollowUps(existing).length + 1;
|
|
192
|
+
const entry = `${itemLine}\n Expected: ${item.expected}\n Current: ${item.current}\n Impact: ${item.impact}\n`;
|
|
193
|
+
const separator = existing.length === 0 || existing.endsWith('\n') ? '' : '\n';
|
|
194
|
+
fs.writeFileSync(file, `${existing}${separator}${entry}`, 'utf-8');
|
|
150
195
|
return { change: changeId, index };
|
|
151
196
|
}
|
|
152
|
-
/**
|
|
153
|
-
|
|
197
|
+
/** Validates a `list --type` value; `untyped` is accepted since the list shows it as a group. */
|
|
198
|
+
function validateListType(value) {
|
|
199
|
+
if (value !== 'untyped')
|
|
200
|
+
validateType(value);
|
|
201
|
+
}
|
|
202
|
+
/** Every open (unchecked) item of every in-flight and archived change, optionally of one type. */
|
|
203
|
+
export function listFollowUps(projectRoot, type) {
|
|
154
204
|
const changesDir = changesDirFor(projectRoot);
|
|
155
205
|
const results = [];
|
|
156
206
|
const collect = (ids, base) => {
|
|
@@ -160,7 +210,11 @@ export function listFollowUps(projectRoot) {
|
|
|
160
210
|
// same reason, so the two commands never disagree about what is still open.
|
|
161
211
|
if (item.done)
|
|
162
212
|
continue;
|
|
163
|
-
|
|
213
|
+
if (type !== undefined && item.type !== type)
|
|
214
|
+
continue;
|
|
215
|
+
// Keeps `state` after `from` in the --json key order.
|
|
216
|
+
const { done, claimedBy, index, text, from, ...typeAndContext } = item;
|
|
217
|
+
results.push({ change: id, index, text, from, state: 'open', ...typeAndContext });
|
|
164
218
|
}
|
|
165
219
|
}
|
|
166
220
|
};
|
|
@@ -203,7 +257,8 @@ export function claimFollowUp(projectRoot, changeId, index, byId) {
|
|
|
203
257
|
throw new Error(`Follow-up ${changeId}#${index} does not exist.`);
|
|
204
258
|
}
|
|
205
259
|
const checked = targetMatch[1].toLowerCase() === 'x';
|
|
206
|
-
|
|
260
|
+
const lineEnd = targetMatch[2].endsWith('\r') ? '\r' : '';
|
|
261
|
+
const body = targetMatch[2].slice(0, targetMatch[2].length - lineEnd.length);
|
|
207
262
|
if (checked) {
|
|
208
263
|
const claim = body.match(CLAIMED_SUFFIX);
|
|
209
264
|
const claimedBy = claim ? claim[2] : 'unknown';
|
|
@@ -212,18 +267,54 @@ export function claimFollowUp(projectRoot, changeId, index, byId) {
|
|
|
212
267
|
if (!resolveChangeId(projectRoot, byId)) {
|
|
213
268
|
throw new Error(`--by change '${byId}' not found (neither in flight nor archived).`);
|
|
214
269
|
}
|
|
215
|
-
lines[targetLine] = `- [x] ${body} -> claimed by ${byId}`;
|
|
270
|
+
lines[targetLine] = `- [x] ${body} -> claimed by ${byId}${lineEnd}`;
|
|
216
271
|
fs.writeFileSync(file, lines.join('\n'), 'utf-8');
|
|
217
272
|
return { change: changeId, index, by: byId };
|
|
218
273
|
}
|
|
219
|
-
/**
|
|
274
|
+
/**
|
|
275
|
+
* Lines grouped by type, with C0, DEL, C1, U+2028 and U+2029 removed from each,
|
|
276
|
+
* since a hand-edited record never passed add's validation; `--json` prints raw values.
|
|
277
|
+
*/
|
|
278
|
+
export function formatFollowUpGroups(items, indent, label) {
|
|
279
|
+
const lines = [];
|
|
280
|
+
for (const type of DISPLAY_ORDER) {
|
|
281
|
+
const group = items.filter((item) => item.type === type);
|
|
282
|
+
if (group.length === 0)
|
|
283
|
+
continue;
|
|
284
|
+
lines.push(`${indent.heading}[${type}] ${group.length}`);
|
|
285
|
+
for (const item of group) {
|
|
286
|
+
const suffix = item.from ? ` (from ${item.from})` : '';
|
|
287
|
+
lines.push(`${indent.item}${label(item)}${item.text}${suffix}`);
|
|
288
|
+
if (item.expected)
|
|
289
|
+
lines.push(`${indent.item} Expected: ${item.expected}`);
|
|
290
|
+
if (item.current)
|
|
291
|
+
lines.push(`${indent.item} Current: ${item.current}`);
|
|
292
|
+
if (item.impact)
|
|
293
|
+
lines.push(`${indent.item} Impact: ${item.impact}`);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
return lines.map((line) => line.replace(CONTROL_CHARS, ''));
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* CLI wrapper for `openspec follow-up add <change-id> '<text>' --type <type>
|
|
300
|
+
* --expected '<text>' --current '<text>' --impact '<text>' [--from <task>]`.
|
|
301
|
+
*/
|
|
220
302
|
export function followUpAddHandler(projectRoot, changeId, text, options) {
|
|
221
303
|
try {
|
|
222
304
|
if (!changeId)
|
|
223
305
|
throw new Error('Missing required argument <change-id>.');
|
|
224
306
|
if (!text)
|
|
225
|
-
throw new Error(
|
|
226
|
-
const
|
|
307
|
+
throw new Error("Missing required argument '<text>'.");
|
|
308
|
+
const { type, expected, current, impact } = options;
|
|
309
|
+
if (type === undefined)
|
|
310
|
+
throw new Error(`Missing required option --type <${FOLLOW_UP_TYPES.join('|')}>.`);
|
|
311
|
+
if (expected === undefined)
|
|
312
|
+
throw new Error("Missing required option --expected '<text>'.");
|
|
313
|
+
if (current === undefined)
|
|
314
|
+
throw new Error("Missing required option --current '<text>'.");
|
|
315
|
+
if (impact === undefined)
|
|
316
|
+
throw new Error("Missing required option --impact '<text>'.");
|
|
317
|
+
const result = addFollowUp(projectRoot, changeId, { text, type, expected, current, impact, from: options.from });
|
|
227
318
|
if (options.json) {
|
|
228
319
|
printJson({ added: true, ...result });
|
|
229
320
|
return 0;
|
|
@@ -236,9 +327,17 @@ export function followUpAddHandler(projectRoot, changeId, text, options) {
|
|
|
236
327
|
return 1;
|
|
237
328
|
}
|
|
238
329
|
}
|
|
239
|
-
/** CLI wrapper for `openspec follow-up list [--json]`. */
|
|
330
|
+
/** CLI wrapper for `openspec follow-up list [--type <type>] [--json]`. */
|
|
240
331
|
export function followUpListHandler(projectRoot, options) {
|
|
241
|
-
|
|
332
|
+
try {
|
|
333
|
+
if (options.type !== undefined)
|
|
334
|
+
validateListType(options.type);
|
|
335
|
+
}
|
|
336
|
+
catch (error) {
|
|
337
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
338
|
+
return 1;
|
|
339
|
+
}
|
|
340
|
+
const items = listFollowUps(projectRoot, options.type);
|
|
242
341
|
if (options.json) {
|
|
243
342
|
printJson(items);
|
|
244
343
|
return 0;
|
|
@@ -247,9 +346,8 @@ export function followUpListHandler(projectRoot, options) {
|
|
|
247
346
|
console.log('no open follow-ups');
|
|
248
347
|
return 0;
|
|
249
348
|
}
|
|
250
|
-
for (const
|
|
251
|
-
|
|
252
|
-
console.log(`${item.change}#${item.index} ${item.text}${suffix}`);
|
|
349
|
+
for (const line of formatFollowUpGroups(items, { heading: '', item: '' }, (item) => `${item.change}#${item.index} `)) {
|
|
350
|
+
console.log(line);
|
|
253
351
|
}
|
|
254
352
|
return 0;
|
|
255
353
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { type ArtifactInstructions } from '../../vendor/openspec/engine/index.js';
|
|
1
|
+
import { type ArtifactInstructions, type ParsedTask } from '../../vendor/openspec/engine/index.js';
|
|
2
|
+
import { type ShimWarning } from './warnings.js';
|
|
2
3
|
export interface InstructionsOptions {
|
|
3
4
|
change?: string;
|
|
4
5
|
schema?: string;
|
|
@@ -17,10 +18,13 @@ interface ApplyInstructions {
|
|
|
17
18
|
complete: number;
|
|
18
19
|
remaining: number;
|
|
19
20
|
};
|
|
20
|
-
tasks:
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
tasks: TaskItem[];
|
|
22
|
+
/** Whether the schema declares `apply.tracks` at all. */
|
|
23
|
+
taskTrackingConfigured: boolean;
|
|
24
|
+
/** A tracking file `apply.tracks` matched that could not be read, and why. */
|
|
25
|
+
unavailableTrackingFiles?: {
|
|
26
|
+
path: string;
|
|
27
|
+
reason: string;
|
|
24
28
|
}[];
|
|
25
29
|
state: 'blocked' | 'all_done' | 'ready';
|
|
26
30
|
missingArtifacts?: string[];
|
|
@@ -34,9 +38,35 @@ interface ApplyInstructions {
|
|
|
34
38
|
*/
|
|
35
39
|
missingPrerequisites?: string[];
|
|
36
40
|
/** Non-blocking problems with the change, reported alongside the instruction. */
|
|
37
|
-
warnings?:
|
|
41
|
+
warnings?: ShimWarning[];
|
|
38
42
|
instruction: string;
|
|
39
43
|
}
|
|
44
|
+
/** The apply skill edits the checkbox at `sourcePath`/`line` instead of searching for its text. */
|
|
45
|
+
interface TaskItem {
|
|
46
|
+
id: string;
|
|
47
|
+
description: string;
|
|
48
|
+
done: boolean;
|
|
49
|
+
sourcePath: string;
|
|
50
|
+
line: number;
|
|
51
|
+
}
|
|
52
|
+
export interface LocatedTask extends ParsedTask {
|
|
53
|
+
sourcePath: string;
|
|
54
|
+
line: number;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Parses one line at a time so each task keeps its own index in the file; the
|
|
58
|
+
* engine reader stays the single definition of what a task line is.
|
|
59
|
+
*/
|
|
60
|
+
export declare function parseLocatedTasks(content: string, sourcePath: string): LocatedTask[];
|
|
61
|
+
type ArtifactOutput = {
|
|
62
|
+
id: string;
|
|
63
|
+
generates: string;
|
|
64
|
+
};
|
|
65
|
+
/**
|
|
66
|
+
* The status side of the proposal-coverage check. Unlike apply it also fires
|
|
67
|
+
* with no delta at all, since status has no zero-delta warning to defer to.
|
|
68
|
+
*/
|
|
69
|
+
export declare function collectSpecsIncompleteWarnings(artifacts: ArtifactOutput[], changeDir: string, skippedArtifacts?: Set<string>): ShimWarning[];
|
|
40
70
|
/**
|
|
41
71
|
* Whether the "Not created yet, in build order" line would repeat information
|
|
42
72
|
* the "Missing artifacts" line already gave in full.
|