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