@codewalla_india/openspec 1.3.1 → 1.3.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/LICENSE +22 -22
- package/README.md +272 -191
- package/bin/openspec.js +5 -5
- package/dist/cli/index.js +99 -62
- package/dist/commands/change.d.ts +0 -2
- package/dist/commands/change.js +107 -86
- package/dist/commands/config.js +14 -16
- package/dist/commands/doctor.js +9 -4
- package/dist/commands/feedback.js +79 -30
- package/dist/commands/modify.d.ts +26 -0
- package/dist/commands/modify.js +147 -0
- package/dist/commands/schema.js +170 -91
- package/dist/commands/show.js +5 -2
- package/dist/commands/spec.js +49 -29
- package/dist/commands/store.js +15 -21
- package/dist/commands/validate.d.ts +8 -0
- package/dist/commands/validate.js +43 -20
- package/dist/commands/workflow/index.d.ts +2 -2
- package/dist/commands/workflow/index.js +1 -1
- package/dist/commands/workflow/instructions.d.ts +6 -26
- package/dist/commands/workflow/instructions.js +183 -376
- package/dist/commands/workflow/new-change.d.ts +0 -4
- package/dist/commands/workflow/new-change.js +10 -28
- package/dist/commands/workflow/shared.d.ts +34 -19
- package/dist/commands/workflow/shared.js +34 -4
- package/dist/commands/workflow/status.js +10 -32
- package/dist/commands/workflow/templates.js +15 -5
- package/dist/commands/workset.d.ts +2 -2
- package/dist/commands/workset.js +19 -22
- package/dist/comprehension-quiz/fingerprint.d.ts +39 -0
- package/dist/comprehension-quiz/fingerprint.js +84 -0
- package/dist/comprehension-quiz/index.d.ts +42 -0
- package/dist/comprehension-quiz/index.js +47 -0
- package/dist/comprehension-quiz/pass-record.d.ts +44 -0
- package/dist/comprehension-quiz/pass-record.js +98 -0
- package/dist/comprehension-quiz/providers/anthropic.d.ts +34 -0
- package/dist/comprehension-quiz/providers/anthropic.js +53 -0
- package/dist/comprehension-quiz/providers/factory.d.ts +44 -0
- package/dist/comprehension-quiz/providers/factory.js +174 -0
- package/dist/comprehension-quiz/providers/interface.d.ts +59 -0
- package/dist/comprehension-quiz/providers/interface.js +63 -0
- package/dist/comprehension-quiz/providers/local.d.ts +34 -0
- package/dist/comprehension-quiz/providers/local.js +53 -0
- package/dist/comprehension-quiz/providers/ollama.d.ts +34 -0
- package/dist/comprehension-quiz/providers/ollama.js +53 -0
- package/dist/comprehension-quiz/providers/openai.d.ts +34 -0
- package/dist/comprehension-quiz/providers/openai.js +53 -0
- package/dist/comprehension-quiz/question-generator.d.ts +69 -0
- package/dist/comprehension-quiz/question-generator.js +141 -0
- package/dist/comprehension-quiz/quiz-executor.d.ts +70 -0
- package/dist/comprehension-quiz/quiz-executor.js +165 -0
- package/dist/comprehension-quiz/types.d.ts +115 -0
- package/dist/comprehension-quiz/types.js +11 -0
- package/dist/core/archive.d.ts +14 -1
- package/dist/core/archive.js +1350 -190
- package/dist/core/artifact-graph/graph.d.ts +28 -9
- package/dist/core/artifact-graph/graph.js +94 -43
- package/dist/core/artifact-graph/index.d.ts +1 -1
- package/dist/core/artifact-graph/index.js +1 -1
- package/dist/core/artifact-graph/instruction-loader.d.ts +30 -3
- package/dist/core/artifact-graph/instruction-loader.js +70 -12
- package/dist/core/artifact-graph/outputs.d.ts +1 -0
- package/dist/core/artifact-graph/outputs.js +67 -5
- package/dist/core/artifact-graph/resolver.d.ts +14 -0
- package/dist/core/artifact-graph/resolver.js +69 -15
- package/dist/core/artifact-graph/types.js +21 -3
- package/dist/core/available-tools.d.ts +2 -2
- package/dist/core/available-tools.js +14 -3
- package/dist/core/change-metadata/schema.d.ts +2 -0
- package/dist/core/change-metadata/schema.js +13 -0
- package/dist/core/change-status-policy.d.ts +10 -1
- package/dist/core/change-status-policy.js +30 -2
- package/dist/core/command-generation/adapters/amazon-q.d.ts +4 -0
- package/dist/core/command-generation/adapters/amazon-q.js +11 -5
- package/dist/core/command-generation/adapters/antigravity.js +6 -5
- package/dist/core/command-generation/adapters/auggie.js +7 -6
- package/dist/core/command-generation/adapters/bob.d.ts +5 -1
- package/dist/core/command-generation/adapters/bob.js +11 -10
- package/dist/core/command-generation/adapters/claude.d.ts +1 -1
- package/dist/core/command-generation/adapters/claude.js +12 -17
- package/dist/core/command-generation/adapters/cline.js +5 -5
- package/dist/core/command-generation/adapters/codebuddy.js +8 -7
- package/dist/core/command-generation/adapters/continue.js +8 -7
- package/dist/core/command-generation/adapters/costrict.js +7 -6
- package/dist/core/command-generation/adapters/crush.js +9 -9
- package/dist/core/command-generation/adapters/cursor.js +8 -8
- package/dist/core/command-generation/adapters/devin.d.ts +19 -0
- package/dist/core/command-generation/adapters/devin.js +36 -0
- package/dist/core/command-generation/adapters/factory.js +7 -6
- package/dist/core/command-generation/adapters/gemini.js +40 -5
- package/dist/core/command-generation/adapters/github-copilot.js +6 -5
- package/dist/core/command-generation/adapters/iflow.js +9 -8
- package/dist/core/command-generation/adapters/index.d.ts +4 -2
- package/dist/core/command-generation/adapters/index.js +4 -2
- package/dist/core/command-generation/adapters/junie.js +6 -5
- package/dist/core/command-generation/adapters/kilocode.js +1 -1
- package/dist/core/command-generation/adapters/kiro.js +6 -5
- package/dist/core/command-generation/adapters/lingma.js +9 -9
- package/dist/core/command-generation/adapters/oh-my-pi.d.ts +21 -0
- package/dist/core/command-generation/adapters/oh-my-pi.js +42 -0
- package/dist/core/command-generation/adapters/opencode.js +6 -8
- package/dist/core/command-generation/adapters/pi.d.ts +2 -2
- package/dist/core/command-generation/adapters/pi.js +7 -10
- package/dist/core/command-generation/adapters/qoder.js +9 -9
- package/dist/core/command-generation/adapters/qwen.d.ts +7 -3
- package/dist/core/command-generation/adapters/qwen.js +14 -9
- package/dist/core/command-generation/adapters/roocode.d.ts +4 -4
- package/dist/core/command-generation/adapters/roocode.js +9 -9
- package/dist/core/command-generation/adapters/trae.d.ts +13 -0
- package/dist/core/command-generation/adapters/trae.js +28 -0
- package/dist/core/command-generation/adapters/zcode.d.ts +16 -0
- package/dist/core/command-generation/adapters/zcode.js +33 -0
- package/dist/core/command-generation/generator.d.ts +8 -0
- package/dist/core/command-generation/generator.js +15 -1
- package/dist/core/command-generation/index.d.ts +1 -1
- package/dist/core/command-generation/index.js +1 -1
- package/dist/core/command-generation/invocation.d.ts +73 -0
- package/dist/core/command-generation/invocation.js +77 -0
- package/dist/core/command-generation/registry.js +8 -4
- package/dist/core/command-generation/types.d.ts +9 -2
- package/dist/core/command-generation/yaml.d.ts +20 -6
- package/dist/core/command-generation/yaml.js +31 -21
- package/dist/core/command-surface.d.ts +15 -0
- package/dist/core/command-surface.js +33 -0
- package/dist/core/completions/command-registry.js +16 -61
- package/dist/core/completions/generators/bash-generator.js +41 -41
- package/dist/core/completions/generators/fish-generator.js +7 -7
- package/dist/core/completions/generators/powershell-generator.js +44 -37
- package/dist/core/completions/generators/zsh-generator.js +33 -33
- package/dist/core/completions/installers/zsh-installer.d.ts +9 -0
- package/dist/core/completions/installers/zsh-installer.js +20 -4
- package/dist/core/completions/templates/bash-templates.js +24 -24
- package/dist/core/completions/templates/fish-templates.js +38 -38
- package/dist/core/completions/templates/powershell-templates.js +28 -28
- package/dist/core/completions/templates/zsh-templates.js +39 -39
- package/dist/core/config-prompts.d.ts +1 -1
- package/dist/core/config-prompts.js +14 -1
- package/dist/core/config-schema.d.ts +10 -1
- package/dist/core/config-schema.js +67 -1
- package/dist/core/config.d.ts +15 -0
- package/dist/core/config.js +46 -5
- package/dist/core/file-state.js +56 -24
- package/dist/core/github-copilot/cloud-agent.d.ts +95 -0
- package/dist/core/github-copilot/cloud-agent.js +497 -0
- package/dist/core/global-config.d.ts +16 -0
- package/dist/core/init.d.ts +58 -0
- package/dist/core/init.js +474 -73
- package/dist/core/legacy-cleanup.d.ts +59 -0
- package/dist/core/legacy-cleanup.js +253 -24
- package/dist/core/list.js +23 -16
- package/dist/core/migration.d.ts +73 -1
- package/dist/core/migration.js +360 -11
- package/dist/core/modify/conflict-detector.d.ts +26 -0
- package/dist/core/modify/conflict-detector.js +62 -0
- package/dist/core/modify/dependency-propagator.d.ts +38 -0
- package/dist/core/modify/dependency-propagator.js +103 -0
- package/dist/core/modify/history-tracker.d.ts +34 -0
- package/dist/core/modify/history-tracker.js +114 -0
- package/dist/core/modify/index.d.ts +7 -0
- package/dist/core/modify/index.js +7 -0
- package/dist/core/onboarding-commands.d.ts +30 -0
- package/dist/core/onboarding-commands.js +38 -0
- package/dist/core/openspec-root.js +18 -15
- package/dist/core/parsers/change-parser.d.ts +17 -2
- package/dist/core/parsers/change-parser.js +35 -21
- package/dist/core/parsers/code-fence.d.ts +15 -0
- package/dist/core/parsers/code-fence.js +49 -0
- package/dist/core/parsers/markdown-parser.d.ts +0 -3
- package/dist/core/parsers/markdown-parser.js +7 -60
- package/dist/core/parsers/requirement-blocks.d.ts +29 -0
- package/dist/core/parsers/requirement-blocks.js +150 -31
- package/dist/core/parsers/requirement-text.d.ts +48 -0
- package/dist/core/parsers/requirement-text.js +98 -0
- package/dist/core/parsers/spec-structure.d.ts +1 -1
- package/dist/core/parsers/spec-structure.js +20 -30
- package/dist/core/profile-sync-drift.d.ts +0 -8
- package/dist/core/profile-sync-drift.js +49 -50
- package/dist/core/profiles.d.ts +2 -2
- package/dist/core/profiles.js +2 -2
- package/dist/core/project-config.d.ts +27 -18
- package/dist/core/project-config.js +101 -34
- package/dist/core/references.js +42 -4
- package/dist/core/relationship-health.d.ts +8 -0
- package/dist/core/relationship-health.js +13 -0
- package/dist/core/root-selection.d.ts +9 -6
- package/dist/core/root-selection.js +42 -5
- package/dist/core/schemas/base.schema.js +7 -2
- package/dist/core/shared/allowed-tools.d.ts +12 -0
- package/dist/core/shared/allowed-tools.js +12 -0
- package/dist/core/shared/index.d.ts +1 -0
- package/dist/core/shared/index.js +1 -0
- package/dist/core/shared/skill-content-equivalence.d.ts +7 -0
- package/dist/core/shared/skill-content-equivalence.js +41 -0
- package/dist/core/shared/skill-generation.js +17 -13
- package/dist/core/shared/skill-paths.d.ts +13 -0
- package/dist/core/shared/skill-paths.js +23 -0
- package/dist/core/shared/tool-detection.d.ts +27 -7
- package/dist/core/shared/tool-detection.js +167 -37
- package/dist/core/shared-skill-target.d.ts +16 -0
- package/dist/core/shared-skill-target.js +156 -0
- package/dist/core/specs-apply.d.ts +80 -33
- package/dist/core/specs-apply.js +667 -145
- package/dist/core/store/git.d.ts +13 -0
- package/dist/core/store/git.js +24 -0
- package/dist/core/store/operations.js +20 -0
- package/dist/core/templates/skill-templates.d.ts +3 -2
- package/dist/core/templates/skill-templates.js +3 -2
- package/dist/core/templates/workflows/apply-change.d.ts +10 -0
- package/dist/core/templates/workflows/apply-change.js +186 -224
- package/dist/core/templates/workflows/archive-change.js +387 -260
- package/dist/core/templates/workflows/bulk-archive-change.js +655 -475
- package/dist/core/templates/workflows/continue-change.js +210 -219
- package/dist/core/templates/workflows/explore.js +479 -443
- package/dist/core/templates/workflows/feedback.js +97 -97
- package/dist/core/templates/workflows/ff-change.js +200 -191
- package/dist/core/templates/workflows/modify-change.d.ts +4 -1
- package/dist/core/templates/workflows/modify-change.js +237 -115
- package/dist/core/templates/workflows/new-change.js +127 -134
- package/dist/core/templates/workflows/onboard.js +548 -542
- package/dist/core/templates/workflows/propose.js +273 -217
- package/dist/core/templates/workflows/store-selection.d.ts +1 -1
- package/dist/core/templates/workflows/store-selection.js +1 -1
- package/dist/core/templates/workflows/sync-specs.js +500 -273
- package/dist/core/templates/workflows/update-change.d.ts +10 -0
- package/dist/core/templates/workflows/update-change.js +177 -0
- package/dist/core/templates/workflows/verify-change.js +324 -326
- package/dist/core/update.d.ts +40 -5
- package/dist/core/update.js +411 -96
- package/dist/core/validation/constants.d.ts +4 -1
- package/dist/core/validation/constants.js +4 -1
- package/dist/core/validation/plan-validator.d.ts +8 -0
- package/dist/core/validation/plan-validator.js +45 -0
- package/dist/core/validation/task-numbering.d.ts +16 -0
- package/dist/core/validation/task-numbering.js +57 -0
- package/dist/core/validation/validator.d.ts +27 -3
- package/dist/core/validation/validator.js +337 -48
- package/dist/core/version-check.d.ts +152 -0
- package/dist/core/version-check.js +731 -0
- package/dist/core/view.js +13 -18
- package/dist/prompts/searchable-multi-select.js +1 -1
- package/dist/telemetry/caller-detection.d.ts +27 -0
- package/dist/telemetry/caller-detection.js +85 -0
- package/dist/telemetry/caller-detection.test.d.ts +5 -0
- package/dist/telemetry/config.d.ts +48 -2
- package/dist/telemetry/config.js +113 -11
- package/dist/telemetry/config.test.d.ts +5 -0
- package/dist/telemetry/deduplication.d.ts +35 -0
- package/dist/telemetry/deduplication.js +82 -0
- package/dist/telemetry/deduplication.test.d.ts +5 -0
- package/dist/telemetry/identity.d.ts +62 -22
- package/dist/telemetry/identity.js +147 -102
- package/dist/telemetry/index.d.ts +126 -15
- package/dist/telemetry/index.js +569 -37
- package/dist/telemetry/sanitization.d.ts +40 -0
- package/dist/telemetry/sanitization.js +114 -0
- package/dist/telemetry/sanitization.test.d.ts +5 -0
- package/dist/telemetry/throttling.d.ts +20 -0
- package/dist/telemetry/throttling.js +70 -0
- package/dist/ui/welcome-screen.d.ts +9 -1
- package/dist/ui/welcome-screen.js +91 -38
- package/dist/utils/change-metadata.d.ts +43 -0
- package/dist/utils/change-metadata.js +123 -7
- package/dist/utils/change-utils.d.ts +10 -4
- package/dist/utils/change-utils.js +20 -13
- package/dist/utils/ci.d.ts +12 -0
- package/dist/utils/ci.js +16 -0
- package/dist/utils/command-references.d.ts +92 -6
- package/dist/utils/command-references.js +180 -7
- package/dist/utils/date.d.ts +8 -0
- package/dist/utils/date.js +13 -0
- package/dist/utils/file-system.d.ts +10 -0
- package/dist/utils/file-system.js +70 -0
- package/dist/utils/index.d.ts +1 -1
- package/dist/utils/index.js +1 -1
- package/dist/utils/interactive.d.ts +23 -0
- package/dist/utils/interactive.js +32 -0
- package/dist/utils/item-discovery.d.ts +19 -0
- package/dist/utils/item-discovery.js +30 -48
- package/dist/utils/shell-detection.d.ts +1 -1
- package/dist/utils/shell-detection.js +64 -11
- package/dist/utils/spec-discovery.d.ts +40 -0
- package/dist/utils/spec-discovery.js +115 -0
- package/dist/utils/task-progress.d.ts +29 -1
- package/dist/utils/task-progress.js +109 -15
- package/package.json +96 -84
- package/schemas/spec-driven/schema.yaml +252 -184
- package/schemas/spec-driven/templates/design.md +19 -19
- package/schemas/spec-driven/templates/plan-template.md +91 -0
- package/schemas/spec-driven/templates/proposal.md +29 -24
- package/schemas/spec-driven/templates/spec.md +11 -8
- package/schemas/spec-driven/templates/tasks.md +9 -9
- package/scripts/postinstall.js +83 -83
- package/dist/core/command-generation/adapters/codex.d.ts +0 -16
- package/dist/core/command-generation/adapters/codex.js +0 -39
- package/dist/core/command-generation/adapters/windsurf.d.ts +0 -14
- package/dist/core/command-generation/adapters/windsurf.js +0 -38
- package/dist/core/comprehension/config.d.ts +0 -20
- package/dist/core/comprehension/config.js +0 -23
- package/dist/core/comprehension/fingerprint.d.ts +0 -14
- package/dist/core/comprehension/fingerprint.js +0 -33
- package/dist/core/comprehension/index.d.ts +0 -57
- package/dist/core/comprehension/index.js +0 -87
- package/dist/core/comprehension/pass-record.d.ts +0 -29
- package/dist/core/comprehension/pass-record.js +0 -64
- package/dist/core/comprehension/stats.d.ts +0 -35
- package/dist/core/comprehension/stats.js +0 -133
- package/dist/core/templates/workflows/comprehension-guidance.d.ts +0 -9
- package/dist/core/templates/workflows/comprehension-guidance.js +0 -73
- package/dist/core/templates/workflows/mcp-guidance.d.ts +0 -13
- package/dist/core/templates/workflows/mcp-guidance.js +0 -131
- package/dist/core/templates/workflows/user-prompt-guidance.d.ts +0 -16
- package/dist/core/templates/workflows/user-prompt-guidance.js +0 -48
- package/dist/telemetry/caller.d.ts +0 -5
- package/dist/telemetry/caller.js +0 -29
- package/dist/telemetry/client.d.ts +0 -27
- package/dist/telemetry/client.js +0 -127
- package/dist/telemetry/command-context.d.ts +0 -13
- package/dist/telemetry/command-context.js +0 -59
- package/dist/telemetry/comprehension.d.ts +0 -44
- package/dist/telemetry/comprehension.js +0 -105
- package/dist/telemetry/content.d.ts +0 -10
- package/dist/telemetry/content.js +0 -56
- package/dist/telemetry/git-stats.d.ts +0 -12
- package/dist/telemetry/git-stats.js +0 -69
- package/dist/telemetry/identify-cache.d.ts +0 -7
- package/dist/telemetry/identify-cache.js +0 -47
- package/dist/telemetry/input.d.ts +0 -17
- package/dist/telemetry/input.js +0 -68
- package/dist/telemetry/marker.d.ts +0 -37
- package/dist/telemetry/marker.js +0 -67
- package/dist/telemetry/workflow.d.ts +0 -85
- package/dist/telemetry/workflow.js +0 -318
- package/schemas/spec-driven/templates/plan.md +0 -19
|
@@ -1,23 +1,25 @@
|
|
|
1
1
|
import { promises as fs } from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
|
+
import { discoverSpecFiles } from './spec-discovery.js';
|
|
4
|
+
/**
|
|
5
|
+
* Returns the ids of active changes: every directory under openspec/changes/
|
|
6
|
+
* except the archive and hidden directories.
|
|
7
|
+
*
|
|
8
|
+
* A change is resolved by its directory alone - the same rule `list`,
|
|
9
|
+
* `status`, `instructions` and `validate` use (`getAvailableChanges`).
|
|
10
|
+
* Requiring proposal.md here made `openspec show` and shell completion miss
|
|
11
|
+
* changes those commands resolve: `openspec new change <name>` scaffolds only
|
|
12
|
+
* `.openspec.yaml`, and a custom schema need not define a proposal artifact at
|
|
13
|
+
* all (#1161).
|
|
14
|
+
*/
|
|
3
15
|
export async function getActiveChangeIds(root = process.cwd()) {
|
|
4
16
|
const changesPath = path.join(root, 'openspec', 'changes');
|
|
5
17
|
try {
|
|
6
18
|
const entries = await fs.readdir(changesPath, { withFileTypes: true });
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
const proposalPath = path.join(changesPath, entry.name, 'proposal.md');
|
|
12
|
-
try {
|
|
13
|
-
await fs.access(proposalPath);
|
|
14
|
-
result.push(entry.name);
|
|
15
|
-
}
|
|
16
|
-
catch {
|
|
17
|
-
// skip directories without proposal.md
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
return result.sort();
|
|
19
|
+
return entries
|
|
20
|
+
.filter((entry) => entry.isDirectory() && entry.name !== 'archive' && !entry.name.startsWith('.'))
|
|
21
|
+
.map((entry) => entry.name)
|
|
22
|
+
.sort();
|
|
21
23
|
}
|
|
22
24
|
catch {
|
|
23
25
|
return [];
|
|
@@ -25,45 +27,25 @@ export async function getActiveChangeIds(root = process.cwd()) {
|
|
|
25
27
|
}
|
|
26
28
|
export async function getSpecIds(root = process.cwd()) {
|
|
27
29
|
const specsPath = path.join(root, 'openspec', 'specs');
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
const entries = await fs.readdir(specsPath, { withFileTypes: true });
|
|
31
|
-
for (const entry of entries) {
|
|
32
|
-
if (!entry.isDirectory() || entry.name.startsWith('.'))
|
|
33
|
-
continue;
|
|
34
|
-
const specFile = path.join(specsPath, entry.name, 'spec.md');
|
|
35
|
-
try {
|
|
36
|
-
await fs.access(specFile);
|
|
37
|
-
result.push(entry.name);
|
|
38
|
-
}
|
|
39
|
-
catch {
|
|
40
|
-
// ignore
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
catch {
|
|
45
|
-
// ignore
|
|
46
|
-
}
|
|
47
|
-
return result.sort();
|
|
30
|
+
const discovered = await discoverSpecFiles(specsPath);
|
|
31
|
+
return discovered.map((spec) => spec.id);
|
|
48
32
|
}
|
|
33
|
+
/**
|
|
34
|
+
* Returns the ids of archived changes: every directory under
|
|
35
|
+
* openspec/changes/archive/ except hidden directories.
|
|
36
|
+
*
|
|
37
|
+
* Resolved by directory for the same reason as `getActiveChangeIds`: a change
|
|
38
|
+
* archived from a schema without a proposal artifact has no proposal.md, and
|
|
39
|
+
* gating on it hid those entries from shell completion.
|
|
40
|
+
*/
|
|
49
41
|
export async function getArchivedChangeIds(root = process.cwd()) {
|
|
50
42
|
const archivePath = path.join(root, 'openspec', 'changes', 'archive');
|
|
51
43
|
try {
|
|
52
44
|
const entries = await fs.readdir(archivePath, { withFileTypes: true });
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
const proposalPath = path.join(archivePath, entry.name, 'proposal.md');
|
|
58
|
-
try {
|
|
59
|
-
await fs.access(proposalPath);
|
|
60
|
-
result.push(entry.name);
|
|
61
|
-
}
|
|
62
|
-
catch {
|
|
63
|
-
// skip directories without proposal.md
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
return result.sort();
|
|
45
|
+
return entries
|
|
46
|
+
.filter((entry) => entry.isDirectory() && !entry.name.startsWith('.'))
|
|
47
|
+
.map((entry) => entry.name)
|
|
48
|
+
.sort();
|
|
67
49
|
}
|
|
68
50
|
catch {
|
|
69
51
|
return [];
|
|
@@ -12,7 +12,7 @@ export interface ShellDetectionResult {
|
|
|
12
12
|
detected: string | undefined;
|
|
13
13
|
}
|
|
14
14
|
/**
|
|
15
|
-
* Detects the current user's shell based on environment
|
|
15
|
+
* Detects the current user's shell based on the parent process and environment
|
|
16
16
|
*
|
|
17
17
|
* @returns Detection result with supported shell and raw detected name
|
|
18
18
|
*/
|
|
@@ -1,21 +1,74 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
1
2
|
/**
|
|
2
|
-
*
|
|
3
|
+
* Map a raw shell name/path to a supported shell, if any.
|
|
4
|
+
*/
|
|
5
|
+
function matchSupportedShell(name) {
|
|
6
|
+
// Match the executable basename exactly so lookalikes such as `fish-lsp` or
|
|
7
|
+
// `bash-language-server` don't get mistaken for the shell itself. Login
|
|
8
|
+
// shells report a leading dash (e.g. `-zsh`), so strip it first.
|
|
9
|
+
const executable = name.trim().toLowerCase().split('/').pop()?.replace(/^-/, '');
|
|
10
|
+
if (executable === 'zsh')
|
|
11
|
+
return 'zsh';
|
|
12
|
+
if (executable === 'bash')
|
|
13
|
+
return 'bash';
|
|
14
|
+
if (executable === 'fish')
|
|
15
|
+
return 'fish';
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Detect the interactive shell from the parent process.
|
|
20
|
+
*
|
|
21
|
+
* `process.env.SHELL` is only the login shell, so users whose interactive shell
|
|
22
|
+
* differs from it (e.g. running fish while their login shell is bash) are
|
|
23
|
+
* misdetected. Inspecting the parent process reflects the shell that actually
|
|
24
|
+
* launched openspec. POSIX-only and best-effort — any failure returns undefined
|
|
25
|
+
* so the caller falls back to `$SHELL`.
|
|
26
|
+
*
|
|
27
|
+
* @returns The supported shell running as the parent process, or undefined
|
|
28
|
+
*/
|
|
29
|
+
function detectShellFromParentProcess() {
|
|
30
|
+
// `ps` is POSIX-only; Windows shells are handled via PSModulePath/COMSPEC.
|
|
31
|
+
if (process.platform === 'win32') {
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
const ppid = process.ppid;
|
|
35
|
+
if (!ppid || ppid <= 1) {
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
try {
|
|
39
|
+
const comm = execFileSync('ps', ['-p', String(ppid), '-o', 'comm='], {
|
|
40
|
+
encoding: 'utf8',
|
|
41
|
+
timeout: 1000,
|
|
42
|
+
}).trim();
|
|
43
|
+
if (!comm) {
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
// Only trust the parent process when it maps to a supported shell; an
|
|
47
|
+
// unrelated parent (node, npm, sudo, a pager) falls through to `$SHELL`.
|
|
48
|
+
return matchSupportedShell(comm);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Detects the current user's shell based on the parent process and environment
|
|
3
56
|
*
|
|
4
57
|
* @returns Detection result with supported shell and raw detected name
|
|
5
58
|
*/
|
|
6
59
|
export function detectShell() {
|
|
7
|
-
//
|
|
60
|
+
// Prefer the actual running shell (parent process) over `$SHELL`, which only
|
|
61
|
+
// reflects the login shell and misses users whose interactive shell differs.
|
|
62
|
+
const parentShell = detectShellFromParentProcess();
|
|
63
|
+
if (parentShell) {
|
|
64
|
+
return { shell: parentShell, detected: parentShell };
|
|
65
|
+
}
|
|
66
|
+
// Try SHELL environment variable next (Unix-like systems)
|
|
8
67
|
const shellPath = process.env.SHELL;
|
|
9
68
|
if (shellPath) {
|
|
10
|
-
const
|
|
11
|
-
if (
|
|
12
|
-
return { shell:
|
|
13
|
-
}
|
|
14
|
-
if (shellName.includes('bash')) {
|
|
15
|
-
return { shell: 'bash', detected: 'bash' };
|
|
16
|
-
}
|
|
17
|
-
if (shellName.includes('fish')) {
|
|
18
|
-
return { shell: 'fish', detected: 'fish' };
|
|
69
|
+
const supported = matchSupportedShell(shellPath);
|
|
70
|
+
if (supported) {
|
|
71
|
+
return { shell: supported, detected: supported };
|
|
19
72
|
}
|
|
20
73
|
// Shell detected but not supported
|
|
21
74
|
// Extract shell name from path (e.g., /bin/tcsh -> tcsh)
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export interface DiscoveredSpec {
|
|
2
|
+
/** Spec id relative to the specs root, forward-slash separated on every platform (e.g. "web" or "platform/session-layout"). */
|
|
3
|
+
id: string;
|
|
4
|
+
/** Path to the spec.md file (absolute if the specs root is absolute). */
|
|
5
|
+
specFile: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Recursively discover every `spec.md` under a specs root, so both the flat
|
|
9
|
+
* `specs/<id>/spec.md` layout and nested `specs/<area>/<id>/spec.md` layouts
|
|
10
|
+
* are found (#1353). A `spec.md` sitting directly in the root is ignored,
|
|
11
|
+
* matching the historical requirement that specs live in a capability folder.
|
|
12
|
+
* Dot-directories are skipped and symlinked directories are not followed.
|
|
13
|
+
* An in-capability symlinked `spec.md` IS resolved: `hasAnyFileUnder` and the
|
|
14
|
+
* artifact graph's globs both count it as content, so dropping it here would
|
|
15
|
+
* silently lose the delta on archive. A link outside its capability is
|
|
16
|
+
* rejected and a dangling link is skipped. Results are sorted by id.
|
|
17
|
+
*
|
|
18
|
+
* A missing root (ENOENT) yields an empty list, but any other read failure
|
|
19
|
+
* (EACCES, EIO, ...) is thrown rather than swallowed: since this feeds the
|
|
20
|
+
* archive/apply merge path, silently dropping an unreadable capability would
|
|
21
|
+
* recreate the exact data-loss class #1353 is closing.
|
|
22
|
+
*/
|
|
23
|
+
export declare function discoverSpecFiles(specsRoot: string): Promise<DiscoveredSpec[]>;
|
|
24
|
+
/**
|
|
25
|
+
* True when any regular non-dot file exists anywhere under the given
|
|
26
|
+
* directory. Used by validate/archive to detect content under a change's
|
|
27
|
+
* specs/ that contradicts a declared skip_specs marker - including files that
|
|
28
|
+
* discoverSpecFiles ignores (a root spec.md, stray non-spec.md notes), since
|
|
29
|
+
* anything there would be silently dropped or misread while the change claims
|
|
30
|
+
* to have nothing. Dot entries (.DS_Store, .gitkeep, dot-directories) are
|
|
31
|
+
* skipped to match discoverSpecFiles - they are invisible to every other
|
|
32
|
+
* code path, so they must not count as spec content. Symlinks DO count
|
|
33
|
+
* (without being followed): the artifact graph's globs follow them, so a
|
|
34
|
+
* symlinked spec would read as existing content while the change claims to
|
|
35
|
+
* have none - it contradicts the marker like any regular file. A missing
|
|
36
|
+
* directory returns false; other read failures are thrown for the caller to
|
|
37
|
+
* decide.
|
|
38
|
+
*/
|
|
39
|
+
export declare function hasAnyFileUnder(dirPath: string): Promise<boolean>;
|
|
40
|
+
//# sourceMappingURL=spec-discovery.d.ts.map
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { promises as fs } from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { FileSystemUtils } from './file-system.js';
|
|
4
|
+
function assertDiscoveredSpecPath(specsRoot, capabilityDir, specFile) {
|
|
5
|
+
try {
|
|
6
|
+
FileSystemUtils.assertPathWithin(specsRoot, specFile);
|
|
7
|
+
}
|
|
8
|
+
catch {
|
|
9
|
+
// Direct capability directories may intentionally be external monorepo
|
|
10
|
+
// links. In that case, confine the file to the capability itself.
|
|
11
|
+
FileSystemUtils.assertPathWithin(capabilityDir, specFile);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Recursively discover every `spec.md` under a specs root, so both the flat
|
|
16
|
+
* `specs/<id>/spec.md` layout and nested `specs/<area>/<id>/spec.md` layouts
|
|
17
|
+
* are found (#1353). A `spec.md` sitting directly in the root is ignored,
|
|
18
|
+
* matching the historical requirement that specs live in a capability folder.
|
|
19
|
+
* Dot-directories are skipped and symlinked directories are not followed.
|
|
20
|
+
* An in-capability symlinked `spec.md` IS resolved: `hasAnyFileUnder` and the
|
|
21
|
+
* artifact graph's globs both count it as content, so dropping it here would
|
|
22
|
+
* silently lose the delta on archive. A link outside its capability is
|
|
23
|
+
* rejected and a dangling link is skipped. Results are sorted by id.
|
|
24
|
+
*
|
|
25
|
+
* A missing root (ENOENT) yields an empty list, but any other read failure
|
|
26
|
+
* (EACCES, EIO, ...) is thrown rather than swallowed: since this feeds the
|
|
27
|
+
* archive/apply merge path, silently dropping an unreadable capability would
|
|
28
|
+
* recreate the exact data-loss class #1353 is closing.
|
|
29
|
+
*/
|
|
30
|
+
export async function discoverSpecFiles(specsRoot) {
|
|
31
|
+
const results = [];
|
|
32
|
+
const walk = async (dir, segments) => {
|
|
33
|
+
let entries;
|
|
34
|
+
try {
|
|
35
|
+
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
36
|
+
}
|
|
37
|
+
catch (err) {
|
|
38
|
+
if (err?.code === 'ENOENT')
|
|
39
|
+
return;
|
|
40
|
+
throw err;
|
|
41
|
+
}
|
|
42
|
+
for (const entry of entries) {
|
|
43
|
+
if (entry.name.startsWith('.'))
|
|
44
|
+
continue;
|
|
45
|
+
if (entry.isDirectory()) {
|
|
46
|
+
await walk(path.join(dir, entry.name), [...segments, entry.name]);
|
|
47
|
+
}
|
|
48
|
+
else if (entry.name === 'spec.md' && segments.length > 0) {
|
|
49
|
+
const specFile = path.join(dir, entry.name);
|
|
50
|
+
if (entry.isFile()) {
|
|
51
|
+
assertDiscoveredSpecPath(specsRoot, dir, specFile);
|
|
52
|
+
results.push({ id: segments.join('/'), specFile });
|
|
53
|
+
}
|
|
54
|
+
else if (entry.isSymbolicLink()) {
|
|
55
|
+
try {
|
|
56
|
+
if ((await fs.stat(specFile)).isFile()) {
|
|
57
|
+
assertDiscoveredSpecPath(specsRoot, dir, specFile);
|
|
58
|
+
results.push({ id: segments.join('/'), specFile });
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
// A dangling link is not content; anything else fails loudly.
|
|
63
|
+
if (err?.code !== 'ENOENT')
|
|
64
|
+
throw err;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
await walk(specsRoot, []);
|
|
71
|
+
// Plain code-point comparison, not localeCompare: the latter follows the
|
|
72
|
+
// process's ICU locale, so ordering could vary by OS/CI. Code-point ordering
|
|
73
|
+
// guarantees the deterministic output the docstring promises.
|
|
74
|
+
return results.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* True when any regular non-dot file exists anywhere under the given
|
|
78
|
+
* directory. Used by validate/archive to detect content under a change's
|
|
79
|
+
* specs/ that contradicts a declared skip_specs marker - including files that
|
|
80
|
+
* discoverSpecFiles ignores (a root spec.md, stray non-spec.md notes), since
|
|
81
|
+
* anything there would be silently dropped or misread while the change claims
|
|
82
|
+
* to have nothing. Dot entries (.DS_Store, .gitkeep, dot-directories) are
|
|
83
|
+
* skipped to match discoverSpecFiles - they are invisible to every other
|
|
84
|
+
* code path, so they must not count as spec content. Symlinks DO count
|
|
85
|
+
* (without being followed): the artifact graph's globs follow them, so a
|
|
86
|
+
* symlinked spec would read as existing content while the change claims to
|
|
87
|
+
* have none - it contradicts the marker like any regular file. A missing
|
|
88
|
+
* directory returns false; other read failures are thrown for the caller to
|
|
89
|
+
* decide.
|
|
90
|
+
*/
|
|
91
|
+
export async function hasAnyFileUnder(dirPath) {
|
|
92
|
+
let entries;
|
|
93
|
+
try {
|
|
94
|
+
entries = await fs.readdir(dirPath, { withFileTypes: true });
|
|
95
|
+
}
|
|
96
|
+
catch (err) {
|
|
97
|
+
if (err?.code === 'ENOENT') {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
throw err;
|
|
101
|
+
}
|
|
102
|
+
for (const entry of entries) {
|
|
103
|
+
if (entry.name.startsWith('.')) {
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
if (entry.isFile() || entry.isSymbolicLink()) {
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
if (entry.isDirectory() && (await hasAnyFileUnder(path.join(dirPath, entry.name)))) {
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
//# sourceMappingURL=spec-discovery.js.map
|
|
@@ -1,8 +1,36 @@
|
|
|
1
|
+
export interface ParsedTask {
|
|
2
|
+
/** Checkbox state: `[x]`/`[X]` is done, anything else is not. */
|
|
3
|
+
done: boolean;
|
|
4
|
+
/** Task text after the checkbox, trimmed (may be empty). */
|
|
5
|
+
description: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Parses every task line in a tasks file, in document order.
|
|
9
|
+
*
|
|
10
|
+
* Every line matching the pattern counts, wherever it sits - inside a code
|
|
11
|
+
* fence, an HTML comment or an indented block, as before. Skipping fenced
|
|
12
|
+
* checkboxes was tried and dropped: every rule for deciding which fence is
|
|
13
|
+
* "real" has an input where a stray or unbalanced ``` swallows genuine tasks.
|
|
14
|
+
* Counting a documented example as work is a loud, bypassable false positive;
|
|
15
|
+
* losing a real task is a silent one.
|
|
16
|
+
*/
|
|
17
|
+
export declare function parseTaskLines(content: string): ParsedTask[];
|
|
1
18
|
export interface TaskProgress {
|
|
2
19
|
total: number;
|
|
3
20
|
completed: number;
|
|
4
21
|
}
|
|
5
22
|
export declare function countTasksFromContent(content: string): TaskProgress;
|
|
6
|
-
|
|
23
|
+
/** Resolves the task files selected by the schema's apply tracking rule. */
|
|
24
|
+
export declare function resolveTaskFilesForChange(changeDir: string, projectRoot: string): string[];
|
|
25
|
+
/**
|
|
26
|
+
* Computes a change's task progress by resolving its tracked-tasks artifact and
|
|
27
|
+
* counting checkboxes across every file matched by that artifact's `generates`
|
|
28
|
+
* glob — the same file-resolution `openspec status` uses to detect the tasks
|
|
29
|
+
* artifact (`resolveArtifactOutputs`) — so progress is no longer blind to nested
|
|
30
|
+
* `tasks.md` files (#1202). Falls back to a single top-level `tasks.md` (exactly
|
|
31
|
+
* as before) when the schema is unresolvable, no tracked-tasks artifact is found,
|
|
32
|
+
* or the glob matches no file. Never throws.
|
|
33
|
+
*/
|
|
34
|
+
export declare function getTaskProgressForChange(changesDir: string, changeName: string, projectRoot: string): Promise<TaskProgress>;
|
|
7
35
|
export declare function formatTaskStatus(progress: TaskProgress): string;
|
|
8
36
|
//# sourceMappingURL=task-progress.d.ts.map
|
|
@@ -1,23 +1,82 @@
|
|
|
1
1
|
import { promises as fs } from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
3
|
+
import { resolveArtifactOutputs, resolveSchema } from '../core/artifact-graph/index.js';
|
|
4
|
+
import { resolveSchemaForChange } from './change-metadata.js';
|
|
5
|
+
/**
|
|
6
|
+
* A Markdown task line: a `-`/`*` bullet carrying a `[ ]` or `[x]` checkbox.
|
|
7
|
+
*
|
|
8
|
+
* Leading whitespace is allowed so nested sub-tasks count like their parents.
|
|
9
|
+
* Anchoring at column 0 made ` - [ ] 1.1.1 ...` invisible to progress, to the
|
|
10
|
+
* apply task list, and to archive's incomplete-task check, so a change with
|
|
11
|
+
* unfinished sub-tasks reported "✓ Complete" and archived without a warning.
|
|
12
|
+
*
|
|
13
|
+
* Permissive on purpose, and safe to keep that way: any character class
|
|
14
|
+
* tightened here - the `\s` inside the brackets, which lets a tab or
|
|
15
|
+
* non-breaking space stand for an empty box - drops lines that used to count,
|
|
16
|
+
* and a task this parser drops is a task `openspec archive` stops warning about.
|
|
17
|
+
*
|
|
18
|
+
* Deliberately unanchored at the end: `.` does not match `\r`, so writing the
|
|
19
|
+
* description group as `(.*)$` would reject every line of a CRLF tasks.md.
|
|
20
|
+
*/
|
|
21
|
+
const TASK_LINE_PATTERN = /^\s*[-*]\s*\[([\sxX])\]\s*(.*)/;
|
|
22
|
+
/**
|
|
23
|
+
* Parses every task line in a tasks file, in document order.
|
|
24
|
+
*
|
|
25
|
+
* Every line matching the pattern counts, wherever it sits - inside a code
|
|
26
|
+
* fence, an HTML comment or an indented block, as before. Skipping fenced
|
|
27
|
+
* checkboxes was tried and dropped: every rule for deciding which fence is
|
|
28
|
+
* "real" has an input where a stray or unbalanced ``` swallows genuine tasks.
|
|
29
|
+
* Counting a documented example as work is a loud, bypassable false positive;
|
|
30
|
+
* losing a real task is a silent one.
|
|
31
|
+
*/
|
|
32
|
+
export function parseTaskLines(content) {
|
|
33
|
+
const tasks = [];
|
|
34
|
+
for (const line of content.split('\n')) {
|
|
35
|
+
const match = line.match(TASK_LINE_PATTERN);
|
|
36
|
+
if (match) {
|
|
37
|
+
tasks.push({ done: match[1].toLowerCase() === 'x', description: match[2].trim() });
|
|
15
38
|
}
|
|
16
39
|
}
|
|
17
|
-
return
|
|
40
|
+
return tasks;
|
|
18
41
|
}
|
|
19
|
-
export
|
|
20
|
-
const
|
|
42
|
+
export function countTasksFromContent(content) {
|
|
43
|
+
const tasks = parseTaskLines(content);
|
|
44
|
+
return {
|
|
45
|
+
total: tasks.length,
|
|
46
|
+
completed: tasks.filter((task) => task.done).length,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Identifies the change's tracked-tasks artifact: the artifact whose `generates`
|
|
51
|
+
* equals the schema's `apply.tracks` value, falling back to the artifact with id
|
|
52
|
+
* `tasks` when no `apply` block declares what it tracks. (`apply.tracks` is a
|
|
53
|
+
* filename that *selects* the artifact; the glob is that artifact's `generates`.)
|
|
54
|
+
*/
|
|
55
|
+
function findTrackedTasksArtifact(schema) {
|
|
56
|
+
const tracks = schema.apply?.tracks;
|
|
57
|
+
if (tracks != null) {
|
|
58
|
+
return schema.artifacts.find((a) => a.generates === tracks);
|
|
59
|
+
}
|
|
60
|
+
return schema.artifacts.find((a) => a.id === 'tasks');
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Resolves the tracked-tasks artifact's output glob for a change, or undefined
|
|
64
|
+
* when the schema cannot be resolved or no tracked-tasks artifact exists.
|
|
65
|
+
* `resolveSchema` throws on an unresolvable/misnamed schema; we swallow that so
|
|
66
|
+
* the caller falls back to a single top-level `tasks.md` and never crashes.
|
|
67
|
+
*/
|
|
68
|
+
function resolveTrackedTasksGlob(changeDir, projectRoot) {
|
|
69
|
+
try {
|
|
70
|
+
const schemaName = resolveSchemaForChange(changeDir, undefined, projectRoot);
|
|
71
|
+
const schema = resolveSchema(schemaName, projectRoot);
|
|
72
|
+
return findTrackedTasksArtifact(schema)?.generates;
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
async function countSingleTopLevelTasksFile(changeDir) {
|
|
79
|
+
const tasksPath = path.join(changeDir, 'tasks.md');
|
|
21
80
|
try {
|
|
22
81
|
const content = await fs.readFile(tasksPath, 'utf-8');
|
|
23
82
|
return countTasksFromContent(content);
|
|
@@ -26,6 +85,41 @@ export async function getTaskProgressForChange(changesDir, changeName) {
|
|
|
26
85
|
return { total: 0, completed: 0 };
|
|
27
86
|
}
|
|
28
87
|
}
|
|
88
|
+
/** Resolves the task files selected by the schema's apply tracking rule. */
|
|
89
|
+
export function resolveTaskFilesForChange(changeDir, projectRoot) {
|
|
90
|
+
const generates = resolveTrackedTasksGlob(changeDir, projectRoot);
|
|
91
|
+
return generates ? resolveArtifactOutputs(changeDir, generates) : [];
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Computes a change's task progress by resolving its tracked-tasks artifact and
|
|
95
|
+
* counting checkboxes across every file matched by that artifact's `generates`
|
|
96
|
+
* glob — the same file-resolution `openspec status` uses to detect the tasks
|
|
97
|
+
* artifact (`resolveArtifactOutputs`) — so progress is no longer blind to nested
|
|
98
|
+
* `tasks.md` files (#1202). Falls back to a single top-level `tasks.md` (exactly
|
|
99
|
+
* as before) when the schema is unresolvable, no tracked-tasks artifact is found,
|
|
100
|
+
* or the glob matches no file. Never throws.
|
|
101
|
+
*/
|
|
102
|
+
export async function getTaskProgressForChange(changesDir, changeName, projectRoot) {
|
|
103
|
+
const changeDir = path.join(changesDir, changeName);
|
|
104
|
+
const files = resolveTaskFilesForChange(changeDir, projectRoot);
|
|
105
|
+
if (files.length > 0) {
|
|
106
|
+
let total = 0;
|
|
107
|
+
let completed = 0;
|
|
108
|
+
for (const file of files) {
|
|
109
|
+
try {
|
|
110
|
+
const content = await fs.readFile(file, 'utf-8');
|
|
111
|
+
const progress = countTasksFromContent(content);
|
|
112
|
+
total += progress.total;
|
|
113
|
+
completed += progress.completed;
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
// Swallow files that vanish between glob and read, as before.
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return { total, completed };
|
|
120
|
+
}
|
|
121
|
+
return countSingleTopLevelTasksFile(changeDir);
|
|
122
|
+
}
|
|
29
123
|
export function formatTaskStatus(progress) {
|
|
30
124
|
if (progress.total === 0)
|
|
31
125
|
return 'No tasks';
|