@fission-ai/openspec 1.8.0 → 1.9.0
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/dist/cli/index.d.ts +15 -0
- package/dist/cli/index.js +33 -4
- package/dist/commands/change.js +1 -0
- package/dist/commands/config.d.ts +5 -0
- package/dist/commands/config.js +5 -1
- package/dist/commands/schema.js +193 -28
- package/dist/commands/validate.d.ts +23 -0
- package/dist/commands/validate.js +148 -4
- package/dist/commands/workflow/schemas.d.ts +2 -0
- package/dist/commands/workflow/schemas.js +9 -2
- package/dist/core/archive.js +10 -3
- package/dist/core/artifact-graph/resolver.d.ts +0 -12
- package/dist/core/artifact-graph/resolver.js +14 -0
- package/dist/core/command-generation/adapters/command-code.d.ts +19 -0
- package/dist/core/command-generation/adapters/command-code.js +34 -0
- package/dist/core/command-generation/adapters/index.d.ts +1 -0
- package/dist/core/command-generation/adapters/index.js +1 -0
- package/dist/core/command-generation/registry.js +2 -0
- package/dist/core/completions/command-registry.js +5 -0
- package/dist/core/config.d.ts +1 -0
- package/dist/core/config.js +16 -15
- package/dist/core/init.js +19 -7
- package/dist/core/legacy-cleanup.d.ts +10 -0
- package/dist/core/legacy-cleanup.js +48 -26
- package/dist/core/parsers/requirement-blocks.js +41 -9
- package/dist/core/parsers/requirement-text.d.ts +8 -0
- package/dist/core/parsers/requirement-text.js +4 -3
- package/dist/core/root-selection.d.ts +1 -1
- package/dist/core/shared-skill-target.d.ts +15 -0
- package/dist/core/shared-skill-target.js +30 -0
- package/dist/core/specs-apply.js +11 -4
- package/dist/core/templates/workflows/apply-change.js +3 -0
- package/dist/core/templates/workflows/propose.js +2 -2
- 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/update-change.js +4 -4
- package/dist/core/update.js +34 -7
- 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 +5 -2
- package/dist/core/validation/validator.js +57 -3
- package/dist/telemetry/index.d.ts +3 -1
- package/dist/telemetry/index.js +7 -1
- package/dist/utils/interactive.d.ts +30 -1
- package/dist/utils/interactive.js +105 -2
- package/dist/utils/task-progress.d.ts +34 -1
- package/dist/utils/task-progress.js +52 -28
- package/package.json +1 -1
|
@@ -99,6 +99,7 @@ ${STORE_SELECTION_GUIDANCE}
|
|
|
99
99
|
**Pause if:**
|
|
100
100
|
- Task is unclear → ask for clarification
|
|
101
101
|
- Implementation reveals a design issue → suggest updating artifacts
|
|
102
|
+
- A task needs work beyond what the spec and tasks describe, or you are tempted to drop, narrow, defer, or accept exceptions to specified behavior to make it fit → surface the added scope and ask; do not absorb it silently
|
|
102
103
|
- Error or blocker encountered → report and wait for guidance
|
|
103
104
|
- User interrupts
|
|
104
105
|
|
|
@@ -169,6 +170,8 @@ What would you like to do?
|
|
|
169
170
|
- Keep code changes minimal and scoped to each task
|
|
170
171
|
- Update task checkbox immediately after completing each task
|
|
171
172
|
- Pause on errors, blockers, or unclear requirements - don't guess
|
|
173
|
+
- 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
|
+
- Only mark a task \`- [x]\` when its specified behavior is fully implemented, not when it is partially done or deferred
|
|
172
175
|
- Use contextFiles from CLI output, don't assume specific file names
|
|
173
176
|
- Do not use context or operation guidance as proof that a task is complete
|
|
174
177
|
- Apply relevant project context; report conflicts with controlling workflow inputs
|
|
@@ -42,7 +42,7 @@ ${STORE_SELECTION_GUIDANCE}
|
|
|
42
42
|
|
|
43
43
|
**Use a different schema only if the user:**
|
|
44
44
|
- Explicitly requests a specific schema by name → use \`--schema <schema-name>\`
|
|
45
|
-
- Asks to "show workflows" or asks "what workflows" exist → resolve the authoritative root by running \`openspec context --json\` from the current working directory. If the user explicitly selected a registered store, use \`openspec context --json --store "<store-id>"\`. Then run \`openspec schemas --json\` with its working directory set to the returned \`root.path\` and let them choose. This preserves roots selected by a local \`store:\` pointer or the global \`defaultStore\`;
|
|
45
|
+
- Asks to "show workflows" or asks "what workflows" exist → resolve the authoritative root by running \`openspec context --json\` from the current working directory. If the user explicitly selected a registered store, use \`openspec context --json --store "<store-id>"\`. Then run \`openspec schemas --json\` with its working directory set to the returned \`root.path\` and let them choose. This preserves roots selected by a local \`store:\` pointer or the global \`defaultStore\`; when a registered store was explicitly selected, append \`--store "<store-id>"\` to \`openspec schemas --json\` as well. If context reports only \`no_openspec_root\`, run \`openspec schemas --json\` from the current working directory instead. Do not use this fallback for invalid or unavailable stores.
|
|
46
46
|
|
|
47
47
|
Otherwise, omit \`--schema\` to preserve the configured default.
|
|
48
48
|
|
|
@@ -190,7 +190,7 @@ ${STORE_SELECTION_GUIDANCE}
|
|
|
190
190
|
|
|
191
191
|
**Use a different schema only if the user:**
|
|
192
192
|
- Explicitly requests a specific schema by name → use \`--schema <schema-name>\`
|
|
193
|
-
- Asks to "show workflows" or asks "what workflows" exist → resolve the authoritative root by running \`openspec context --json\` from the current working directory. If the user explicitly selected a registered store, use \`openspec context --json --store "<store-id>"\`. Then run \`openspec schemas --json\` with its working directory set to the returned \`root.path\` and let them choose. This preserves roots selected by a local \`store:\` pointer or the global \`defaultStore\`;
|
|
193
|
+
- Asks to "show workflows" or asks "what workflows" exist → resolve the authoritative root by running \`openspec context --json\` from the current working directory. If the user explicitly selected a registered store, use \`openspec context --json --store "<store-id>"\`. Then run \`openspec schemas --json\` with its working directory set to the returned \`root.path\` and let them choose. This preserves roots selected by a local \`store:\` pointer or the global \`defaultStore\`; when a registered store was explicitly selected, append \`--store "<store-id>"\` to \`openspec schemas --json\` as well. If context reports only \`no_openspec_root\`, run \`openspec schemas --json\` from the current working directory instead. Do not use this fallback for invalid or unavailable stores.
|
|
194
194
|
|
|
195
195
|
Otherwise, omit \`--schema\` to preserve the configured default.
|
|
196
196
|
|
|
@@ -4,5 +4,5 @@
|
|
|
4
4
|
* Interpolated into every workflow's instructions so generated skills
|
|
5
5
|
* consistently teach how to target a registered store with `--store <id>`.
|
|
6
6
|
*/
|
|
7
|
-
export declare const STORE_SELECTION_GUIDANCE = "**Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store <id>` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`, `view`). Once selected, treat `--store <id>` as sticky for the rest of the workflow. Every unscoped example of those commands below is shorthand: before running it, append the flag. For example, run `openspec status --change \"<name>\" --json --store \"<id>\"`, not the unscoped form shown below. Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root.";
|
|
7
|
+
export declare const STORE_SELECTION_GUIDANCE = "**Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store <id>` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`, `schemas`, `view`). Once selected, treat `--store <id>` as sticky for the rest of the workflow. Every unscoped example of those commands below is shorthand: before running it, append the flag. For example, run `openspec status --change \"<name>\" --json --store \"<id>\"`, not the unscoped form shown below. Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root.";
|
|
8
8
|
//# sourceMappingURL=store-selection.d.ts.map
|
|
@@ -4,5 +4,5 @@
|
|
|
4
4
|
* Interpolated into every workflow's instructions so generated skills
|
|
5
5
|
* consistently teach how to target a registered store with `--store <id>`.
|
|
6
6
|
*/
|
|
7
|
-
export const STORE_SELECTION_GUIDANCE = `**Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run \`openspec store list --json\` to discover registered store ids, then pass \`--store <id>\` on the commands that read or write specs and changes (\`new change\`, \`status\`, \`instructions\`, \`list\`, \`show\`, \`validate\`, \`archive\`, \`doctor\`, \`context\`, \`view\`). Once selected, treat \`--store <id>\` as sticky for the rest of the workflow. Every unscoped example of those commands below is shorthand: before running it, append the flag. For example, run \`openspec status --change "<name>" --json --store "<id>"\`, not the unscoped form shown below. Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local \`openspec/\` root.`;
|
|
7
|
+
export const STORE_SELECTION_GUIDANCE = `**Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run \`openspec store list --json\` to discover registered store ids, then pass \`--store <id>\` on the commands that read or write specs and changes (\`new change\`, \`status\`, \`instructions\`, \`list\`, \`show\`, \`validate\`, \`archive\`, \`doctor\`, \`context\`, \`schemas\`, \`view\`). Once selected, treat \`--store <id>\` as sticky for the rest of the workflow. Every unscoped example of those commands below is shorthand: before running it, append the flag. For example, run \`openspec status --change "<name>" --json --store "<id>"\`, not the unscoped form shown below. Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local \`openspec/\` root.`;
|
|
8
8
|
//# sourceMappingURL=store-selection.js.map
|
|
@@ -9,7 +9,7 @@ ${STORE_SELECTION_GUIDANCE}
|
|
|
9
9
|
|
|
10
10
|
**Input**: Optionally specify a change name. If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes.
|
|
11
11
|
|
|
12
|
-
\`/opsx:continue\` is an
|
|
12
|
+
\`/opsx:continue\` is an optional workflow and may not be installed. Before suggesting it anywhere below, verify that it is available. If it is unavailable, \`openspec status --change "<name>" --json\` shows the next artifact and \`openspec instructions "<artifact-id>" --change "<name>" --json\` explains how to create it.
|
|
13
13
|
|
|
14
14
|
**Steps**
|
|
15
15
|
|
|
@@ -81,7 +81,7 @@ After each invocation, show:
|
|
|
81
81
|
- Edit only the concrete files in \`existingOutputPaths\`; never write to a glob \`resolvedOutputPath\`.
|
|
82
82
|
- Do not advance the build frontier: no new artifacts, no new files under glob artifacts - that is \`/opsx:continue\`'s job.
|
|
83
83
|
- Confirm every edit with the user before writing.
|
|
84
|
-
- If the request changes the change's *intent* rather than refining it, first verify whether the
|
|
84
|
+
- If the request changes the change's *intent* rather than refining it, first verify whether the optional \`/opsx:new\` workflow is available. If it is, recommend starting fresh with \`/opsx:new\` (the "Update vs. Start Fresh" heuristic). If it is unavailable, ask for a distinct unused change name and recommend \`openspec new change "<new-change-name>"\` instead.`,
|
|
85
85
|
license: 'MIT',
|
|
86
86
|
compatibility: 'Requires openspec CLI.',
|
|
87
87
|
metadata: { author: 'openspec', version: '1.0' },
|
|
@@ -99,7 +99,7 @@ ${STORE_SELECTION_GUIDANCE}
|
|
|
99
99
|
|
|
100
100
|
**Input**: Optionally specify a change name after \`/opsx:update\` (e.g., \`/opsx:update add-auth\`). If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes.
|
|
101
101
|
|
|
102
|
-
\`/opsx:continue\` is an
|
|
102
|
+
\`/opsx:continue\` is an optional workflow and may not be installed. Before suggesting it anywhere below, verify that it is available. If it is unavailable, \`openspec status --change "<name>" --json\` shows the next artifact and \`openspec instructions "<artifact-id>" --change "<name>" --json\` explains how to create it.
|
|
103
103
|
|
|
104
104
|
**Steps**
|
|
105
105
|
|
|
@@ -171,7 +171,7 @@ After each invocation, show:
|
|
|
171
171
|
- Edit only the concrete files in \`existingOutputPaths\`; never write to a glob \`resolvedOutputPath\`.
|
|
172
172
|
- Do not advance the build frontier: no new artifacts, no new files under glob artifacts - that is \`/opsx:continue\`'s job.
|
|
173
173
|
- Confirm every edit with the user before writing.
|
|
174
|
-
- If the request changes the change's *intent* rather than refining it, first verify whether the
|
|
174
|
+
- If the request changes the change's *intent* rather than refining it, first verify whether the optional \`/opsx:new\` workflow is available. If it is, recommend starting fresh with \`/opsx:new\` (the "Update vs. Start Fresh" heuristic). If it is unavailable, ask for a distinct unused change name and recommend \`openspec new change "<new-change-name>"\` instead.`
|
|
175
175
|
};
|
|
176
176
|
}
|
|
177
177
|
//# sourceMappingURL=update-change.js.map
|
package/dist/core/update.js
CHANGED
|
@@ -14,7 +14,7 @@ import { getSkillReferenceTransformer, getTransformerForTool, transformToSkillRe
|
|
|
14
14
|
import { AI_TOOLS, OPENSPEC_DIR_NAME } from './config.js';
|
|
15
15
|
import { generateCommands, CommandAdapterRegistry, } from './command-generation/index.js';
|
|
16
16
|
import { getToolVersionStatus, getSkillTemplates, getCommandContents, generateSkillContent, getToolsWithSkillsDir, hasGlobalSkillTarget, resolveToolSkillsDir, toolSupportsSkills, } from './shared/index.js';
|
|
17
|
-
import { detectLegacyArtifacts, cleanupLegacyArtifacts, formatDeferredGlobalPromptSummary, formatCleanupSummary, formatDetectionSummary, getLegacyGlobalPromptMatches, getLegacyWorkflowIdsForTool, getToolsFromLegacyArtifacts, omitGlobalLegacyPromptFiles, pickGlobalLegacyPromptFiles, } from './legacy-cleanup.js';
|
|
17
|
+
import { detectLegacyArtifacts, cleanupLegacyArtifacts, formatDeferredGlobalPromptSummary, formatCleanupSummary, formatDetectionSummary, getLegacyGlobalPromptMatches, getLegacyWorkflowIdsForTool, getToolsFromLegacyArtifacts, omitGlobalLegacyPromptFiles, omitToolLegacyArtifacts, pickGlobalLegacyPromptFiles, } from './legacy-cleanup.js';
|
|
18
18
|
import { isInteractive } from '../utils/interactive.js';
|
|
19
19
|
import { getGlobalConfig } from './global-config.js';
|
|
20
20
|
import { getProfileWorkflows, ALL_WORKFLOWS, CORE_WORKFLOWS } from './profiles.js';
|
|
@@ -23,7 +23,7 @@ import { getAvailableTools } from './available-tools.js';
|
|
|
23
23
|
import { WORKFLOW_TO_SKILL_DIR, getConfiguredToolsForProfileSync, getToolsNeedingProfileSync, } from './profile-sync-drift.js';
|
|
24
24
|
import { scanInstalledWorkflows as scanInstalledWorkflowsShared, migrateIfNeeded as migrateIfNeededShared, findLegacyToolMigrations, migrateLegacyToolDirs, describeLegacyMigration, legacyMigrationNotice, keptInPlaceNotice, hasMovableContent, } from './migration.js';
|
|
25
25
|
import { resolveCommandSurfaceCapability, resolveCommandInvocation, shouldGenerateCommandsForTool, shouldGenerateSkillsForTool, shouldReconcileCommandFilesForTool, shouldRemoveSkillsForTool, } from './command-surface.js';
|
|
26
|
-
import { writeSharedSkillTarget } from './shared-skill-target.js';
|
|
26
|
+
import { writeSharedSkillTarget, sharedSkillRootOwner } from './shared-skill-target.js';
|
|
27
27
|
import { includesGitHubCopilot, writeCopilotCloudFiles, removeCopilotCloudFiles, isCopilotCloudEnabled, readCopilotCloudOptIn, findUnmanagedCloudFiles } from './github-copilot/cloud-agent.js';
|
|
28
28
|
const require = createRequire(import.meta.url);
|
|
29
29
|
const { version: OPENSPEC_VERSION } = require('../../package.json');
|
|
@@ -647,7 +647,7 @@ export class UpdateCommand {
|
|
|
647
647
|
const canPrompt = isInteractive();
|
|
648
648
|
if (this.force) {
|
|
649
649
|
const legacyUpgrade = await this.upgradeLegacyTools(projectPath, detection, canPrompt, desiredWorkflows, delivery);
|
|
650
|
-
await this.performImmediateLegacyCleanup(projectPath, detection);
|
|
650
|
+
await this.performImmediateLegacyCleanup(projectPath, detection, legacyUpgrade.skippedSharedSkillTools);
|
|
651
651
|
return {
|
|
652
652
|
...legacyUpgrade,
|
|
653
653
|
deferredGlobalCleanup: pickGlobalLegacyPromptFiles(detection, detection.globalSlashCommandFiles),
|
|
@@ -668,7 +668,7 @@ export class UpdateCommand {
|
|
|
668
668
|
});
|
|
669
669
|
if (shouldCleanup) {
|
|
670
670
|
const legacyUpgrade = await this.upgradeLegacyTools(projectPath, detection, canPrompt, desiredWorkflows, delivery);
|
|
671
|
-
await this.performImmediateLegacyCleanup(projectPath, detection);
|
|
671
|
+
await this.performImmediateLegacyCleanup(projectPath, detection, legacyUpgrade.skippedSharedSkillTools);
|
|
672
672
|
return {
|
|
673
673
|
...legacyUpgrade,
|
|
674
674
|
deferredGlobalCleanup: pickGlobalLegacyPromptFiles(detection, detection.globalSlashCommandFiles),
|
|
@@ -683,8 +683,10 @@ export class UpdateCommand {
|
|
|
683
683
|
/**
|
|
684
684
|
* Cleans approved repo-local legacy artifacts before configured tools refresh.
|
|
685
685
|
*/
|
|
686
|
-
async performImmediateLegacyCleanup(projectPath, detection) {
|
|
687
|
-
|
|
686
|
+
async performImmediateLegacyCleanup(projectPath, detection, skippedSharedSkillTools = []) {
|
|
687
|
+
// Tools whose upgrade was skipped (shared root owned by another) had no
|
|
688
|
+
// replacement written, so their repo-local legacy files must be preserved.
|
|
689
|
+
const immediateDetection = omitToolLegacyArtifacts(omitGlobalLegacyPromptFiles(detection), skippedSharedSkillTools);
|
|
688
690
|
if (immediateDetection.hasLegacyArtifacts) {
|
|
689
691
|
await this.performLegacyCleanup(projectPath, immediateDetection);
|
|
690
692
|
}
|
|
@@ -788,6 +790,7 @@ export class UpdateCommand {
|
|
|
788
790
|
const inferredCodexWorkflows = getLegacyWorkflowIdsForTool(detection, 'codex');
|
|
789
791
|
// Create skills/commands for selected tools using effective profile+delivery.
|
|
790
792
|
const newlyConfigured = [];
|
|
793
|
+
const skippedSharedSkillTools = [];
|
|
791
794
|
const workflowOverrides = {};
|
|
792
795
|
for (const toolId of selectedTools) {
|
|
793
796
|
const tool = AI_TOOLS.find((t) => t.value === toolId);
|
|
@@ -807,6 +810,30 @@ export class UpdateCommand {
|
|
|
807
810
|
}
|
|
808
811
|
const skillTemplates = getSkillTemplates(toolWorkflows);
|
|
809
812
|
const commandContents = getCommandContents(toolWorkflows);
|
|
813
|
+
// A shared skills root (e.g. `.agents`) already owned by another tool
|
|
814
|
+
// must not be overwritten by a tool inferred from legacy artifacts: a
|
|
815
|
+
// Codex install detected only from global `~/.codex/prompts` would
|
|
816
|
+
// otherwise rewrite an existing vendor-neutral `agents` tree with
|
|
817
|
+
// Codex-specific syntax and flip its ownership marker `agents → codex`.
|
|
818
|
+
// Leave the established owner in place. (init applies the same
|
|
819
|
+
// one-writer rule up front when both targets are selected.)
|
|
820
|
+
//
|
|
821
|
+
// Skipping here means the tool is never recorded as configured, so a
|
|
822
|
+
// persistent legacy signal re-offers it on later runs. Because no
|
|
823
|
+
// replacement is written, this tool is also exempted from immediate
|
|
824
|
+
// legacy cleanup (see skippedSharedSkillTools) — otherwise a repo-local
|
|
825
|
+
// `.codex/prompts` would be deleted with nothing put in its place. That
|
|
826
|
+
// repeat is idempotent and harmless — the alternative is the silent
|
|
827
|
+
// hijack this prevents.
|
|
828
|
+
const sharedOwner = shouldGenerateSkills
|
|
829
|
+
? sharedSkillRootOwner(projectPath, tool.value)
|
|
830
|
+
: undefined;
|
|
831
|
+
if (sharedOwner) {
|
|
832
|
+
const ownerName = AI_TOOLS.find((candidate) => candidate.value === sharedOwner)?.name ?? sharedOwner;
|
|
833
|
+
spinner.info(`Skipped ${tool.name}: ${tool.skillsDir}/skills is already managed by another tool (${ownerName}).`);
|
|
834
|
+
skippedSharedSkillTools.push(tool.value);
|
|
835
|
+
continue;
|
|
836
|
+
}
|
|
810
837
|
// Create skill files when delivery includes skills
|
|
811
838
|
if (shouldGenerateSkills) {
|
|
812
839
|
for (const { template, dirName } of skillTemplates) {
|
|
@@ -847,7 +874,7 @@ export class UpdateCommand {
|
|
|
847
874
|
if (newlyConfigured.length > 0) {
|
|
848
875
|
console.log();
|
|
849
876
|
}
|
|
850
|
-
return { newlyConfiguredTools: newlyConfigured, workflowOverrides };
|
|
877
|
+
return { newlyConfiguredTools: newlyConfigured, workflowOverrides, skippedSharedSkillTools };
|
|
851
878
|
}
|
|
852
879
|
}
|
|
853
880
|
//# sourceMappingURL=update.js.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface TaskNumberingDocument {
|
|
2
|
+
path: string;
|
|
3
|
+
content: string;
|
|
4
|
+
}
|
|
5
|
+
export interface TaskNumberingIssue {
|
|
6
|
+
path: string;
|
|
7
|
+
line: number;
|
|
8
|
+
message: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Finds ambiguous task references across the task files tracked by a change.
|
|
12
|
+
* Numbering is interpreted only inside `## N.` groups. Unnumbered sections,
|
|
13
|
+
* unnumbered tasks, and files without numbered groups are intentionally ignored.
|
|
14
|
+
*/
|
|
15
|
+
export declare function findTaskNumberingIssues(documents: readonly TaskNumberingDocument[]): TaskNumberingIssue[];
|
|
16
|
+
//# sourceMappingURL=task-numbering.d.ts.map
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { parseTaskLines } from '../../utils/task-progress.js';
|
|
2
|
+
const LEVEL_TWO_HEADING = /^ {0,3}##(?!#)(?:[ \t]+|[ \t]*\r?$)/;
|
|
3
|
+
const NUMBERED_GROUP_HEADING = /^ {0,3}##[ \t]+(\d+)\.(?:[ \t]|\r?$)/;
|
|
4
|
+
const TASK_ID = /^(\d+(?:\.\d+)+(?:[A-Za-z]+)?)(?=\s|$)/;
|
|
5
|
+
/**
|
|
6
|
+
* Finds ambiguous task references across the task files tracked by a change.
|
|
7
|
+
* Numbering is interpreted only inside `## N.` groups. Unnumbered sections,
|
|
8
|
+
* unnumbered tasks, and files without numbered groups are intentionally ignored.
|
|
9
|
+
*/
|
|
10
|
+
export function findTaskNumberingIssues(documents) {
|
|
11
|
+
const issues = [];
|
|
12
|
+
const firstLocationById = new Map();
|
|
13
|
+
for (const document of documents) {
|
|
14
|
+
const lines = document.content.split('\n');
|
|
15
|
+
if (!lines.some((line) => NUMBERED_GROUP_HEADING.test(line)))
|
|
16
|
+
continue;
|
|
17
|
+
let currentGroup;
|
|
18
|
+
lines.forEach((line, index) => {
|
|
19
|
+
if (LEVEL_TWO_HEADING.test(line)) {
|
|
20
|
+
currentGroup = line.match(NUMBERED_GROUP_HEADING)?.[1];
|
|
21
|
+
}
|
|
22
|
+
if (currentGroup === undefined)
|
|
23
|
+
return;
|
|
24
|
+
const task = parseTaskLines(line)[0];
|
|
25
|
+
const id = task?.description.match(TASK_ID)?.[1];
|
|
26
|
+
if (!id)
|
|
27
|
+
return;
|
|
28
|
+
const lineNumber = index + 1;
|
|
29
|
+
const taskGroup = id.split('.')[0];
|
|
30
|
+
const normalizedTaskGroup = taskGroup.replace(/^0+(?=\d)/, '');
|
|
31
|
+
const normalizedCurrentGroup = currentGroup.replace(/^0+(?=\d)/, '');
|
|
32
|
+
if (normalizedTaskGroup !== normalizedCurrentGroup) {
|
|
33
|
+
issues.push({
|
|
34
|
+
path: document.path,
|
|
35
|
+
line: lineNumber,
|
|
36
|
+
message: `Task "${id}" is under group ${currentGroup}, but its leading number points to group ${taskGroup}. Move it to group ${taskGroup} or renumber it.`,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
const firstLocation = firstLocationById.get(id);
|
|
40
|
+
if (firstLocation !== undefined) {
|
|
41
|
+
const firstDeclaration = firstLocation.path === document.path
|
|
42
|
+
? `on line ${firstLocation.line}`
|
|
43
|
+
: `in ${firstLocation.path} on line ${firstLocation.line}`;
|
|
44
|
+
issues.push({
|
|
45
|
+
path: document.path,
|
|
46
|
+
line: lineNumber,
|
|
47
|
+
message: `Task ID "${id}" is duplicated; it was first declared ${firstDeclaration}.`,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
firstLocationById.set(id, { path: document.path, line: lineNumber });
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
return issues;
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=task-numbering.js.map
|
|
@@ -20,12 +20,15 @@ export declare class Validator {
|
|
|
20
20
|
*
|
|
21
21
|
* When `options.mainSpecsDir` is given, MODIFIED blocks are also checked
|
|
22
22
|
* against the current main specs for the scenario loss archive refuses to
|
|
23
|
-
* apply (#1477).
|
|
24
|
-
*
|
|
23
|
+
* apply (#1477). When `options.projectRoot` is given, the schema's tracked
|
|
24
|
+
* task files are checked for ambiguous numbering (#1520). Omitting either
|
|
25
|
+
* option keeps existing library and archive callers behaving as before.
|
|
25
26
|
*/
|
|
26
27
|
validateChangeDeltaSpecs(changeDir: string, options?: {
|
|
27
28
|
mainSpecsDir?: string;
|
|
29
|
+
projectRoot?: string;
|
|
28
30
|
}): Promise<ValidationReport>;
|
|
31
|
+
private collectTaskNumberingIssues;
|
|
29
32
|
/**
|
|
30
33
|
* Report MODIFIED requirements whose block omits a scenario the main spec
|
|
31
34
|
* still carries. Uses the same comparison archive applies, so validate can
|
|
@@ -9,7 +9,10 @@ import { extractRequirementBody as extractRequirementBodyShared, containsShallOr
|
|
|
9
9
|
import { findMainSpecStructureIssues } from '../parsers/spec-structure.js';
|
|
10
10
|
import { FileSystemUtils } from '../../utils/file-system.js';
|
|
11
11
|
import { discoverSpecFiles, hasAnyFileUnder } from '../../utils/spec-discovery.js';
|
|
12
|
-
import { METADATA_FILENAME, readSkipSpecsMarker } from '../../utils/change-metadata.js';
|
|
12
|
+
import { METADATA_FILENAME, readSkipSpecsMarker, resolveSchemaForChange, } from '../../utils/change-metadata.js';
|
|
13
|
+
import { resolveTaskFilesForChange } from '../../utils/task-progress.js';
|
|
14
|
+
import { findTaskNumberingIssues } from './task-numbering.js';
|
|
15
|
+
import { getPackageSchemasDir, getSchemaDir } from '../artifact-graph/index.js';
|
|
13
16
|
export class Validator {
|
|
14
17
|
strictMode;
|
|
15
18
|
constructor(strictMode = false) {
|
|
@@ -109,8 +112,9 @@ export class Validator {
|
|
|
109
112
|
*
|
|
110
113
|
* When `options.mainSpecsDir` is given, MODIFIED blocks are also checked
|
|
111
114
|
* against the current main specs for the scenario loss archive refuses to
|
|
112
|
-
* apply (#1477).
|
|
113
|
-
*
|
|
115
|
+
* apply (#1477). When `options.projectRoot` is given, the schema's tracked
|
|
116
|
+
* task files are checked for ambiguous numbering (#1520). Omitting either
|
|
117
|
+
* option keeps existing library and archive callers behaving as before.
|
|
114
118
|
*/
|
|
115
119
|
async validateChangeDeltaSpecs(changeDir, options = {}) {
|
|
116
120
|
const issues = [];
|
|
@@ -387,8 +391,58 @@ export class Validator {
|
|
|
387
391
|
issues.push({ level: 'ERROR', path: 'file', message: this.enrichTopLevelError('change', VALIDATION_MESSAGES.CHANGE_NO_DELTAS) });
|
|
388
392
|
}
|
|
389
393
|
}
|
|
394
|
+
if (options.projectRoot) {
|
|
395
|
+
issues.push(...await this.collectTaskNumberingIssues(changeDir, options.projectRoot));
|
|
396
|
+
}
|
|
390
397
|
return this.createReport(issues);
|
|
391
398
|
}
|
|
399
|
+
async collectTaskNumberingIssues(changeDir, projectRoot) {
|
|
400
|
+
try {
|
|
401
|
+
const schemaName = resolveSchemaForChange(changeDir, undefined, projectRoot).replace(/\.ya?ml$/, '');
|
|
402
|
+
const schemaDir = getSchemaDir(schemaName, projectRoot);
|
|
403
|
+
const builtInSchemaDir = path.join(getPackageSchemasDir(), 'spec-driven');
|
|
404
|
+
if (schemaName !== 'spec-driven' ||
|
|
405
|
+
schemaDir === null ||
|
|
406
|
+
FileSystemUtils.canonicalizeExistingPath(schemaDir) !==
|
|
407
|
+
FileSystemUtils.canonicalizeExistingPath(builtInSchemaDir)) {
|
|
408
|
+
return [];
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
catch {
|
|
412
|
+
return [];
|
|
413
|
+
}
|
|
414
|
+
let taskFiles;
|
|
415
|
+
try {
|
|
416
|
+
taskFiles = resolveTaskFilesForChange(changeDir, projectRoot);
|
|
417
|
+
}
|
|
418
|
+
catch {
|
|
419
|
+
return [];
|
|
420
|
+
}
|
|
421
|
+
if (taskFiles.length === 0) {
|
|
422
|
+
taskFiles = [path.join(changeDir, 'tasks.md')];
|
|
423
|
+
}
|
|
424
|
+
const documents = [];
|
|
425
|
+
for (const taskFile of taskFiles) {
|
|
426
|
+
let content;
|
|
427
|
+
try {
|
|
428
|
+
content = await fs.readFile(taskFile, 'utf-8');
|
|
429
|
+
}
|
|
430
|
+
catch {
|
|
431
|
+
continue;
|
|
432
|
+
}
|
|
433
|
+
documents.push({
|
|
434
|
+
path: FileSystemUtils.toPosixPath(path.relative(changeDir, taskFile)),
|
|
435
|
+
content,
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
documents.sort((left, right) => left.path.localeCompare(right.path));
|
|
439
|
+
return findTaskNumberingIssues(documents).map((issue) => ({
|
|
440
|
+
level: 'WARNING',
|
|
441
|
+
path: issue.path,
|
|
442
|
+
line: issue.line,
|
|
443
|
+
message: issue.message,
|
|
444
|
+
}));
|
|
445
|
+
}
|
|
392
446
|
/**
|
|
393
447
|
* Report MODIFIED requirements whose block omits a scenario the main spec
|
|
394
448
|
* still carries. Uses the same comparison archive applies, so validate can
|
|
@@ -27,7 +27,9 @@ export declare function trackCommand(commandName: string, version: string): Prom
|
|
|
27
27
|
/**
|
|
28
28
|
* Show first-run telemetry notice if not already seen.
|
|
29
29
|
*/
|
|
30
|
-
export declare function maybeShowTelemetryNotice(
|
|
30
|
+
export declare function maybeShowTelemetryNotice(options?: {
|
|
31
|
+
silent?: boolean;
|
|
32
|
+
}): Promise<void>;
|
|
31
33
|
/**
|
|
32
34
|
* Flush pending telemetry events.
|
|
33
35
|
* Call this before CLI exit.
|
package/dist/telemetry/index.js
CHANGED
|
@@ -159,7 +159,7 @@ export async function trackCommand(commandName, version) {
|
|
|
159
159
|
/**
|
|
160
160
|
* Show first-run telemetry notice if not already seen.
|
|
161
161
|
*/
|
|
162
|
-
export async function maybeShowTelemetryNotice() {
|
|
162
|
+
export async function maybeShowTelemetryNotice(options = {}) {
|
|
163
163
|
if (!isTelemetryEnabled()) {
|
|
164
164
|
return;
|
|
165
165
|
}
|
|
@@ -168,6 +168,12 @@ export async function maybeShowTelemetryNotice() {
|
|
|
168
168
|
if (config.noticeSeen) {
|
|
169
169
|
return;
|
|
170
170
|
}
|
|
171
|
+
// In --json mode the notice would pollute stdout and break parsers, so
|
|
172
|
+
// defer it: skip the notice AND leave noticeSeen unset so the disclosure
|
|
173
|
+
// still appears on the user's first later non-JSON run.
|
|
174
|
+
if (options.silent) {
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
171
177
|
// Display notice
|
|
172
178
|
console.log('Note: OpenSpec collects anonymous usage stats. Opt out: OPENSPEC_TELEMETRY=0 or openspec config set telemetry.enabled false');
|
|
173
179
|
// Mark as seen
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Readable, Writable } from 'node:stream';
|
|
1
2
|
export type InteractiveOptions = {
|
|
2
3
|
/**
|
|
3
4
|
* Explicit "disable prompts" flag passed by internal callers.
|
|
@@ -35,7 +36,35 @@ export declare function isInteractive(value?: boolean | InteractiveOptions): boo
|
|
|
35
36
|
* somebody was there and chose to quit.
|
|
36
37
|
*
|
|
37
38
|
* Beyond that it defers to `isInteractive()`, so `CI`, `OPEN_SPEC_INTERACTIVE=0`
|
|
38
|
-
* and `--no-interactive` count even when a runner allocated a pty.
|
|
39
|
+
* and `--no-interactive` count even when a runner allocated a pty. It also
|
|
40
|
+
* counts a redirected stdout: `confirmPrompt` drops to the plain reader whenever
|
|
41
|
+
* *either* stream is not a TTY, so a stdin-TTY-but-stdout-redirected run
|
|
42
|
+
* (`openspec archive x > log.txt` from a terminal) that hits EOF must classify
|
|
43
|
+
* the same way the prompt was selected — otherwise it would leak the raw
|
|
44
|
+
* `ExitPromptError` instead of the `--yes` guidance.
|
|
39
45
|
*/
|
|
40
46
|
export declare function isNonInteractivePromptError(error: unknown, value?: boolean | InteractiveOptions): boolean;
|
|
47
|
+
export type ConfirmPrompt = {
|
|
48
|
+
message: string;
|
|
49
|
+
default: boolean;
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* Ask a yes/no question. A real terminal gets @inquirer's rich prompt;
|
|
53
|
+
* everything else — a pipe, a file redirect, an agent that captures stdout —
|
|
54
|
+
* reads one plain line instead.
|
|
55
|
+
*
|
|
56
|
+
* @inquirer renders `confirm` by writing ANSI cursor-movement escape sequences,
|
|
57
|
+
* and it emits them even when stdout is not a TTY. Redirected to a file those
|
|
58
|
+
* sequences are noise, and in some non-TTY hosts the render loop never settles
|
|
59
|
+
* and repeats `ESC[NNG` cursor moves until the disk fills (#1526). Reading
|
|
60
|
+
* the answer ourselves keeps the single piped answer @inquirer ever supported
|
|
61
|
+
* working (`printf 'y\n' | openspec archive ...`) without emitting any escapes.
|
|
62
|
+
*
|
|
63
|
+
* `io` overrides the streams; it exists for tests and mirrors @inquirer's own
|
|
64
|
+
* `{ input, output }` context. Production callers pass only the prompt.
|
|
65
|
+
*/
|
|
66
|
+
export declare function confirmPrompt(prompt: ConfirmPrompt, io?: {
|
|
67
|
+
input?: Readable;
|
|
68
|
+
output?: Writable;
|
|
69
|
+
}): Promise<boolean>;
|
|
41
70
|
//# sourceMappingURL=interactive.d.ts.map
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createInterface } from 'node:readline';
|
|
1
2
|
/**
|
|
2
3
|
* Resolves whether non-interactive mode is requested.
|
|
3
4
|
* Handles both explicit `noInteractive: true` and Commander.js style `interactive: false`.
|
|
@@ -38,7 +39,12 @@ export function isInteractive(value) {
|
|
|
38
39
|
* somebody was there and chose to quit.
|
|
39
40
|
*
|
|
40
41
|
* Beyond that it defers to `isInteractive()`, so `CI`, `OPEN_SPEC_INTERACTIVE=0`
|
|
41
|
-
* and `--no-interactive` count even when a runner allocated a pty.
|
|
42
|
+
* and `--no-interactive` count even when a runner allocated a pty. It also
|
|
43
|
+
* counts a redirected stdout: `confirmPrompt` drops to the plain reader whenever
|
|
44
|
+
* *either* stream is not a TTY, so a stdin-TTY-but-stdout-redirected run
|
|
45
|
+
* (`openspec archive x > log.txt` from a terminal) that hits EOF must classify
|
|
46
|
+
* the same way the prompt was selected — otherwise it would leak the raw
|
|
47
|
+
* `ExitPromptError` instead of the `--yes` guidance.
|
|
42
48
|
*/
|
|
43
49
|
export function isNonInteractivePromptError(error, value) {
|
|
44
50
|
if (!(error instanceof Error))
|
|
@@ -48,6 +54,103 @@ export function isNonInteractivePromptError(error, value) {
|
|
|
48
54
|
return false;
|
|
49
55
|
if (error.message.includes('SIGINT'))
|
|
50
56
|
return false;
|
|
51
|
-
return !isInteractive(value);
|
|
57
|
+
return !isInteractive(value) || !process.stdout.isTTY;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Ask a yes/no question. A real terminal gets @inquirer's rich prompt;
|
|
61
|
+
* everything else — a pipe, a file redirect, an agent that captures stdout —
|
|
62
|
+
* reads one plain line instead.
|
|
63
|
+
*
|
|
64
|
+
* @inquirer renders `confirm` by writing ANSI cursor-movement escape sequences,
|
|
65
|
+
* and it emits them even when stdout is not a TTY. Redirected to a file those
|
|
66
|
+
* sequences are noise, and in some non-TTY hosts the render loop never settles
|
|
67
|
+
* and repeats `ESC[NNG` cursor moves until the disk fills (#1526). Reading
|
|
68
|
+
* the answer ourselves keeps the single piped answer @inquirer ever supported
|
|
69
|
+
* working (`printf 'y\n' | openspec archive ...`) without emitting any escapes.
|
|
70
|
+
*
|
|
71
|
+
* `io` overrides the streams; it exists for tests and mirrors @inquirer's own
|
|
72
|
+
* `{ input, output }` context. Production callers pass only the prompt.
|
|
73
|
+
*/
|
|
74
|
+
export async function confirmPrompt(prompt, io = {}) {
|
|
75
|
+
const input = io.input ?? process.stdin;
|
|
76
|
+
const output = io.output ?? process.stdout;
|
|
77
|
+
const isTerminal = Boolean(input.isTTY) &&
|
|
78
|
+
Boolean(output.isTTY);
|
|
79
|
+
if (isTerminal) {
|
|
80
|
+
const { confirm } = await import('@inquirer/prompts');
|
|
81
|
+
return confirm(prompt);
|
|
82
|
+
}
|
|
83
|
+
return readYesNo(prompt, input, output);
|
|
84
|
+
}
|
|
85
|
+
function readYesNo(prompt, input, output) {
|
|
86
|
+
return new Promise((resolve, reject) => {
|
|
87
|
+
let settled = false;
|
|
88
|
+
// Detach the input-stream error listener on settle: `input` is the
|
|
89
|
+
// long-lived process.stdin, so a leftover listener would accumulate across
|
|
90
|
+
// archive's successive prompts and swallow a later, unrelated stdin error.
|
|
91
|
+
const cleanup = () => {
|
|
92
|
+
input.removeListener('error', onError);
|
|
93
|
+
};
|
|
94
|
+
const blockOnNoAnswer = () => {
|
|
95
|
+
if (settled)
|
|
96
|
+
return;
|
|
97
|
+
settled = true;
|
|
98
|
+
cleanup();
|
|
99
|
+
// No line could be read (stdin closed / EOF). Mirror @inquirer's failure
|
|
100
|
+
// so callers that classify it — isNonInteractivePromptError, the #1479
|
|
101
|
+
// "rerun with --yes" guidance — keep working unchanged.
|
|
102
|
+
const error = new Error('User force closed the prompt');
|
|
103
|
+
error.name = 'ExitPromptError';
|
|
104
|
+
reject(error);
|
|
105
|
+
};
|
|
106
|
+
// A stdin error surfaces on the interface (readline forwards input-stream
|
|
107
|
+
// errors since Node 16). Without a handler the promise would hang and the
|
|
108
|
+
// 'error' would go unhandled; settle it with the real fault instead.
|
|
109
|
+
const onError = (err) => {
|
|
110
|
+
if (settled)
|
|
111
|
+
return;
|
|
112
|
+
settled = true;
|
|
113
|
+
cleanup();
|
|
114
|
+
rl.close();
|
|
115
|
+
reject(err instanceof Error ? err : new Error(String(err)));
|
|
116
|
+
};
|
|
117
|
+
// An earlier prompt may have already drained stdin (only one piped answer
|
|
118
|
+
// was ever supported). A fresh readline over an ended stream never emits
|
|
119
|
+
// 'close', so guard here rather than hang and exit as a no-op.
|
|
120
|
+
if (input.readableEnded) {
|
|
121
|
+
blockOnNoAnswer();
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
output.write(`${prompt.message} ${prompt.default ? '(Y/n)' : '(y/N)'} `);
|
|
125
|
+
// terminal:false guarantees readline never emits its own line-editing
|
|
126
|
+
// escapes — an escape-free read is the whole point here.
|
|
127
|
+
const rl = createInterface({ input, terminal: false });
|
|
128
|
+
input.once('error', onError);
|
|
129
|
+
rl.once('error', onError);
|
|
130
|
+
rl.once('line', (line) => {
|
|
131
|
+
if (settled)
|
|
132
|
+
return;
|
|
133
|
+
settled = true;
|
|
134
|
+
cleanup();
|
|
135
|
+
rl.close();
|
|
136
|
+
output.write('\n');
|
|
137
|
+
// Mirror @inquirer/confirm's parser (prefix match on y/yes and n/no,
|
|
138
|
+
// otherwise the default) so a piped answer resolves identically to the
|
|
139
|
+
// interactive prompt it replaces.
|
|
140
|
+
const answer = line.trim();
|
|
141
|
+
if (/^(y|yes)/i.test(answer)) {
|
|
142
|
+
resolve(true);
|
|
143
|
+
}
|
|
144
|
+
else if (/^(n|no)/i.test(answer)) {
|
|
145
|
+
resolve(false);
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
resolve(prompt.default);
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
rl.once('close', () => {
|
|
152
|
+
blockOnNoAnswer();
|
|
153
|
+
});
|
|
154
|
+
});
|
|
52
155
|
}
|
|
53
156
|
//# sourceMappingURL=interactive.js.map
|
|
@@ -20,6 +20,27 @@ export interface TaskProgress {
|
|
|
20
20
|
completed: number;
|
|
21
21
|
}
|
|
22
22
|
export declare function countTasksFromContent(content: string): TaskProgress;
|
|
23
|
+
/**
|
|
24
|
+
* Run-scoped memo mapping a schema name to its tracked-tasks `generates` glob.
|
|
25
|
+
* When one command resolves progress for many changes under a constant
|
|
26
|
+
* `projectRoot` — e.g. `validate --archived` over an append-only archive — this
|
|
27
|
+
* avoids re-reading and re-parsing (YAML + Zod) the same `schema.yaml` once per
|
|
28
|
+
* change. Keyed by schema name alone, which is safe *only* because a single run
|
|
29
|
+
* holds `projectRoot` constant; never reuse one cache across differing roots.
|
|
30
|
+
*/
|
|
31
|
+
export type SchemaGlobCache = Map<string, string | undefined>;
|
|
32
|
+
/** Resolves the task files selected by the schema's apply tracking rule. */
|
|
33
|
+
export declare function resolveTaskFilesForChange(changeDir: string, projectRoot: string, schemaGlobCache?: SchemaGlobCache): string[];
|
|
34
|
+
export interface TaskProgressDetail extends TaskProgress {
|
|
35
|
+
/**
|
|
36
|
+
* Task files that exist but could not be read (any error other than ENOENT).
|
|
37
|
+
* `getTaskProgressForChange` discards this list to preserve its behavior;
|
|
38
|
+
* callers that must fail loudly on an unreadable tasks file — e.g.
|
|
39
|
+
* `openspec validate --archived` — read it so an unreadable file is never
|
|
40
|
+
* silently counted as "no tasks" (#205).
|
|
41
|
+
*/
|
|
42
|
+
unreadable: string[];
|
|
43
|
+
}
|
|
23
44
|
/**
|
|
24
45
|
* Computes a change's task progress by resolving its tracked-tasks artifact and
|
|
25
46
|
* counting checkboxes across every file matched by that artifact's `generates`
|
|
@@ -27,7 +48,19 @@ export declare function countTasksFromContent(content: string): TaskProgress;
|
|
|
27
48
|
* artifact (`resolveArtifactOutputs`) — so progress is no longer blind to nested
|
|
28
49
|
* `tasks.md` files (#1202). Falls back to a single top-level `tasks.md` (exactly
|
|
29
50
|
* as before) when the schema is unresolvable, no tracked-tasks artifact is found,
|
|
30
|
-
* or the glob matches no file.
|
|
51
|
+
* or the glob matches no file. Also reports task files that exist but could not
|
|
52
|
+
* be read. Per-file read errors are captured (never thrown); the only throw path
|
|
53
|
+
* is a malformed/unsafe schema whose glob resolution rejects (path traversal or
|
|
54
|
+
* a linked-directory cycle in `resolveArtifactOutputs`). Pass `schemaGlobCache`
|
|
55
|
+
* to memoize schema→glob resolution across many changes in one run.
|
|
56
|
+
*/
|
|
57
|
+
export declare function getTaskProgressDetailForChange(changesDir: string, changeName: string, projectRoot: string, schemaGlobCache?: SchemaGlobCache): Promise<TaskProgressDetail>;
|
|
58
|
+
/**
|
|
59
|
+
* The task-completion counter `status`, `list`, and `archive` share. Delegates
|
|
60
|
+
* to `getTaskProgressDetailForChange` and drops the `unreadable` detail, so its
|
|
61
|
+
* returned totals are unchanged. Throws only on the same malformed/unsafe-schema
|
|
62
|
+
* glob-resolution path as that function (existing behavior; callers guard it as
|
|
63
|
+
* they did before).
|
|
31
64
|
*/
|
|
32
65
|
export declare function getTaskProgressForChange(changesDir: string, changeName: string, projectRoot: string): Promise<TaskProgress>;
|
|
33
66
|
export declare function formatTaskStatus(progress: TaskProgress): string;
|