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
|
@@ -7,13 +7,14 @@
|
|
|
7
7
|
import { promises as fs } from 'fs';
|
|
8
8
|
import { randomUUID } from 'crypto';
|
|
9
9
|
import path from 'path';
|
|
10
|
-
import { extractRequirementsSection,
|
|
10
|
+
import { extractRequirementsSection, diffScenarioNames, describeScenarioBalance, describeUnappliedContent, foldRequirementName, parseDeltaSpec, normalizeRequirementName, } from '../validation/parsers/requirement-blocks.js';
|
|
11
11
|
import { findMainSpecStructureIssues } from '../validation/parsers/spec-structure.js';
|
|
12
12
|
import { buildCodeFenceMask } from '../validation/parsers/code-fence.js';
|
|
13
13
|
import { MarkdownParser } from '../validation/parsers/markdown-parser.js';
|
|
14
14
|
import { MIN_PURPOSE_LENGTH, PURPOSE_PLACEHOLDER_PREFIX, PURPOSE_PLACEHOLDER_SUFFIX, } from '../validation/constants.js';
|
|
15
15
|
import { discoverSpecFiles } from '../validation/spec-discovery.js';
|
|
16
16
|
import { FileSystemUtils } from '../engine/file-system.js';
|
|
17
|
+
import { matchLineEnding } from './line-endings.js';
|
|
17
18
|
function isLexicallyWithin(allowedDirectory, targetPath) {
|
|
18
19
|
const relative = path.relative(path.resolve(allowedDirectory), path.resolve(targetPath));
|
|
19
20
|
return (relative === '' ||
|
|
@@ -21,7 +22,7 @@ function isLexicallyWithin(allowedDirectory, targetPath) {
|
|
|
21
22
|
!relative.startsWith(`..${path.sep}`) &&
|
|
22
23
|
!path.isAbsolute(relative)));
|
|
23
24
|
}
|
|
24
|
-
function resolveTrustedSpecPath(specsRoot, specPath) {
|
|
25
|
+
function resolveTrustedSpecPath(specsRoot, specPath, projectRoot) {
|
|
25
26
|
if (!isLexicallyWithin(specsRoot, specPath)) {
|
|
26
27
|
throw new Error(`Path is outside the allowed directory: ${specPath}`);
|
|
27
28
|
}
|
|
@@ -41,6 +42,12 @@ function resolveTrustedSpecPath(specsRoot, specPath) {
|
|
|
41
42
|
// Freeze their canonical location as the trust root so later swaps are
|
|
42
43
|
// rejected while a nested spec.md link still cannot escape.
|
|
43
44
|
const root = FileSystemUtils.canonicalizeExistingPath(path.dirname(specPath));
|
|
45
|
+
// An external capability link is supported; name the real write target
|
|
46
|
+
// instead of silently writing outside the project while reporting the
|
|
47
|
+
// in-project path.
|
|
48
|
+
if (projectRoot && !isLexicallyWithin(FileSystemUtils.canonicalizeExistingPath(projectRoot), root)) {
|
|
49
|
+
process.emitWarning(`Capability '${path.basename(path.dirname(specPath))}' links outside the project; writing to ${root}`, 'OpenSpecExternalSpecWrite');
|
|
50
|
+
}
|
|
44
51
|
const file = path.join(root, path.basename(specPath));
|
|
45
52
|
FileSystemUtils.assertPathWithin(root, file);
|
|
46
53
|
return { root, file };
|
|
@@ -68,7 +75,8 @@ export async function findSpecUpdates(changeDir, mainSpecsDir) {
|
|
|
68
75
|
for (const { id, specFile } of discovered) {
|
|
69
76
|
const targetFile = path.join(mainSpecsDir, ...id.split('/'), 'spec.md');
|
|
70
77
|
const source = resolveTrustedSpecPath(changeSpecsDir, specFile);
|
|
71
|
-
|
|
78
|
+
// mainSpecsDir is always <project root>/openspec/specs.
|
|
79
|
+
const target = resolveTrustedSpecPath(mainSpecsDir, targetFile, path.dirname(path.dirname(mainSpecsDir)));
|
|
72
80
|
// Check if target exists
|
|
73
81
|
let exists = false;
|
|
74
82
|
try {
|
|
@@ -110,6 +118,27 @@ export async function buildUpdatedSpec(update, changeName, options = {}) {
|
|
|
110
118
|
// Parse deltas from the change spec file
|
|
111
119
|
const plan = parseDeltaSpec(changeContent);
|
|
112
120
|
const specName = update.id;
|
|
121
|
+
// Without this refusal a TO: with no pending FROM: pairs with whichever
|
|
122
|
+
// FROM: comes next, renaming a requirement the author never named. Refuse
|
|
123
|
+
// rather than guess. (A FROM: without its TO: stays the FreeHarness
|
|
124
|
+
// advisory note reported by validate; not blocking here.)
|
|
125
|
+
if (plan.unpairedRenames.length > 0) {
|
|
126
|
+
const first = plan.unpairedRenames[0];
|
|
127
|
+
throw new Error(`${specName} validation failed - RENAMED entry on line ${first.line} has no matching FROM: for header "### Requirement: ${first.name}". ` +
|
|
128
|
+
`Write each rename as a FROM: line followed immediately by its TO: line.`);
|
|
129
|
+
}
|
|
130
|
+
// A well-formed requirement outside every delta section is not applied.
|
|
131
|
+
// Say so here too: archive is the last point the author can still notice.
|
|
132
|
+
for (const orphan of plan.orphanedRequirements) {
|
|
133
|
+
const where = orphan.section ? `under "## ${orphan.section}"` : 'above the first "## " section';
|
|
134
|
+
warn(`${specName} - requirement "${orphan.name}" (line ${orphan.line}) is ${where}, ` +
|
|
135
|
+
`which is not a delta section, so it was not applied. ` +
|
|
136
|
+
`Move it under ADDED/MODIFIED/REMOVED/RENAMED Requirements.`);
|
|
137
|
+
}
|
|
138
|
+
// Archive carries requirement blocks only; name any other text it leaves behind.
|
|
139
|
+
for (const lost of plan.unappliedContent) {
|
|
140
|
+
warn(`${specName} - ${describeUnappliedContent(lost)} (line ${lost.line}) is not carried into the main spec.`);
|
|
141
|
+
}
|
|
113
142
|
// Pre-validate duplicates within sections
|
|
114
143
|
const addedNames = new Set();
|
|
115
144
|
for (const add of plan.added) {
|
|
@@ -165,12 +194,11 @@ export async function buildUpdatedSpec(update, changeName, options = {}) {
|
|
|
165
194
|
for (const { from, to } of plan.renamed) {
|
|
166
195
|
const fromNorm = normalizeRequirementName(from);
|
|
167
196
|
const toNorm = normalizeRequirementName(to);
|
|
168
|
-
// A REMOVED naming the FROM side contradicts the rename.
|
|
169
|
-
//
|
|
170
|
-
//
|
|
171
|
-
//
|
|
172
|
-
//
|
|
173
|
-
// into a warned no-op.
|
|
197
|
+
// A REMOVED naming the FROM side contradicts the rename. A missing
|
|
198
|
+
// REMOVED target is a no-op, so without this explicit rejection the
|
|
199
|
+
// rename consumes the old header and the REMOVED silently does nothing.
|
|
200
|
+
// Compared folded, so a case/whitespace variant cannot slip past the
|
|
201
|
+
// guard and degrade into a warned no-op.
|
|
174
202
|
const removedFoldMatch = [...removedNamesSet].find((r) => foldRequirementName(r) === foldRequirementName(fromNorm));
|
|
175
203
|
if (removedFoldMatch !== undefined) {
|
|
176
204
|
throw new Error(`${specName} validation failed - requirement present in multiple sections (RENAMED and REMOVED) for header "### Requirement: ${from}"` +
|
|
@@ -293,6 +321,13 @@ export async function buildUpdatedSpec(update, changeName, options = {}) {
|
|
|
293
321
|
if (nameToBlock.has(to)) {
|
|
294
322
|
throw new Error(`${specName} RENAMED failed for header "### Requirement: ${r.to}" - target already exists`);
|
|
295
323
|
}
|
|
324
|
+
// A target differing from another requirement only in case/whitespace
|
|
325
|
+
// would leave two copies; the source itself is exempt so a case-only
|
|
326
|
+
// rename stays allowed.
|
|
327
|
+
const targetNearMiss = [...nameToBlock.keys()].find((k) => k !== from && foldRequirementName(k) === foldRequirementName(to));
|
|
328
|
+
if (targetNearMiss !== undefined) {
|
|
329
|
+
throw new Error(`${specName} RENAMED failed for header "### Requirement: ${r.to}" - "### Requirement: ${nameToBlock.get(targetNearMiss).name}" already exists and differs only in case or spacing; choose a distinct name`);
|
|
330
|
+
}
|
|
296
331
|
const block = nameToBlock.get(from);
|
|
297
332
|
const newHeader = `### Requirement: ${to}`;
|
|
298
333
|
const rawLines = block.raw.split('\n');
|
|
@@ -352,9 +387,9 @@ export async function buildUpdatedSpec(update, changeName, options = {}) {
|
|
|
352
387
|
if (!modHeaderMatch || normalizeRequirementName(modHeaderMatch[1]) !== key) {
|
|
353
388
|
throw new Error(`${specName} MODIFIED failed for header "### Requirement: ${mod.name}" - header mismatch in content`);
|
|
354
389
|
}
|
|
355
|
-
const
|
|
356
|
-
if (
|
|
357
|
-
throw new Error(`${specName} MODIFIED failed for header "### Requirement: ${mod.name}" - current spec contains scenario(s) not present in the modified block: ${
|
|
390
|
+
const scenarioDiff = diffScenarioNames(currentBlock, mod);
|
|
391
|
+
if (scenarioDiff.missing.length > 0) {
|
|
392
|
+
throw new Error(`${specName} MODIFIED failed for header "### Requirement: ${mod.name}" - current spec contains scenario(s) not present in the modified block: ${scenarioDiff.missing.map(name => `"${name}"`).join(', ')}. ${describeScenarioBalance(scenarioDiff)} Refresh the change spec before archiving to avoid dropping scenarios.`);
|
|
358
393
|
}
|
|
359
394
|
// Identical content means the modification was already synced to the
|
|
360
395
|
// baseline (early-sync pattern) — count only real replacements, so a
|
|
@@ -379,6 +414,14 @@ export async function buildUpdatedSpec(update, changeName, options = {}) {
|
|
|
379
414
|
}
|
|
380
415
|
throw new Error(`${specName} ADDED failed for header "### Requirement: ${add.name}" - already exists`);
|
|
381
416
|
}
|
|
417
|
+
// A name differing from an existing requirement only in case/whitespace is
|
|
418
|
+
// that requirement written again. Compared against the spec as it stands
|
|
419
|
+
// after the earlier operations, so a variant this delta removed or
|
|
420
|
+
// renamed away is fine.
|
|
421
|
+
const nearMiss = [...nameToBlock.keys()].find((k) => foldRequirementName(k) === foldRequirementName(key));
|
|
422
|
+
if (nearMiss !== undefined) {
|
|
423
|
+
throw new Error(`${specName} ADDED failed for header "### Requirement: ${add.name}" - "### Requirement: ${nameToBlock.get(nearMiss).name}" already exists and differs only in case or spacing; use MODIFIED with that exact header to change it, or choose a distinct name`);
|
|
424
|
+
}
|
|
382
425
|
nameToBlock.set(key, add);
|
|
383
426
|
addedApplied++;
|
|
384
427
|
}
|
|
@@ -865,11 +908,20 @@ export async function writeUpdatedSpec(update, rebuilt, counts, options = {}) {
|
|
|
865
908
|
// Create target directory if needed
|
|
866
909
|
const targetDir = path.dirname(update.target);
|
|
867
910
|
await fs.mkdir(targetDir, { recursive: true });
|
|
911
|
+
// `rebuilt` is always LF; match the target's own convention on write, or a
|
|
912
|
+
// CRLF checkout sees the whole spec change. Only ENOENT means "no file to
|
|
913
|
+
// match"; any other read error propagates rather than reading as absent.
|
|
914
|
+
const previous = await fs.readFile(update.target, 'utf-8').catch((error) => {
|
|
915
|
+
if (error.code === 'ENOENT')
|
|
916
|
+
return undefined;
|
|
917
|
+
throw error;
|
|
918
|
+
});
|
|
919
|
+
const toWrite = previous === undefined ? rebuilt : matchLineEnding(rebuilt, previous);
|
|
868
920
|
await options.beforeMutate?.();
|
|
869
921
|
// Preserve the established in-place write semantics: symlink referents,
|
|
870
922
|
// hard-linked specs, ACLs, extended attributes, and filesystems without hard
|
|
871
923
|
// links must continue to behave as they did before capability retirement.
|
|
872
|
-
await fs.writeFile(update.target,
|
|
924
|
+
await fs.writeFile(update.target, toWrite);
|
|
873
925
|
if (options.silent)
|
|
874
926
|
return;
|
|
875
927
|
const specName = update.id;
|
|
@@ -886,15 +938,36 @@ export async function writeUpdatedSpec(update, rebuilt, counts, options = {}) {
|
|
|
886
938
|
/** Blank out `<!-- ... -->` spans, preserving line count so indices stay aligned. */
|
|
887
939
|
function maskHtmlComments(content) {
|
|
888
940
|
const blank = (text) => text.replace(/[^\n]/g, ' ');
|
|
889
|
-
//
|
|
890
|
-
|
|
891
|
-
//
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
941
|
+
// Linear scan: every character is visited once. A `/<!--[\s\S]*?--!?>/g`
|
|
942
|
+
// replace re-scans to end of file from every `<!--`, which is quadratic on a
|
|
943
|
+
// spec dense in comment openers.
|
|
944
|
+
let out = '';
|
|
945
|
+
let index = 0;
|
|
946
|
+
for (;;) {
|
|
947
|
+
const open = content.indexOf('<!--', index);
|
|
948
|
+
if (open === -1)
|
|
949
|
+
return out + content.slice(index);
|
|
950
|
+
out += content.slice(index, open);
|
|
951
|
+
// `--!>` is a comment terminator as well as `-->`.
|
|
952
|
+
let close = -1;
|
|
953
|
+
for (let i = open + 4; i < content.length; i++) {
|
|
954
|
+
if (content.startsWith('-->', i)) {
|
|
955
|
+
close = i + 3;
|
|
956
|
+
break;
|
|
957
|
+
}
|
|
958
|
+
if (content.startsWith('--!>', i)) {
|
|
959
|
+
close = i + 4;
|
|
960
|
+
break;
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
// A comment that is never closed runs to end of file, so everything after
|
|
964
|
+
// it is commented out too. Without this an unterminated `<!--` above a
|
|
965
|
+
// `## Purpose` left the commented-out header looking real (#1413).
|
|
966
|
+
if (close === -1)
|
|
967
|
+
return out + blank(content.slice(open));
|
|
968
|
+
out += blank(content.slice(open, close));
|
|
969
|
+
index = close;
|
|
970
|
+
}
|
|
898
971
|
}
|
|
899
972
|
/**
|
|
900
973
|
* Read the body of a `## Purpose` section, ignoring markdown that only appears
|
|
@@ -23,4 +23,10 @@ export interface ActionContextInput {
|
|
|
23
23
|
artifactIds: string[];
|
|
24
24
|
}
|
|
25
25
|
export declare function buildActionContext(input: ActionContextInput): ActionContext;
|
|
26
|
+
/** One next action, in both forms, so text and JSON name the same step. */
|
|
27
|
+
export interface ChangeNextStep {
|
|
28
|
+
command: string;
|
|
29
|
+
sentence: string;
|
|
30
|
+
}
|
|
31
|
+
export declare function resolveNextStep(input: ChangeNextStepsInput): ChangeNextStep | undefined;
|
|
26
32
|
export declare function buildNextSteps(input: ChangeNextStepsInput): string[];
|
|
@@ -19,14 +19,22 @@ export function buildActionContext(input) {
|
|
|
19
19
|
],
|
|
20
20
|
};
|
|
21
21
|
}
|
|
22
|
-
export function
|
|
22
|
+
export function resolveNextStep(input) {
|
|
23
23
|
const readyArtifact = input.artifactStatuses.find((artifact) => artifact.status === 'ready');
|
|
24
|
-
const steps = [];
|
|
25
24
|
if (readyArtifact) {
|
|
26
|
-
|
|
25
|
+
const command = `openspec instructions ${readyArtifact.id} --change "${input.changeName}" --json`;
|
|
26
|
+
return { command, sentence: `Run ${command} before writing that artifact.` };
|
|
27
27
|
}
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
if (input.allArtifactsComplete) {
|
|
29
|
+
const command = `openspec instructions apply --change "${input.changeName}" --json`;
|
|
30
|
+
return {
|
|
31
|
+
command,
|
|
32
|
+
sentence: `All planning artifacts are complete. Run ${command} to inspect implementation progress.`,
|
|
33
|
+
};
|
|
30
34
|
}
|
|
31
|
-
return
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
export function buildNextSteps(input) {
|
|
38
|
+
const step = resolveNextStep(input);
|
|
39
|
+
return step ? [step.sentence] : [];
|
|
32
40
|
}
|
|
@@ -8,4 +8,4 @@ export { artifactOutputExists, isGlobPattern, resolveArtifactOutputPath, resolve
|
|
|
8
8
|
export { readChangeMetadata, resolveSchemaForChange, writeChangeMetadata, ChangeMetadataSchema, ChangeMetadataError, METADATA_FILENAME, type ChangeMetadata, } from './change-metadata.js';
|
|
9
9
|
export { loadTemplate, loadChangeContext, generateInstructions, formatChangeStatus, TemplateLoadError, SKIP_SPECS_INSTRUCTIONS_WARNING, type ChangeContext, type LoadChangeContextOptions, type ArtifactInstructions, type DependencyInfo, type ArtifactStatus, type ChangeStatus, type ArtifactPathSummary, } from './instruction-loader.js';
|
|
10
10
|
export { getTaskProgressForChange, parseTaskLines, countTasksFromContent, formatTaskStatus, type TaskProgress, type ParsedTask, } from './task-progress.js';
|
|
11
|
-
export { buildActionContext, buildNextSteps, type ActionContext, } from './change-status-policy.js';
|
|
11
|
+
export { buildActionContext, buildNextSteps, resolveNextStep, type ActionContext, type ChangeNextStep, } from './change-status-policy.js';
|
|
@@ -18,4 +18,4 @@ export { loadTemplate, loadChangeContext, generateInstructions, formatChangeStat
|
|
|
18
18
|
// Task progress (list)
|
|
19
19
|
export { getTaskProgressForChange, parseTaskLines, countTasksFromContent, formatTaskStatus, } from './task-progress.js';
|
|
20
20
|
// Change-status policy
|
|
21
|
-
export { buildActionContext, buildNextSteps, } from './change-status-policy.js';
|
|
21
|
+
export { buildActionContext, buildNextSteps, resolveNextStep, } from './change-status-policy.js';
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
3
|
-
*/
|
|
1
|
+
/** True when a `{...}` group has a comma (alternation) or `..` (range): `{api,ui}` is a glob, `{draft}` alone is literal. */
|
|
2
|
+
export declare function hasBraceExpansion(pattern: string): boolean;
|
|
3
|
+
/** Checks if a path is a glob: wildcards, a brace expansion, or an extglob (`@(...)`, `!(...)`, ...). */
|
|
4
4
|
export declare function isGlobPattern(pattern: string): boolean;
|
|
5
5
|
/**
|
|
6
6
|
* Resolves an artifact's `generates` value to an absolute path under the change
|
|
@@ -13,11 +13,31 @@
|
|
|
13
13
|
import * as fs from 'node:fs';
|
|
14
14
|
import * as path from 'node:path';
|
|
15
15
|
import { FileSystemUtils } from './file-system.js';
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
*/
|
|
16
|
+
const EXTGLOB_RE = /[!*+?@]\([^(]*\)/u;
|
|
17
|
+
const BRACE_EXPANSION_SEPARATORS_RE = /,|\.\./u;
|
|
18
|
+
/** True when a `{...}` group has a comma (alternation) or `..` (range): `{api,ui}` is a glob, `{draft}` alone is literal. */
|
|
19
|
+
export function hasBraceExpansion(pattern) {
|
|
20
|
+
const openings = [];
|
|
21
|
+
for (let index = 0; index < pattern.length; index += 1) {
|
|
22
|
+
if (pattern[index] === '{')
|
|
23
|
+
openings.push(index);
|
|
24
|
+
if (pattern[index] !== '}')
|
|
25
|
+
continue;
|
|
26
|
+
const opening = openings.pop();
|
|
27
|
+
if (opening !== undefined && BRACE_EXPANSION_SEPARATORS_RE.test(pattern.slice(opening, index))) {
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
/** Checks if a path is a glob: wildcards, a brace expansion, or an extglob (`@(...)`, `!(...)`, ...). */
|
|
19
34
|
export function isGlobPattern(pattern) {
|
|
20
|
-
|
|
35
|
+
const normalized = FileSystemUtils.toPosixPath(pattern);
|
|
36
|
+
return (normalized.includes('*') ||
|
|
37
|
+
normalized.includes('?') ||
|
|
38
|
+
normalized.includes('[') ||
|
|
39
|
+
EXTGLOB_RE.test(normalized) ||
|
|
40
|
+
hasBraceExpansion(normalized));
|
|
21
41
|
}
|
|
22
42
|
/**
|
|
23
43
|
* Resolves an artifact's `generates` value to an absolute path under the change
|
|
@@ -11,17 +11,14 @@ import * as path from 'node:path';
|
|
|
11
11
|
import { resolveSchema } from './resolver.js';
|
|
12
12
|
import { resolveArtifactOutputs } from './outputs.js';
|
|
13
13
|
import { resolveSchemaForChange } from './change-metadata.js';
|
|
14
|
-
/**
|
|
15
|
-
|
|
16
|
-
* Leading whitespace is allowed so nested sub-tasks count like their parents.
|
|
17
|
-
*/
|
|
18
|
-
const TASK_LINE_PATTERN = /^\s*[-*]\s*\[([\sxX])\]\s*(.*)/;
|
|
14
|
+
/** The task-line pattern of validation/task-lines.ts, which documents it; keep the two identical. */
|
|
15
|
+
const TASK_LINE_PATTERN = /^\s*(?:[-*+]|\d{1,9}[.)])\s*\[(?:\s*([^\]\s])\s*\](?![([])|\](?![([])|\s+\])\s*(.*)/;
|
|
19
16
|
export function parseTaskLines(content) {
|
|
20
17
|
const tasks = [];
|
|
21
18
|
for (const line of content.split('\n')) {
|
|
22
19
|
const match = line.match(TASK_LINE_PATTERN);
|
|
23
20
|
if (match) {
|
|
24
|
-
tasks.push({ done: match[1].toLowerCase() === 'x', description: match[2].trim() });
|
|
21
|
+
tasks.push({ done: (match[1] ?? '').toLowerCase() === 'x', description: match[2].trim() });
|
|
25
22
|
}
|
|
26
23
|
}
|
|
27
24
|
return tasks;
|
|
@@ -49,7 +46,7 @@ function parseTasksWithDisposition(content) {
|
|
|
49
46
|
for (const line of content.split('\n')) {
|
|
50
47
|
const task = line.match(TASK_LINE_PATTERN);
|
|
51
48
|
if (task) {
|
|
52
|
-
current = { done: task[1].toLowerCase() === 'x', description: task[2].trim(), dispositioned: false };
|
|
49
|
+
current = { done: (task[1] ?? '').toLowerCase() === 'x', description: task[2].trim(), dispositioned: false };
|
|
53
50
|
tasks.push(current);
|
|
54
51
|
continue;
|
|
55
52
|
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// No behavioural changes. Part of the schema-validation slice.
|
|
4
4
|
import * as path from 'node:path';
|
|
5
5
|
import { z } from 'zod';
|
|
6
|
+
import { hasBraceExpansion } from './outputs.js';
|
|
6
7
|
function relativePathSchema(fieldName) {
|
|
7
8
|
return z
|
|
8
9
|
.string()
|
|
@@ -21,13 +22,24 @@ function relativePathSchema(fieldName) {
|
|
|
21
22
|
});
|
|
22
23
|
}
|
|
23
24
|
// Artifact definition schema
|
|
24
|
-
export const ArtifactSchema = z
|
|
25
|
+
export const ArtifactSchema = z
|
|
26
|
+
.object({
|
|
25
27
|
id: z.string().min(1, { error: 'Artifact ID is required' }),
|
|
26
28
|
generates: relativePathSchema('generates field'),
|
|
27
29
|
description: z.string(),
|
|
28
30
|
template: relativePathSchema('template field'),
|
|
29
31
|
instruction: z.string().optional(),
|
|
30
32
|
requires: z.array(z.string()).default([]),
|
|
33
|
+
})
|
|
34
|
+
// Refined at the object level so the message can name the artifact id.
|
|
35
|
+
.superRefine((artifact, ctx) => {
|
|
36
|
+
if (hasBraceExpansion(artifact.generates)) {
|
|
37
|
+
ctx.addIssue({
|
|
38
|
+
code: 'custom',
|
|
39
|
+
path: ['generates'],
|
|
40
|
+
message: `artifact "${artifact.id}" generates pattern "${artifact.generates}": brace expansion is not supported (the matcher would never match it)`,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
31
43
|
});
|
|
32
44
|
// Apply phase configuration for schema-aware apply instructions
|
|
33
45
|
export const ApplyPhaseSchema = z.object({
|
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
import { STORE_SELECTION_GUIDANCE } from './store-selection.js';
|
|
2
|
+
import { PROJECT_ROOT_GUARD } from './project-root.js';
|
|
3
|
+
/**
|
|
4
|
+
* `continue` is not among FreeHarness's six emitted workflows, so the
|
|
5
|
+
* blocked-state handoff below always renders the CLI fallback rather than
|
|
6
|
+
* naming a skill that was never generated.
|
|
7
|
+
*/
|
|
8
|
+
const BLOCKED_STATE_HANDOFF = 'run `openspec status --change "<name>" --json`, select the next `ready` artifact (not `skipped` or `blocked`), and use `openspec instructions "<artifact-id>" --change "<name>" --json` for its rules and template. Keep the selected `--store <id>` on both commands.';
|
|
2
9
|
/**
|
|
3
10
|
* The apply workflow instructions, authored once and rendered by both the
|
|
4
11
|
* skill and command surfaces. The surfaces are intentionally distinct, but
|
|
@@ -13,6 +20,8 @@ export function getApplyInstructions() {
|
|
|
13
20
|
|
|
14
21
|
${STORE_SELECTION_GUIDANCE}
|
|
15
22
|
|
|
23
|
+
${PROJECT_ROOT_GUARD}
|
|
24
|
+
|
|
16
25
|
**Input**: Optionally specify a change name (e.g., \`/opsx:apply add-auth\`). If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes.
|
|
17
26
|
|
|
18
27
|
**Steps**
|
|
@@ -44,14 +53,17 @@ ${STORE_SELECTION_GUIDANCE}
|
|
|
44
53
|
This returns:
|
|
45
54
|
- \`contextFiles\`: artifact ID -> array of concrete file paths (varies by schema - could be proposal/specs/design/tasks or spec/tests/implementation/docs)
|
|
46
55
|
- Progress (total, complete, remaining)
|
|
47
|
-
- Task list with status
|
|
56
|
+
- Task list with status, source path, and source line
|
|
48
57
|
- Dynamic instruction based on current state
|
|
49
58
|
- Optional \`context\`: current required project instruction input from the selected root
|
|
50
59
|
- Optional \`operationGuidance\`: current advisory guidance for apply
|
|
60
|
+
- \`missingArtifacts\` (when present): required artifact ids with no output
|
|
51
61
|
|
|
52
62
|
**Handle states:**
|
|
53
|
-
- If \`state: "blocked"
|
|
54
|
-
|
|
63
|
+
- If \`state: "blocked"\`: show the message and pause implementation.
|
|
64
|
+
- If \`missingArtifacts\` is non-empty: ${BLOCKED_STATE_HANDOFF}
|
|
65
|
+
- Otherwise, follow the CLI instruction to create or repair the schema-configured tracking file from existing planning artifacts. Do not assume another artifact is ready or start implementation while blocked.
|
|
66
|
+
- If \`state: "all_done"\`: report that all tracked tasks are complete and suggest review or verification as appropriate before archiving
|
|
55
67
|
- Otherwise: proceed to implementation
|
|
56
68
|
|
|
57
69
|
Treat \`context\` as a required prompt-level input. Read and consider it, and
|
|
@@ -93,7 +105,9 @@ ${STORE_SELECTION_GUIDANCE}
|
|
|
93
105
|
- Show which task is being worked on
|
|
94
106
|
- Make the code changes required
|
|
95
107
|
- Keep changes minimal and focused
|
|
96
|
-
-
|
|
108
|
+
- Before editing, confirm the checkbox at the returned \`sourcePath\` and \`line\` still matches the task description; if it does not, rerun the apply instructions and use the refreshed location
|
|
109
|
+
- Mark the task complete at its returned \`sourcePath\` and \`line\`: \`- [ ]\` → \`- [x]\`
|
|
110
|
+
- Rerun the apply instructions and confirm that task is now done and progress changed
|
|
97
111
|
- Continue to next task
|
|
98
112
|
|
|
99
113
|
**Pause if:**
|
|
@@ -108,7 +122,7 @@ ${STORE_SELECTION_GUIDANCE}
|
|
|
108
122
|
Display:
|
|
109
123
|
- Tasks completed this session
|
|
110
124
|
- Overall progress: "N/M tasks complete"
|
|
111
|
-
- If all done: suggest
|
|
125
|
+
- If all done: report that tracked tasks are complete and suggest review or verification as appropriate before archiving
|
|
112
126
|
- If paused: explain why and wait for guidance
|
|
113
127
|
|
|
114
128
|
**Output During Implementation**
|
|
@@ -139,7 +153,7 @@ Working on task 4/7: <task description>
|
|
|
139
153
|
- [x] Task 2
|
|
140
154
|
...
|
|
141
155
|
|
|
142
|
-
All tasks complete
|
|
156
|
+
All tracked tasks are complete. Review or verify the change as appropriate before archiving. You can archive this change with \`/opsx:archive\`.
|
|
143
157
|
\`\`\`
|
|
144
158
|
|
|
145
159
|
**Output On Pause (Issue Encountered)**
|
|
@@ -173,6 +187,7 @@ What would you like to do?
|
|
|
173
187
|
- When a task needs work beyond what the spec describes, surface the added scope and pause - never silently narrow, defer, or simplify away specified behavior
|
|
174
188
|
- Only mark a task \`- [x]\` when its specified behavior is fully implemented, not when it is partially done or deferred
|
|
175
189
|
- Use contextFiles from CLI output, don't assume specific file names
|
|
190
|
+
- Use each task's sourcePath and line to update its exact checkbox
|
|
176
191
|
- Do not use context or operation guidance as proof that a task is complete
|
|
177
192
|
- Apply relevant project context; report conflicts with controlling workflow inputs
|
|
178
193
|
- Consider every guidance entry; explain any inapplicable or conflicting advice
|