@seanmars/tospec 0.23.0 → 0.26.0-beta
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/CHANGELOG.md +281 -0
- package/README.md +27 -9
- package/assets/claude-mod/tospec-dashboard/.claude-plugin/plugin.json +7 -0
- package/assets/claude-mod/tospec-dashboard/hooks/hooks.json +1 -0
- package/assets/claude-mod/tospec-dashboard/hooks/register.ts +695 -0
- package/assets/claude-mod/tospec-dashboard/tests/register.test.ts +448 -0
- package/assets/claude-mod/tospec-dashboard/types/index.d.ts +8 -0
- package/assets/dashboard/app.js +218 -1180
- package/assets/dashboard/index.html +31 -22
- package/assets/dashboard/lib/charts.js +355 -0
- package/assets/dashboard/lib/derive.js +375 -0
- package/assets/dashboard/lib/dom.js +121 -0
- package/assets/dashboard/lib/routes.js +52 -0
- package/assets/dashboard/lib/store.js +326 -0
- package/assets/dashboard/style.css +3103 -664
- package/assets/dashboard/views/alerts.js +170 -0
- package/assets/dashboard/views/change.js +234 -0
- package/assets/dashboard/views/docs.js +214 -0
- package/assets/dashboard/views/home.js +357 -0
- package/assets/dashboard/views/project.js +168 -0
- package/assets/dashboard/views/session.js +270 -0
- package/assets/dashboard/views/sidebar.js +131 -0
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +116 -102
- package/dist/cli/index.js.map +1 -1
- package/dist/commands/config.d.ts.map +1 -1
- package/dist/commands/config.js +213 -100
- package/dist/commands/config.js.map +1 -1
- package/dist/commands/dashboard-relay.d.ts +11 -0
- package/dist/commands/dashboard-relay.d.ts.map +1 -0
- package/dist/commands/dashboard-relay.js +88 -0
- package/dist/commands/dashboard-relay.js.map +1 -0
- package/dist/commands/dashboard.d.ts +69 -100
- package/dist/commands/dashboard.d.ts.map +1 -1
- package/dist/commands/dashboard.js +420 -598
- package/dist/commands/dashboard.js.map +1 -1
- package/dist/commands/decision.d.ts +38 -1
- package/dist/commands/decision.d.ts.map +1 -1
- package/dist/commands/decision.js +303 -27
- package/dist/commands/decision.js.map +1 -1
- package/dist/commands/metrics.d.ts +3 -4
- package/dist/commands/metrics.d.ts.map +1 -1
- package/dist/commands/metrics.js +3 -5
- package/dist/commands/metrics.js.map +1 -1
- package/dist/commands/shared-output.d.ts +24 -0
- package/dist/commands/shared-output.d.ts.map +1 -1
- package/dist/commands/shared-output.js +21 -2
- package/dist/commands/shared-output.js.map +1 -1
- package/dist/commands/show.d.ts.map +1 -1
- package/dist/commands/show.js +12 -9
- package/dist/commands/show.js.map +1 -1
- package/dist/commands/validate.d.ts +19 -0
- package/dist/commands/validate.d.ts.map +1 -1
- package/dist/commands/validate.js +243 -64
- package/dist/commands/validate.js.map +1 -1
- package/dist/commands/workflow/instructions.d.ts +7 -1
- package/dist/commands/workflow/instructions.d.ts.map +1 -1
- package/dist/commands/workflow/instructions.js +299 -81
- package/dist/commands/workflow/instructions.js.map +1 -1
- package/dist/commands/workflow/new-change.d.ts.map +1 -1
- package/dist/commands/workflow/new-change.js +26 -17
- package/dist/commands/workflow/new-change.js.map +1 -1
- package/dist/commands/workflow/prompt-escape.d.ts +25 -0
- package/dist/commands/workflow/prompt-escape.d.ts.map +1 -0
- package/dist/commands/workflow/prompt-escape.js +62 -0
- package/dist/commands/workflow/prompt-escape.js.map +1 -0
- package/dist/commands/workflow/schemas.d.ts.map +1 -1
- package/dist/commands/workflow/schemas.js +34 -16
- package/dist/commands/workflow/schemas.js.map +1 -1
- package/dist/commands/workflow/shared.d.ts +46 -10
- package/dist/commands/workflow/shared.d.ts.map +1 -1
- package/dist/commands/workflow/shared.js +66 -30
- package/dist/commands/workflow/shared.js.map +1 -1
- package/dist/commands/workflow/status.d.ts +5 -2
- package/dist/commands/workflow/status.d.ts.map +1 -1
- package/dist/commands/workflow/status.js +62 -28
- package/dist/commands/workflow/status.js.map +1 -1
- package/dist/core/archive.d.ts +38 -0
- package/dist/core/archive.d.ts.map +1 -1
- package/dist/core/archive.js +492 -75
- package/dist/core/archive.js.map +1 -1
- package/dist/core/artifact-graph/index.d.ts +2 -2
- package/dist/core/artifact-graph/index.d.ts.map +1 -1
- package/dist/core/artifact-graph/index.js +2 -2
- package/dist/core/artifact-graph/index.js.map +1 -1
- package/dist/core/artifact-graph/instruction-loader.d.ts +8 -0
- package/dist/core/artifact-graph/instruction-loader.d.ts.map +1 -1
- package/dist/core/artifact-graph/instruction-loader.js +58 -36
- package/dist/core/artifact-graph/instruction-loader.js.map +1 -1
- package/dist/core/artifact-graph/resolver.d.ts +39 -12
- package/dist/core/artifact-graph/resolver.d.ts.map +1 -1
- package/dist/core/artifact-graph/resolver.js +82 -36
- package/dist/core/artifact-graph/resolver.js.map +1 -1
- package/dist/core/artifact-graph/schema.d.ts +24 -0
- package/dist/core/artifact-graph/schema.d.ts.map +1 -1
- package/dist/core/artifact-graph/schema.js +32 -1
- package/dist/core/artifact-graph/schema.js.map +1 -1
- package/dist/core/artifact-graph/stub-detection.d.ts +10 -2
- package/dist/core/artifact-graph/stub-detection.d.ts.map +1 -1
- package/dist/core/artifact-graph/stub-detection.js +18 -11
- package/dist/core/artifact-graph/stub-detection.js.map +1 -1
- package/dist/core/artifact-graph/types.d.ts +2 -2
- package/dist/core/artifact-graph/types.d.ts.map +1 -1
- package/dist/core/artifact-graph/types.js +10 -1
- package/dist/core/artifact-graph/types.js.map +1 -1
- package/dist/core/change-metadata/schema.d.ts.map +1 -1
- package/dist/core/change-metadata/schema.js +18 -4
- package/dist/core/change-metadata/schema.js.map +1 -1
- package/dist/core/change-presenter.d.ts.map +1 -1
- package/dist/core/change-presenter.js +13 -8
- package/dist/core/change-presenter.js.map +1 -1
- package/dist/core/change-status-policy.d.ts.map +1 -1
- package/dist/core/change-status-policy.js +3 -1
- package/dist/core/change-status-policy.js.map +1 -1
- package/dist/core/codex-metrics.d.ts +1 -1
- package/dist/core/codex-metrics.js +1 -1
- package/dist/core/config-schema.d.ts +8 -0
- package/dist/core/config-schema.d.ts.map +1 -1
- package/dist/core/config-schema.js +10 -0
- package/dist/core/config-schema.js.map +1 -1
- package/dist/core/dashboard-activity.d.ts +4 -4
- package/dist/core/dashboard-activity.js +4 -4
- package/dist/core/dashboard-data.d.ts +11 -5
- package/dist/core/dashboard-data.d.ts.map +1 -1
- package/dist/core/dashboard-data.js +54 -28
- package/dist/core/dashboard-data.js.map +1 -1
- package/dist/core/dashboard-hub.d.ts +258 -0
- package/dist/core/dashboard-hub.d.ts.map +1 -0
- package/dist/core/dashboard-hub.js +655 -0
- package/dist/core/dashboard-hub.js.map +1 -0
- package/dist/core/dashboard-routes.d.ts +57 -0
- package/dist/core/dashboard-routes.d.ts.map +1 -0
- package/dist/core/dashboard-routes.js +398 -0
- package/dist/core/dashboard-routes.js.map +1 -0
- package/dist/core/global-config.d.ts +71 -2
- package/dist/core/global-config.d.ts.map +1 -1
- package/dist/core/global-config.js +149 -4
- package/dist/core/global-config.js.map +1 -1
- package/dist/core/id.d.ts +9 -0
- package/dist/core/id.d.ts.map +1 -1
- package/dist/core/id.js +37 -0
- package/dist/core/id.js.map +1 -1
- package/dist/core/init.d.ts +9 -2
- package/dist/core/init.d.ts.map +1 -1
- package/dist/core/init.js +119 -33
- package/dist/core/init.js.map +1 -1
- package/dist/core/list.d.ts.map +1 -1
- package/dist/core/list.js +69 -45
- package/dist/core/list.js.map +1 -1
- package/dist/core/local-server.d.ts +10 -3
- package/dist/core/local-server.d.ts.map +1 -1
- package/dist/core/local-server.js +25 -5
- package/dist/core/local-server.js.map +1 -1
- package/dist/core/markdown-render.d.ts.map +1 -1
- package/dist/core/markdown-render.js +7 -2
- package/dist/core/markdown-render.js.map +1 -1
- package/dist/core/migrate.d.ts +24 -2
- package/dist/core/migrate.d.ts.map +1 -1
- package/dist/core/migrate.js +335 -59
- package/dist/core/migrate.js.map +1 -1
- package/dist/core/parsers/change-parser.d.ts +20 -1
- package/dist/core/parsers/change-parser.d.ts.map +1 -1
- package/dist/core/parsers/change-parser.js +76 -73
- package/dist/core/parsers/change-parser.js.map +1 -1
- package/dist/core/parsers/markdown-parser.d.ts +24 -0
- package/dist/core/parsers/markdown-parser.d.ts.map +1 -1
- package/dist/core/parsers/markdown-parser.js +65 -10
- package/dist/core/parsers/markdown-parser.js.map +1 -1
- package/dist/core/parsers/requirement-blocks.d.ts +127 -6
- package/dist/core/parsers/requirement-blocks.d.ts.map +1 -1
- package/dist/core/parsers/requirement-blocks.js +349 -56
- package/dist/core/parsers/requirement-blocks.js.map +1 -1
- package/dist/core/parsers/requirement-text.d.ts +25 -1
- package/dist/core/parsers/requirement-text.d.ts.map +1 -1
- package/dist/core/parsers/requirement-text.js +62 -6
- package/dist/core/parsers/requirement-text.js.map +1 -1
- package/dist/core/parsers/spec-structure.d.ts.map +1 -1
- package/dist/core/parsers/spec-structure.js +36 -24
- package/dist/core/parsers/spec-structure.js.map +1 -1
- package/dist/core/project-config.d.ts +9 -2
- package/dist/core/project-config.d.ts.map +1 -1
- package/dist/core/project-config.js +30 -7
- package/dist/core/project-config.js.map +1 -1
- package/dist/core/project-layout.d.ts +26 -0
- package/dist/core/project-layout.d.ts.map +1 -1
- package/dist/core/project-layout.js +51 -1
- package/dist/core/project-layout.js.map +1 -1
- package/dist/core/schema-names.d.ts +17 -0
- package/dist/core/schema-names.d.ts.map +1 -1
- package/dist/core/schema-names.js +24 -0
- package/dist/core/schema-names.js.map +1 -1
- package/dist/core/shared/dashboard-mod.d.ts +24 -0
- package/dist/core/shared/dashboard-mod.d.ts.map +1 -0
- package/dist/core/shared/dashboard-mod.js +44 -0
- package/dist/core/shared/dashboard-mod.js.map +1 -0
- package/dist/core/shared/index.d.ts +2 -1
- package/dist/core/shared/index.d.ts.map +1 -1
- package/dist/core/shared/index.js +2 -1
- package/dist/core/shared/index.js.map +1 -1
- package/dist/core/shared/rules-generation.d.ts +12 -0
- package/dist/core/shared/rules-generation.d.ts.map +1 -1
- package/dist/core/shared/rules-generation.js +26 -2
- package/dist/core/shared/rules-generation.js.map +1 -1
- package/dist/core/shared/skill-generation.d.ts +7 -0
- package/dist/core/shared/skill-generation.d.ts.map +1 -1
- package/dist/core/shared/skill-generation.js +19 -0
- package/dist/core/shared/skill-generation.js.map +1 -1
- package/dist/core/shared/tool-detection.d.ts +10 -1
- package/dist/core/shared/tool-detection.d.ts.map +1 -1
- package/dist/core/shared/tool-detection.js +37 -6
- package/dist/core/shared/tool-detection.js.map +1 -1
- package/dist/core/spec-presenter.d.ts.map +1 -1
- package/dist/core/spec-presenter.js +23 -1
- package/dist/core/spec-presenter.js.map +1 -1
- package/dist/core/specs-apply.d.ts +98 -1
- package/dist/core/specs-apply.d.ts.map +1 -1
- package/dist/core/specs-apply.js +426 -63
- package/dist/core/specs-apply.js.map +1 -1
- package/dist/core/templates/fragments/change-diff.d.ts +37 -0
- package/dist/core/templates/fragments/change-diff.d.ts.map +1 -0
- package/dist/core/templates/fragments/change-diff.js +37 -0
- package/dist/core/templates/fragments/change-diff.js.map +1 -0
- package/dist/core/templates/fragments/sync-fail.d.ts +8 -0
- package/dist/core/templates/fragments/sync-fail.d.ts.map +1 -0
- package/dist/core/templates/fragments/sync-fail.js +8 -0
- package/dist/core/templates/fragments/sync-fail.js.map +1 -0
- package/dist/core/templates/fragments/validate.d.ts +14 -1
- package/dist/core/templates/fragments/validate.d.ts.map +1 -1
- package/dist/core/templates/fragments/validate.js +14 -1
- package/dist/core/templates/fragments/validate.js.map +1 -1
- package/dist/core/templates/fragments/verify.d.ts +2 -2
- package/dist/core/templates/fragments/verify.d.ts.map +1 -1
- package/dist/core/templates/fragments/verify.js +8 -5
- package/dist/core/templates/fragments/verify.js.map +1 -1
- package/dist/core/templates/workflows/apply.d.ts +1 -1
- package/dist/core/templates/workflows/apply.d.ts.map +1 -1
- package/dist/core/templates/workflows/apply.js +29 -10
- package/dist/core/templates/workflows/apply.js.map +1 -1
- package/dist/core/templates/workflows/archive.d.ts.map +1 -1
- package/dist/core/templates/workflows/archive.js +27 -9
- package/dist/core/templates/workflows/archive.js.map +1 -1
- package/dist/core/templates/workflows/decision.d.ts.map +1 -1
- package/dist/core/templates/workflows/decision.js +10 -4
- package/dist/core/templates/workflows/decision.js.map +1 -1
- package/dist/core/templates/workflows/explore.d.ts +2 -2
- package/dist/core/templates/workflows/explore.d.ts.map +1 -1
- package/dist/core/templates/workflows/explore.js +7 -3
- package/dist/core/templates/workflows/explore.js.map +1 -1
- package/dist/core/templates/workflows/grill.d.ts +2 -2
- package/dist/core/templates/workflows/grill.d.ts.map +1 -1
- package/dist/core/templates/workflows/grill.js +5 -4
- package/dist/core/templates/workflows/grill.js.map +1 -1
- package/dist/core/templates/workflows/issue.d.ts.map +1 -1
- package/dist/core/templates/workflows/issue.js +9 -3
- package/dist/core/templates/workflows/issue.js.map +1 -1
- package/dist/core/templates/workflows/propose.d.ts.map +1 -1
- package/dist/core/templates/workflows/propose.js +4 -1
- package/dist/core/templates/workflows/propose.js.map +1 -1
- package/dist/core/templates/workflows/sync.d.ts.map +1 -1
- package/dist/core/templates/workflows/sync.js +25 -11
- package/dist/core/templates/workflows/sync.js.map +1 -1
- package/dist/core/templates/workflows/update.d.ts.map +1 -1
- package/dist/core/templates/workflows/update.js +17 -10
- package/dist/core/templates/workflows/update.js.map +1 -1
- package/dist/core/update.d.ts +5 -0
- package/dist/core/update.d.ts.map +1 -1
- package/dist/core/update.js +69 -29
- package/dist/core/update.js.map +1 -1
- package/dist/core/validation/constants.d.ts +7 -1
- package/dist/core/validation/constants.d.ts.map +1 -1
- package/dist/core/validation/constants.js +16 -2
- package/dist/core/validation/constants.js.map +1 -1
- package/dist/core/validation/purpose-placeholder.d.ts.map +1 -1
- package/dist/core/validation/purpose-placeholder.js +35 -11
- package/dist/core/validation/purpose-placeholder.js.map +1 -1
- package/dist/core/validation/section-validator.d.ts +7 -0
- package/dist/core/validation/section-validator.d.ts.map +1 -1
- package/dist/core/validation/section-validator.js +22 -2
- package/dist/core/validation/section-validator.js.map +1 -1
- package/dist/core/validation/sister-changes.d.ts +93 -0
- package/dist/core/validation/sister-changes.d.ts.map +1 -0
- package/dist/core/validation/sister-changes.js +180 -0
- package/dist/core/validation/sister-changes.js.map +1 -0
- package/dist/core/validation/task-markers.d.ts +9 -0
- package/dist/core/validation/task-markers.d.ts.map +1 -0
- package/dist/core/validation/task-markers.js +71 -0
- package/dist/core/validation/task-markers.js.map +1 -0
- package/dist/core/validation/types.d.ts +58 -0
- package/dist/core/validation/types.d.ts.map +1 -1
- package/dist/core/validation/types.js +22 -0
- package/dist/core/validation/types.js.map +1 -1
- package/dist/core/validation/validator.d.ts +79 -2
- package/dist/core/validation/validator.d.ts.map +1 -1
- package/dist/core/validation/validator.js +565 -84
- package/dist/core/validation/validator.js.map +1 -1
- package/dist/utils/change-metadata.d.ts +62 -5
- package/dist/utils/change-metadata.d.ts.map +1 -1
- package/dist/utils/change-metadata.js +193 -31
- package/dist/utils/change-metadata.js.map +1 -1
- package/dist/utils/change-utils.d.ts +21 -0
- package/dist/utils/change-utils.d.ts.map +1 -1
- package/dist/utils/change-utils.js +168 -53
- package/dist/utils/change-utils.js.map +1 -1
- package/dist/utils/command-line.d.ts +11 -0
- package/dist/utils/command-line.d.ts.map +1 -0
- package/dist/utils/command-line.js +43 -0
- package/dist/utils/command-line.js.map +1 -0
- package/dist/utils/display-width.d.ts +10 -0
- package/dist/utils/display-width.d.ts.map +1 -0
- package/dist/utils/display-width.js +15 -0
- package/dist/utils/display-width.js.map +1 -0
- package/dist/utils/file-system.d.ts +16 -0
- package/dist/utils/file-system.d.ts.map +1 -1
- package/dist/utils/file-system.js +49 -2
- package/dist/utils/file-system.js.map +1 -1
- package/dist/utils/frontmatter.d.ts.map +1 -1
- package/dist/utils/frontmatter.js +11 -5
- package/dist/utils/frontmatter.js.map +1 -1
- package/dist/utils/interactive.d.ts.map +1 -1
- package/dist/utils/interactive.js +3 -1
- package/dist/utils/interactive.js.map +1 -1
- package/dist/utils/item-discovery.d.ts +3 -2
- package/dist/utils/item-discovery.d.ts.map +1 -1
- package/dist/utils/item-discovery.js +34 -16
- package/dist/utils/item-discovery.js.map +1 -1
- package/dist/utils/link.d.ts +0 -1
- package/dist/utils/link.d.ts.map +1 -1
- package/dist/utils/link.js +0 -1
- package/dist/utils/link.js.map +1 -1
- package/dist/utils/match.d.ts.map +1 -1
- package/dist/utils/match.js +10 -1
- package/dist/utils/match.js.map +1 -1
- package/dist/utils/option-flags.d.ts +9 -0
- package/dist/utils/option-flags.d.ts.map +1 -0
- package/dist/utils/option-flags.js +12 -0
- package/dist/utils/option-flags.js.map +1 -0
- package/dist/utils/sentence.d.ts +9 -0
- package/dist/utils/sentence.d.ts.map +1 -0
- package/dist/utils/sentence.js +12 -0
- package/dist/utils/sentence.js.map +1 -0
- package/dist/utils/spec-files.d.ts +84 -1
- package/dist/utils/spec-files.d.ts.map +1 -1
- package/dist/utils/spec-files.js +192 -3
- package/dist/utils/spec-files.js.map +1 -1
- package/dist/utils/task-progress.d.ts.map +1 -1
- package/dist/utils/task-progress.js +32 -9
- package/dist/utils/task-progress.js.map +1 -1
- package/dist/utils/timestamp.d.ts +27 -0
- package/dist/utils/timestamp.d.ts.map +1 -1
- package/dist/utils/timestamp.js +37 -0
- package/dist/utils/timestamp.js.map +1 -1
- package/package.json +6 -2
- package/schemas/decision/templates/decision.md +2 -2
- package/schemas/issue/schema.yaml +12 -6
- package/schemas/issue/templates/task.md +1 -1
- package/schemas/sdd/schema.yaml +21 -10
- package/schemas/sdd/templates/proposal.md +1 -1
- package/assets/dashboard/chart.umd.js +0 -14
|
@@ -4,13 +4,18 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import path from 'path';
|
|
6
6
|
import * as fs from 'fs';
|
|
7
|
-
import { loadChangeContext, generateInstructions, resolveSchema, resolveArtifactOutputs, } from '../../core/artifact-graph/index.js';
|
|
7
|
+
import { loadChangeContext, generateInstructions, resolveSchema, resolveArtifactOutputs, findSchemaWarnings, } from '../../core/artifact-graph/index.js';
|
|
8
8
|
import { getChangeDir, resolveCurrentPlanningHomeSync, } from '../../core/planning-home.js';
|
|
9
9
|
import { resolveRootForCommand, toPlanningHome, toRootOutput, } from '../../core/root-selection.js';
|
|
10
|
-
import { APPLY_RULES_KEY, readProjectConfigWithDiagnostics } from '../../core/project-config.js';
|
|
10
|
+
import { APPLY_RULES_KEY, printProjectConfigDiagnostic, readProjectConfigWithDiagnostics, } from '../../core/project-config.js';
|
|
11
11
|
import { parseTasksFromContent, resolveTaskFiles } from '../../utils/task-progress.js';
|
|
12
|
-
import {
|
|
12
|
+
import { readSkipSpecsMarker } from '../../utils/change-metadata.js';
|
|
13
|
+
import { findMisplacedSpecFiles, findSkipSpecsContradictions } from '../../utils/spec-files.js';
|
|
14
|
+
import { changeRequiresDeltas } from '../validate.js';
|
|
15
|
+
import { Validator, headerCorrectionClause } from '../../core/validation/validator.js';
|
|
16
|
+
import { validateChangeExists, validateChangeSchema, changeUnreadable, ChangeUnreadableError, } from './shared.js';
|
|
13
17
|
import { emitSuccess } from '../shared-output.js';
|
|
18
|
+
import { attributeValue, inlineField, neutralizeTags } from './prompt-escape.js';
|
|
14
19
|
/**
|
|
15
20
|
* The `--json` null-shape. This command's success payload is flat, so there is
|
|
16
21
|
* no wrapper key to null; `instruction` is the one key both success shapes
|
|
@@ -42,25 +47,37 @@ async function resolveInstructionsTarget(options) {
|
|
|
42
47
|
}
|
|
43
48
|
const projectRoot = root.path;
|
|
44
49
|
if (options.schema) {
|
|
45
|
-
|
|
46
|
-
assertChangeSchema(options.schema);
|
|
50
|
+
validateChangeSchema(options.schema, projectRoot);
|
|
47
51
|
}
|
|
48
52
|
const changeName = await validateChangeExists(options.change, projectRoot, root.changesDir, {
|
|
49
53
|
newChangeHint: 'tospec new change <name>',
|
|
50
54
|
});
|
|
51
55
|
return { root, planningHome: toPlanningHome(root), projectRoot, changeName };
|
|
52
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* The change's context, or `change_unreadable` when it exists but does not
|
|
59
|
+
* load. The schema comes from the change's metadata when not given. Only the
|
|
60
|
+
* load is wrapped, as in `status --change`: a missing change and a bad
|
|
61
|
+
* `--schema` are refused before it, under their own codes.
|
|
62
|
+
*/
|
|
63
|
+
function loadInstructionsContext(projectRoot, changeName, schemaName, planningHome) {
|
|
64
|
+
try {
|
|
65
|
+
return loadChangeContext(projectRoot, changeName, schemaName, {
|
|
66
|
+
changeDir: getChangeDir(planningHome, changeName),
|
|
67
|
+
planningHome,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
throw new ChangeUnreadableError(changeUnreadable(changeName, error, { planningHome, projectRoot, schema: schemaName }).diagnostic);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
53
74
|
export async function instructionsCommand(artifactId, options) {
|
|
54
75
|
const target = await resolveInstructionsTarget(options);
|
|
55
76
|
if (!target) {
|
|
56
77
|
return;
|
|
57
78
|
}
|
|
58
79
|
const { root, planningHome, projectRoot, changeName } = target;
|
|
59
|
-
|
|
60
|
-
const context = loadChangeContext(projectRoot, changeName, options.schema, {
|
|
61
|
-
changeDir: getChangeDir(planningHome, changeName),
|
|
62
|
-
planningHome,
|
|
63
|
-
});
|
|
80
|
+
const context = loadInstructionsContext(projectRoot, changeName, options.schema, planningHome);
|
|
64
81
|
// Includes `apply`: the schema's artifacts alone omit the one valid argument
|
|
65
82
|
// that is not an artifact.
|
|
66
83
|
const validArgumentList = () => [...context.graph.getAllArtifacts().map((a) => a.id), APPLY_ARGUMENT].join('\n ');
|
|
@@ -84,9 +101,7 @@ export async function instructionsCommand(artifactId, options) {
|
|
|
84
101
|
// while `status` called it ready — and sent the agent to write the specs the
|
|
85
102
|
// change had just declared it has none of.
|
|
86
103
|
const unmetDependencies = instructions.dependencies
|
|
87
|
-
.filter((d) => !d.done &&
|
|
88
|
-
!(context.skippedArtifacts?.has(d.id) ?? false) &&
|
|
89
|
-
context.graph.getArtifact(d.id)?.optional !== true)
|
|
104
|
+
.filter((d) => !d.done && !isMetWithoutFile(d))
|
|
90
105
|
.map((d) => d.id);
|
|
91
106
|
// `status` says "skipped" for this artifact, so a brief that reads as a
|
|
92
107
|
// plain "write it" sends an agent into the contradiction `validate` rejects
|
|
@@ -155,25 +170,28 @@ function blockedDiagnostics(instructions, changeName, missing) {
|
|
|
155
170
|
}
|
|
156
171
|
export function printInstructionsText(instructions, unmetDependencies, flags = {}) {
|
|
157
172
|
const { artifactId, changeName, schemaName, changeDir, resolvedOutputPath, description, instruction, context, rules, template, validation, dependencies, unlocks, changeMetadata, } = instructions;
|
|
158
|
-
|
|
173
|
+
// Every value below is neutralized before it is printed (prompt-escape.ts):
|
|
174
|
+
// config, metadata and schemas are repo-controlled, and an unescaped closing
|
|
175
|
+
// tag in any of them would end the block it sits in.
|
|
176
|
+
console.log(`<artifact id="${attributeValue(artifactId)}" change="${attributeValue(changeName)}" schema="${attributeValue(schemaName)}">`);
|
|
159
177
|
console.log();
|
|
160
178
|
if (flags.skipped) {
|
|
161
179
|
console.log('<warning>');
|
|
162
|
-
console.log(skippedArtifactMessage(artifactId, changeName));
|
|
163
|
-
console.log(`Remove skip_specs from the change's .tospec.yaml before writing ${artifactId}.`);
|
|
180
|
+
console.log(inlineField(skippedArtifactMessage(artifactId, changeName)));
|
|
181
|
+
console.log(inlineField(`Remove skip_specs from the change's .tospec.yaml before writing ${artifactId}.`));
|
|
164
182
|
console.log('</warning>');
|
|
165
183
|
console.log();
|
|
166
184
|
}
|
|
167
185
|
if (unmetDependencies.length > 0) {
|
|
168
186
|
console.log('<warning>');
|
|
169
187
|
console.log('This artifact has unmet dependencies. Complete them first or proceed with caution.');
|
|
170
|
-
console.log(`Missing: ${unmetDependencies.join(', ')}`);
|
|
188
|
+
console.log(`Missing: ${inlineField(unmetDependencies.join(', '))}`);
|
|
171
189
|
console.log('</warning>');
|
|
172
190
|
console.log();
|
|
173
191
|
}
|
|
174
192
|
console.log('<task>');
|
|
175
|
-
console.log(`Create the ${artifactId} artifact for change "${changeName}".`);
|
|
176
|
-
console.log(description);
|
|
193
|
+
console.log(inlineField(`Create the ${artifactId} artifact for change "${changeName}".`));
|
|
194
|
+
console.log(neutralizeTags(description));
|
|
177
195
|
console.log('</task>');
|
|
178
196
|
console.log();
|
|
179
197
|
// What `tospec new change` recorded, same as the JSON payload's
|
|
@@ -183,12 +201,12 @@ export function printInstructionsText(instructions, unmetDependencies, flags = {
|
|
|
183
201
|
if (changeMetadata.goal || decisions.length > 0) {
|
|
184
202
|
console.log('<change_context>');
|
|
185
203
|
if (changeMetadata.goal) {
|
|
186
|
-
console.log(`Goal: ${changeMetadata.goal}`);
|
|
204
|
+
console.log(`Goal: ${inlineField(changeMetadata.goal)}`);
|
|
187
205
|
}
|
|
188
206
|
if (decisions.length > 0) {
|
|
189
207
|
console.log('Decisions driving this change (read them under tospec/decisions/):');
|
|
190
208
|
for (const file of decisions) {
|
|
191
|
-
console.log(`- ${file}`);
|
|
209
|
+
console.log(`- ${inlineField(file)}`);
|
|
192
210
|
}
|
|
193
211
|
}
|
|
194
212
|
console.log('</change_context>');
|
|
@@ -197,7 +215,7 @@ export function printInstructionsText(instructions, unmetDependencies, flags = {
|
|
|
197
215
|
if (context) {
|
|
198
216
|
console.log('<project_context>');
|
|
199
217
|
console.log('<!-- This is background information for you. Do NOT include this in your output. -->');
|
|
200
|
-
console.log(context);
|
|
218
|
+
console.log(neutralizeTags(context));
|
|
201
219
|
console.log('</project_context>');
|
|
202
220
|
console.log();
|
|
203
221
|
}
|
|
@@ -205,39 +223,52 @@ export function printInstructionsText(instructions, unmetDependencies, flags = {
|
|
|
205
223
|
console.log('<rules>');
|
|
206
224
|
console.log('<!-- These are constraints for you to follow. Do NOT include this in your output. -->');
|
|
207
225
|
for (const rule of rules) {
|
|
208
|
-
console.log(`- ${rule}`);
|
|
226
|
+
console.log(`- ${inlineField(rule)}`);
|
|
209
227
|
}
|
|
210
228
|
console.log('</rules>');
|
|
211
229
|
console.log();
|
|
212
230
|
}
|
|
231
|
+
// A dependency met without a file stays in the block, labelled as `status`
|
|
232
|
+
// labels it, but outside the read instruction and without a path: listed
|
|
233
|
+
// there, a skipped `specs` sent the agent after a glob that matches nothing.
|
|
234
|
+
const toRead = dependencies.filter((dep) => !isMetWithoutFile(dep));
|
|
235
|
+
const metWithoutFile = dependencies.filter(isMetWithoutFile);
|
|
213
236
|
if (dependencies.length > 0) {
|
|
214
237
|
console.log('<dependencies>');
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
const
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
238
|
+
if (toRead.length > 0) {
|
|
239
|
+
console.log('Read the current contents of these files before creating this artifact (re-read them from disk even if you saw them earlier - they may have been edited since):');
|
|
240
|
+
console.log();
|
|
241
|
+
for (const dep of toRead) {
|
|
242
|
+
printDependency(dep, dep.done ? 'done' : 'missing', path.join(changeDir, dep.path));
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
if (metWithoutFile.length > 0) {
|
|
246
|
+
if (toRead.length > 0)
|
|
247
|
+
console.log();
|
|
248
|
+
console.log('Not written, and this change does not need them, so there is no file to read:');
|
|
249
|
+
console.log();
|
|
250
|
+
for (const dep of metWithoutFile) {
|
|
251
|
+
// `skipped` first, as `status` ranks it: the change's declaration is
|
|
252
|
+
// the stronger statement than the schema's "may be omitted".
|
|
253
|
+
printDependency(dep, dep.skipped ? 'skipped' : 'optional');
|
|
254
|
+
}
|
|
224
255
|
}
|
|
225
256
|
console.log('</dependencies>');
|
|
226
257
|
console.log();
|
|
227
258
|
}
|
|
228
259
|
console.log('<output>');
|
|
229
|
-
console.log(`Write to: ${resolvedOutputPath}`);
|
|
260
|
+
console.log(`Write to: ${inlineField(resolvedOutputPath)}`);
|
|
230
261
|
console.log('</output>');
|
|
231
262
|
console.log();
|
|
232
263
|
if (instruction) {
|
|
233
264
|
console.log('<instruction>');
|
|
234
|
-
console.log(instruction.trim());
|
|
265
|
+
console.log(neutralizeTags(instruction.trim()));
|
|
235
266
|
console.log('</instruction>');
|
|
236
267
|
console.log();
|
|
237
268
|
}
|
|
238
269
|
console.log('<template>');
|
|
239
270
|
console.log('<!-- Use this as the structure for your output file. Fill in the sections. -->');
|
|
240
|
-
console.log(template.trim());
|
|
271
|
+
console.log(neutralizeTags(template.trim()));
|
|
241
272
|
console.log('</template>');
|
|
242
273
|
console.log();
|
|
243
274
|
// The schema's own acceptance criteria, present only when it declares some:
|
|
@@ -245,25 +276,41 @@ export function printInstructionsText(instructions, unmetDependencies, flags = {
|
|
|
245
276
|
if (validation) {
|
|
246
277
|
console.log('<success_criteria>');
|
|
247
278
|
if (validation.requiredSections.length > 0) {
|
|
248
|
-
console.log(`Required sections: ${validation.requiredSections.map((s) => `## ${s}`).join(', ')}`);
|
|
279
|
+
console.log(`Required sections: ${inlineField(validation.requiredSections.map((s) => `## ${s}`).join(', '))}`);
|
|
249
280
|
}
|
|
250
281
|
for (const [section, minLength] of Object.entries(validation.minSectionLength)) {
|
|
251
|
-
console.log(`Section "${section}" must be at least ${minLength} characters.`);
|
|
282
|
+
console.log(`Section "${inlineField(section)}" must be at least ${minLength} characters.`);
|
|
252
283
|
}
|
|
253
284
|
for (const [section, maxLength] of Object.entries(validation.maxSectionLength ?? {})) {
|
|
254
|
-
console.log(`Section "${section}" should stay under ${maxLength} characters.`);
|
|
285
|
+
console.log(`Section "${inlineField(section)}" should stay under ${maxLength} characters.`);
|
|
255
286
|
}
|
|
256
287
|
console.log('</success_criteria>');
|
|
257
288
|
console.log();
|
|
258
289
|
}
|
|
259
290
|
if (unlocks.length > 0) {
|
|
260
291
|
console.log('<unlocks>');
|
|
261
|
-
console.log(`Completing this artifact enables: ${unlocks.join(', ')}`);
|
|
292
|
+
console.log(`Completing this artifact enables: ${inlineField(unlocks.join(', '))}`);
|
|
262
293
|
console.log('</unlocks>');
|
|
263
294
|
console.log();
|
|
264
295
|
}
|
|
265
296
|
console.log('</artifact>');
|
|
266
297
|
}
|
|
298
|
+
/**
|
|
299
|
+
* Declared away by the change, or optional and not written: the graph counts
|
|
300
|
+
* either as met, and neither has a file to read.
|
|
301
|
+
*/
|
|
302
|
+
function isMetWithoutFile(dep) {
|
|
303
|
+
return !dep.done && (dep.skipped === true || dep.optional);
|
|
304
|
+
}
|
|
305
|
+
/** One `<dependency>` element; `fullPath` is omitted when there is no file to read. */
|
|
306
|
+
function printDependency(dep, status, fullPath) {
|
|
307
|
+
console.log(`<dependency id="${attributeValue(dep.id)}" status="${status}">`);
|
|
308
|
+
if (fullPath !== undefined) {
|
|
309
|
+
console.log(` <path>${inlineField(fullPath)}</path>`);
|
|
310
|
+
}
|
|
311
|
+
console.log(` <description>${inlineField(dep.description)}</description>`);
|
|
312
|
+
console.log('</dependency>');
|
|
313
|
+
}
|
|
267
314
|
/**
|
|
268
315
|
* The planning skill that produces a schema's artifacts, so blocked-state
|
|
269
316
|
* instructions point at a skill that exists.
|
|
@@ -275,6 +322,18 @@ function planningSkillFor(schemaName) {
|
|
|
275
322
|
return 'tospec-propose';
|
|
276
323
|
return 'tospec-propose (sdd) or tospec-issue (issue)';
|
|
277
324
|
}
|
|
325
|
+
/**
|
|
326
|
+
* The `apply_archive_would_refuse` fix for a refusal that carries
|
|
327
|
+
* `suggestedHeader`. Only a sister change's spelling needs the sentence on
|
|
328
|
+
* archive order: that refusal is the one a folded match once promised the
|
|
329
|
+
* sister's archive would settle. A main-spec name is settled already.
|
|
330
|
+
*/
|
|
331
|
+
function headerCorrectionFix({ operation, source }, suggestedHeader) {
|
|
332
|
+
const edit = headerCorrectionClause(operation, suggestedHeader);
|
|
333
|
+
return source === 'sister-change'
|
|
334
|
+
? `${edit}, the name the in-progress change that adds it uses. Archive matches requirement names exactly, so archiving that change first does not resolve it.`
|
|
335
|
+
: `${edit}, the name the main spec uses. Archive matches requirement names exactly.`;
|
|
336
|
+
}
|
|
278
337
|
/**
|
|
279
338
|
* How to name the tracked task file in a message to the user. `path.basename`
|
|
280
339
|
* alone turns a glob (`tasks/*.md`) into a bare `*.md`, which names no file, so
|
|
@@ -286,13 +345,19 @@ function describeTracksFile(tracksFile) {
|
|
|
286
345
|
/**
|
|
287
346
|
* Shares `parseTasksFromContent` with task progress so the apply instructions
|
|
288
347
|
* and the reported `x/y` can never disagree about what counts as a task.
|
|
348
|
+
* Exported so the backtracking test clock holds only the parse.
|
|
289
349
|
*/
|
|
290
|
-
function parseTasksFile(content) {
|
|
350
|
+
export function parseTasksFile(content) {
|
|
291
351
|
return parseTasksFromContent(content).map((task, i) => {
|
|
292
352
|
// The number the file gives the task, not this array's index: they diverge
|
|
293
353
|
// the moment a slice starts at 2.1 or a task is deleted, and the id is what
|
|
294
354
|
// a caller quotes back. Falls back to the ordinal when there is none.
|
|
295
|
-
|
|
355
|
+
//
|
|
356
|
+
// `[^\r\n]*` as in TASK_PATTERN, whose text can hold U+2028/U+2029: `.*`
|
|
357
|
+
// stopped there, so the id fell back to the ordinal, and `\s+` re-split the
|
|
358
|
+
// blanks before it, 2.6 s for `1.1` + 80k blanks + `a<U+2028>b`. Task text
|
|
359
|
+
// holds no CR or LF, so this always reaches its end.
|
|
360
|
+
const numbered = /^(\d+(?:\.\d+)*)\s+([^\r\n]*)$/.exec(task.text);
|
|
296
361
|
return {
|
|
297
362
|
id: numbered?.[1] ?? `${i + 1}`,
|
|
298
363
|
// Without that number, since `id` carries it and a consumer joining the
|
|
@@ -308,11 +373,7 @@ function parseTasksFile(content) {
|
|
|
308
373
|
*/
|
|
309
374
|
export async function generateApplyInstructions(projectRoot, changeName, schemaName, options = {}) {
|
|
310
375
|
const planningHome = options.planningHome ?? resolveCurrentPlanningHomeSync({ startPath: projectRoot });
|
|
311
|
-
|
|
312
|
-
const context = loadChangeContext(projectRoot, changeName, schemaName, {
|
|
313
|
-
changeDir: getChangeDir(planningHome, changeName),
|
|
314
|
-
planningHome,
|
|
315
|
-
});
|
|
376
|
+
const context = loadInstructionsContext(projectRoot, changeName, schemaName, planningHome);
|
|
316
377
|
const changeDir = context.changeDir;
|
|
317
378
|
const schema = resolveSchema(context.schemaName, projectRoot);
|
|
318
379
|
const applyConfig = schema.apply;
|
|
@@ -320,13 +381,27 @@ export async function generateApplyInstructions(projectRoot, changeName, schemaN
|
|
|
320
381
|
const requiredArtifactIds = applyConfig?.requires ?? schema.artifacts.map((a) => a.id);
|
|
321
382
|
const tracksFile = applyConfig?.tracks ?? null;
|
|
322
383
|
const schemaInstruction = applyConfig?.instruction ?? null;
|
|
323
|
-
|
|
384
|
+
// A required artifact still empty or holding its template blocks like an
|
|
385
|
+
// absent one: counted by existence alone, a copied tasks template reached
|
|
386
|
+
// `ready` and its placeholders were handed over as the work, while `status`
|
|
387
|
+
// called it a stub. Read from `context.stubs`, so the two cannot disagree.
|
|
388
|
+
// Kept apart from the absent ones only for the wording: a stub's file is on
|
|
389
|
+
// disk, and calling it "missing" sends the reader looking for a file that is
|
|
390
|
+
// right there.
|
|
391
|
+
const absentArtifacts = [];
|
|
392
|
+
const stubArtifacts = [];
|
|
324
393
|
for (const artifactId of requiredArtifactIds) {
|
|
325
394
|
const artifact = schema.artifacts.find((a) => a.id === artifactId);
|
|
326
|
-
if (artifact
|
|
327
|
-
|
|
395
|
+
if (!artifact)
|
|
396
|
+
continue;
|
|
397
|
+
if (resolveArtifactOutputs(changeDir, artifact.generates).length === 0) {
|
|
398
|
+
absentArtifacts.push(artifactId);
|
|
399
|
+
}
|
|
400
|
+
else if (context.stubs?.has(artifactId)) {
|
|
401
|
+
stubArtifacts.push(artifactId);
|
|
328
402
|
}
|
|
329
403
|
}
|
|
404
|
+
const missingArtifacts = [...absentArtifacts, ...stubArtifacts];
|
|
330
405
|
const contextFiles = {};
|
|
331
406
|
for (const artifact of schema.artifacts) {
|
|
332
407
|
const outputs = resolveArtifactOutputs(changeDir, artifact.generates);
|
|
@@ -353,9 +428,70 @@ export async function generateApplyInstructions(projectRoot, changeName, schemaN
|
|
|
353
428
|
let state;
|
|
354
429
|
let instruction;
|
|
355
430
|
const planningSkill = planningSkillFor(context.schemaName);
|
|
431
|
+
// Absent artifacts `apply.requires` does not insist on: reported, not
|
|
432
|
+
// blocking. sdd's apply requires only `tasks`, so a change missing
|
|
433
|
+
// proposal/design/specs reaches `ready` while its instruction still refers
|
|
434
|
+
// to them.
|
|
435
|
+
const missingContext = schema.artifacts
|
|
436
|
+
.filter((artifact) => !contextFiles[artifact.id] && !missingArtifacts.includes(artifact.id))
|
|
437
|
+
.map((artifact) => artifact.id);
|
|
438
|
+
// `specs` is the one absent artifact apply must not wave away: when the
|
|
439
|
+
// schema requires it and the change did not declare it away (skip_specs,
|
|
440
|
+
// scoped to that id like every other reader of the marker), validate and
|
|
441
|
+
// archive refuse the change. Told to ignore it, an agent finished the work
|
|
442
|
+
// and only learned that at archive time. Decided before `state`, because
|
|
443
|
+
// `all_done` must not call such a change ready to archive.
|
|
444
|
+
const specsArtifact = schema.artifacts.find((artifact) => artifact.id === 'specs');
|
|
445
|
+
const specsRequiredButMissing = specsArtifact !== undefined &&
|
|
446
|
+
!specsArtifact.optional &&
|
|
447
|
+
missingContext.includes('specs') &&
|
|
448
|
+
!(context.skippedArtifacts?.has('specs') ?? false);
|
|
449
|
+
// The opposite fault: skip_specs: true beside spec files. validate refuses
|
|
450
|
+
// it, and so archive, yet only validate reported it, at apply's step 6, once
|
|
451
|
+
// the work was done. Read through validate's own helper and under any
|
|
452
|
+
// schema, as validate reads it, so the two cannot disagree about a change.
|
|
453
|
+
const skipSpecsMarker = readSkipSpecsMarker(changeDir);
|
|
454
|
+
const contradictingSpecs = await findSkipSpecsContradictions(changeDir, skipSpecsMarker);
|
|
455
|
+
// Spec files validate refuses for where they sit (nested, a dot folder, a
|
|
456
|
+
// misnamed file, specs/spec.md). Neither warning above names them: under
|
|
457
|
+
// skip_specs they contradict nothing, so apply said ready, while the
|
|
458
|
+
// "Not present in this change: specs" line told the agent to ignore them,
|
|
459
|
+
// and validate refused the change only at step 6.
|
|
460
|
+
const misplacedSpecs = await findMisplacedSpecFiles(path.join(changeDir, 'specs'), {
|
|
461
|
+
requireDelta: await changeRequiresDeltas(projectRoot, changeDir, { marker: skipSpecsMarker, contradictingSpecs }),
|
|
462
|
+
});
|
|
463
|
+
// Deltas archive's merge would refuse (a MODIFIED, REMOVED or RENAMED on a capability
|
|
464
|
+
// with no main spec, an ADDED that already exists). validate reported them
|
|
465
|
+
// and apply said ready, so the work was done before archive refused it.
|
|
466
|
+
// Through validate's own dry run, not a second list of preconditions here,
|
|
467
|
+
// which would be free to drift from archive's.
|
|
468
|
+
const archiveRefusals = fs.existsSync(path.join(changeDir, 'specs'))
|
|
469
|
+
? await new Validator().findArchiveRefusals(changeDir, path.join(projectRoot, 'tospec', 'specs'))
|
|
470
|
+
: [];
|
|
471
|
+
// What keeps an all-done change from archiving. apply_specs_missing and
|
|
472
|
+
// apply_skip_specs_contradicted cannot both hold, as a declared marker drops
|
|
473
|
+
// the first; a misplaced file can sit beside either, and is named after
|
|
474
|
+
// them. A refused delta is named last: the faults above decide which files
|
|
475
|
+
// archive merges at all.
|
|
476
|
+
const archiveBlocker = specsRequiredButMissing
|
|
477
|
+
? 'it has no delta specs (see the apply_specs_missing warning)'
|
|
478
|
+
: contradictingSpecs.length > 0
|
|
479
|
+
? 'it sets skip_specs: true beside delta spec files (see the apply_skip_specs_contradicted warning)'
|
|
480
|
+
: misplacedSpecs.length > 0
|
|
481
|
+
? 'it has spec files validate refuses for where they sit (see the apply_specs_misplaced warning)'
|
|
482
|
+
: archiveRefusals.length > 0
|
|
483
|
+
? 'archive would refuse its delta specs (see the apply_archive_would_refuse warning)'
|
|
484
|
+
: undefined;
|
|
356
485
|
if (missingArtifacts.length > 0) {
|
|
357
486
|
state = 'blocked';
|
|
358
|
-
|
|
487
|
+
const reasons = [
|
|
488
|
+
...(absentArtifacts.length > 0 ? [`Missing artifacts: ${absentArtifacts.join(', ')}.`] : []),
|
|
489
|
+
// "empty or": a stub is either, and an empty file called a template sends
|
|
490
|
+
// the reader comparing it against one.
|
|
491
|
+
...(stubArtifacts.length > 0 ? [`Still empty or the unfilled template: ${stubArtifacts.join(', ')}.`] : []),
|
|
492
|
+
];
|
|
493
|
+
const action = stubArtifacts.length === 0 ? 'create the missing artifacts' : absentArtifacts.length === 0 ? 'fill them in' : 'create and fill them in';
|
|
494
|
+
instruction = `Cannot apply this change yet. ${reasons.join(' ')}\nStop and send the user back to ${planningSkill} to ${action} first.`;
|
|
359
495
|
}
|
|
360
496
|
else if (tracksFile && !tracksFileExists) {
|
|
361
497
|
const tracksFilename = describeTracksFile(tracksFile);
|
|
@@ -369,7 +505,9 @@ export async function generateApplyInstructions(projectRoot, changeName, schemaN
|
|
|
369
505
|
}
|
|
370
506
|
else if (tracksFile && remaining === 0 && total > 0) {
|
|
371
507
|
state = 'all_done';
|
|
372
|
-
instruction =
|
|
508
|
+
instruction = archiveBlocker
|
|
509
|
+
? `All tasks are complete, but this change cannot be archived yet: ${archiveBlocker}.\nResolve that warning before archiving.`
|
|
510
|
+
: 'All tasks are complete! This change is ready to be archived.\nConsider running tests and reviewing the changes before archiving.';
|
|
373
511
|
}
|
|
374
512
|
else if (!tracksFile) {
|
|
375
513
|
state = 'ready';
|
|
@@ -379,17 +517,73 @@ export async function generateApplyInstructions(projectRoot, changeName, schemaN
|
|
|
379
517
|
state = 'ready';
|
|
380
518
|
instruction = schemaInstruction?.trim() ?? 'Read context files, work through pending tasks, mark complete as you go.\nPause if you hit blockers or need clarification.';
|
|
381
519
|
}
|
|
382
|
-
//
|
|
383
|
-
//
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
520
|
+
// Reported even when blocked: an unmatched `apply.tracks` is what produces a
|
|
521
|
+
// "file is missing" block for a file that exists, so the block needs it.
|
|
522
|
+
const warnings = findSchemaWarnings(schema);
|
|
523
|
+
if (specsRequiredButMissing && state !== 'blocked') {
|
|
524
|
+
// A mistyped value (`skip_specs: "yes"`) is a declaration that failed, not
|
|
525
|
+
// an absent one: saying "does not declare" sent the user looking for a key
|
|
526
|
+
// they had written.
|
|
527
|
+
const marker = context.skipSpecsInvalidReason
|
|
528
|
+
? `its skip_specs value is invalid (${context.skipSpecsInvalidReason.replace(/\.$/, '')})`
|
|
529
|
+
: 'does not set skip_specs: true';
|
|
530
|
+
warnings.push({
|
|
531
|
+
code: 'apply_specs_missing',
|
|
532
|
+
message: `Change '${changeName}' has no delta specs and ${marker}, so validate and archive will refuse it once the work is done.`,
|
|
533
|
+
fix: `Write the delta specs with ${planningSkill} before implementing, or set \`skip_specs: true\` in the change's .tospec.yaml only when the user confirms it needs no delta spec.`,
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
// Reported even when blocked, unlike apply_specs_missing: the planning skill
|
|
537
|
+
// a block sends the user to does not settle it. Only the user can say
|
|
538
|
+
// whether the flag or the spec files are wrong.
|
|
539
|
+
if (contradictingSpecs.length > 0) {
|
|
540
|
+
warnings.push({
|
|
541
|
+
code: 'apply_skip_specs_contradicted',
|
|
542
|
+
message: `Change '${changeName}' sets skip_specs: true but has ${contradictingSpecs.length} spec file(s) (${contradictingSpecs.join(', ')}), so validate and archive will refuse it once the work is done.`,
|
|
543
|
+
fix: `Remove skip_specs from the change's .tospec.yaml when the user wants these delta specs; delete the spec files only when the user confirms this change needs no delta spec.`,
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
// Reported even when blocked, like the contradiction: the planning skill a
|
|
547
|
+
// block sends the user to does not move files.
|
|
548
|
+
if (misplacedSpecs.length > 0) {
|
|
549
|
+
const listed = misplacedSpecs.map((file) => `specs/${file}`).join(', ');
|
|
550
|
+
warnings.push({
|
|
551
|
+
code: 'apply_specs_misplaced',
|
|
552
|
+
message: `Change '${changeName}' has ${misplacedSpecs.length} spec file(s) that archive does not merge where they sit (${listed}), so validate and archive will refuse it once the work is done.`,
|
|
553
|
+
fix: `Move each file to specs/<capability>/spec.md; \`tospec validate ${changeName}\` names the exact path for each.`,
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
// Reported even when blocked, like the two above: the planning skill a block
|
|
557
|
+
// sends the user to writes deltas against the main specs it reads, and when
|
|
558
|
+
// the cause is a sister change still unarchived, no edit there settles it.
|
|
559
|
+
// validate's text verbatim, so its "Archive '<change>' first" hint survives.
|
|
560
|
+
// `blockedBy` is validate's own field, carried as is, so an agent reads the
|
|
561
|
+
// order from one place in both commands instead of from the message.
|
|
562
|
+
// `suggestedHeader` likewise: the refusal's text is the merge's and does not
|
|
563
|
+
// name the spelling the sister change uses, so the fix has to. For a near
|
|
564
|
+
// miss of the main spec the merge's text does name it, but the field is what
|
|
565
|
+
// an agent reads, so the fix names it the same way.
|
|
566
|
+
for (const { issue: refusal, headerCorrection } of archiveRefusals) {
|
|
567
|
+
const blockedBy = refusal.blockedBy ?? [];
|
|
568
|
+
const { suggestedHeader } = refusal;
|
|
569
|
+
const validateNote = `\`tospec validate ${changeName}\` reports the same finding.`;
|
|
570
|
+
warnings.push({
|
|
571
|
+
code: 'apply_archive_would_refuse',
|
|
572
|
+
message: `Change '${changeName}', specs/${refusal.path}: ${refusal.message}`,
|
|
573
|
+
fix: blockedBy.length > 0
|
|
574
|
+
? `Archive ${blockedBy.map((name) => `'${name}'`).join(', ')} first, then run apply again; no edit to this delta resolves it. ${validateNote}`
|
|
575
|
+
: suggestedHeader !== undefined && headerCorrection !== undefined
|
|
576
|
+
? `Correct the delta with ${planningSkill} before implementing: ${headerCorrectionFix(headerCorrection, suggestedHeader)} ${validateNote}`
|
|
577
|
+
: `Correct the delta with ${planningSkill} before implementing. ${validateNote}`,
|
|
578
|
+
...(blockedBy.length > 0 ? { blockedBy } : {}),
|
|
579
|
+
...(suggestedHeader === undefined ? {} : { suggestedHeader }),
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
const ignorable = specsRequiredButMissing ? missingContext.filter((id) => id !== 'specs') : missingContext;
|
|
583
|
+
if (ignorable.length > 0 && state !== 'blocked') {
|
|
390
584
|
instruction += `
|
|
391
585
|
|
|
392
|
-
Not present in this change: ${
|
|
586
|
+
Not present in this change: ${ignorable.join(', ')}. Ignore any part of the above that refers to them.`;
|
|
393
587
|
}
|
|
394
588
|
// The project's own context and rules, as every artifact's instructions carry.
|
|
395
589
|
const { config: projectConfig, diagnostics: configWarnings } = readProjectConfigWithDiagnostics(projectRoot);
|
|
@@ -409,6 +603,7 @@ Not present in this change: ${missingContext.join(', ')}. Ignore any part of the
|
|
|
409
603
|
...(configContext ? { context: configContext } : {}),
|
|
410
604
|
...(applyRules?.length ? { rules: applyRules } : {}),
|
|
411
605
|
...(configWarnings.length ? { configWarnings } : {}),
|
|
606
|
+
...(warnings.length ? { warnings } : {}),
|
|
412
607
|
};
|
|
413
608
|
}
|
|
414
609
|
export async function applyInstructionsCommand(options) {
|
|
@@ -420,43 +615,58 @@ export async function applyInstructionsCommand(options) {
|
|
|
420
615
|
const instructions = await generateApplyInstructions(projectRoot, changeName, options.schema, {
|
|
421
616
|
planningHome,
|
|
422
617
|
});
|
|
618
|
+
// Printed in both modes, as `instructions <artifact>` does: the human text
|
|
619
|
+
// otherwise never said that the context or rules it shows were cut short.
|
|
620
|
+
for (const warning of instructions.configWarnings ?? []) {
|
|
621
|
+
printProjectConfigDiagnostic(warning.message);
|
|
622
|
+
}
|
|
423
623
|
if (options.json) {
|
|
424
624
|
// As in the artifact payload: `configWarnings` becomes the envelope's
|
|
425
625
|
// `status[]`, not a field of its own.
|
|
426
|
-
const { configWarnings, ...payload } = instructions;
|
|
626
|
+
const { configWarnings, warnings, ...payload } = instructions;
|
|
627
|
+
// The change's own warning first: it is the one that decides whether the
|
|
628
|
+
// work about to start can be archived at all.
|
|
629
|
+
const status = [
|
|
630
|
+
...(warnings ?? []).map(({ code, message, fix, blockedBy, suggestedHeader }) => ({
|
|
631
|
+
severity: 'warning',
|
|
632
|
+
code,
|
|
633
|
+
message,
|
|
634
|
+
...(fix ? { fix } : {}),
|
|
635
|
+
...(blockedBy ? { blockedBy } : {}),
|
|
636
|
+
...(suggestedHeader ? { suggestedHeader } : {}),
|
|
637
|
+
})),
|
|
638
|
+
...(configWarnings ?? []).map(({ code, message }) => ({
|
|
639
|
+
severity: 'warning',
|
|
640
|
+
code,
|
|
641
|
+
message,
|
|
642
|
+
})),
|
|
643
|
+
];
|
|
427
644
|
emitSuccess({
|
|
428
645
|
...payload,
|
|
429
|
-
...(
|
|
430
|
-
? {
|
|
431
|
-
status: configWarnings.map(({ code, message }) => ({
|
|
432
|
-
severity: 'warning',
|
|
433
|
-
code,
|
|
434
|
-
message,
|
|
435
|
-
})),
|
|
436
|
-
}
|
|
437
|
-
: {}),
|
|
646
|
+
...(status.length ? { status } : {}),
|
|
438
647
|
}, toRootOutput(root));
|
|
439
648
|
return;
|
|
440
649
|
}
|
|
441
650
|
printApplyInstructionsText(instructions);
|
|
442
651
|
}
|
|
443
652
|
export function printApplyInstructionsText(instructions) {
|
|
444
|
-
const { changeName, schemaName, contextFiles, progress, tasks, state, missingArtifacts, instruction, context, rules, } = instructions;
|
|
445
|
-
console.log(`## Apply: ${changeName}`);
|
|
446
|
-
console.log(`Schema: ${schemaName}`);
|
|
653
|
+
const { changeName, schemaName, contextFiles, progress, tasks, state, missingArtifacts, instruction, context, rules, warnings, } = instructions;
|
|
654
|
+
console.log(`## Apply: ${inlineField(changeName)}`);
|
|
655
|
+
console.log(`Schema: ${inlineField(schemaName)}`);
|
|
447
656
|
console.log();
|
|
448
657
|
// The same tags the artifact instructions use, so an agent reading either
|
|
449
|
-
// treats project constraints the same way
|
|
658
|
+
// treats project constraints the same way — and the same escaping, since the
|
|
659
|
+
// same repo-controlled text fills them.
|
|
450
660
|
if (context) {
|
|
451
661
|
console.log('<project_context>');
|
|
452
|
-
console.log(context);
|
|
662
|
+
console.log(neutralizeTags(context));
|
|
453
663
|
console.log('</project_context>');
|
|
454
664
|
console.log();
|
|
455
665
|
}
|
|
456
666
|
if (rules && rules.length > 0) {
|
|
457
667
|
console.log('<rules>');
|
|
458
668
|
for (const rule of rules) {
|
|
459
|
-
console.log(`- ${rule}`);
|
|
669
|
+
console.log(`- ${inlineField(rule)}`);
|
|
460
670
|
}
|
|
461
671
|
console.log('</rules>');
|
|
462
672
|
console.log();
|
|
@@ -464,8 +674,10 @@ export function printApplyInstructionsText(instructions) {
|
|
|
464
674
|
if (state === 'blocked' && missingArtifacts) {
|
|
465
675
|
console.log('### ⚠️ Blocked');
|
|
466
676
|
console.log();
|
|
467
|
-
|
|
468
|
-
|
|
677
|
+
// Archive's wording for the same list: it holds unfilled templates too, and
|
|
678
|
+
// the instruction below says which is which.
|
|
679
|
+
console.log(`Missing or incomplete artifacts: ${missingArtifacts.map(inlineField).join(', ')}`);
|
|
680
|
+
console.log(`Use the ${planningSkillFor(schemaName)} skill to complete these first.`);
|
|
469
681
|
console.log();
|
|
470
682
|
}
|
|
471
683
|
const contextFileEntries = Object.entries(contextFiles);
|
|
@@ -473,7 +685,7 @@ export function printApplyInstructionsText(instructions) {
|
|
|
473
685
|
console.log('### Context Files');
|
|
474
686
|
for (const [artifactId, filePaths] of contextFileEntries) {
|
|
475
687
|
for (const filePath of filePaths) {
|
|
476
|
-
console.log(`- ${artifactId}: ${filePath}`);
|
|
688
|
+
console.log(`- ${inlineField(artifactId)}: ${inlineField(filePath)}`);
|
|
477
689
|
}
|
|
478
690
|
}
|
|
479
691
|
console.log();
|
|
@@ -494,11 +706,17 @@ export function printApplyInstructionsText(instructions) {
|
|
|
494
706
|
const checkbox = task.done ? '[x]' : '[ ]';
|
|
495
707
|
// Joined again here: the number is stored apart only so a caller can
|
|
496
708
|
// address a task by it.
|
|
497
|
-
console.log(`- ${checkbox} ${task.id} ${task.description}`);
|
|
709
|
+
console.log(`- ${checkbox} ${task.id} ${inlineField(task.description)}`);
|
|
498
710
|
}
|
|
499
711
|
console.log();
|
|
500
712
|
}
|
|
713
|
+
for (const warning of warnings ?? []) {
|
|
714
|
+
console.log(`Warning: ${inlineField(warning.message)}`);
|
|
715
|
+
if (warning.fix)
|
|
716
|
+
console.log(`Fix: ${inlineField(warning.fix)}`);
|
|
717
|
+
console.log();
|
|
718
|
+
}
|
|
501
719
|
console.log('### Instruction');
|
|
502
|
-
console.log(instruction);
|
|
720
|
+
console.log(neutralizeTags(instruction));
|
|
503
721
|
}
|
|
504
722
|
//# sourceMappingURL=instructions.js.map
|