spectra-cli 4.2.0 → 4.3.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/README.md +12 -5
- package/dist/.spectra-build-meta.json +9 -0
- package/dist/adapters/language-adapter.d.ts +7 -12
- package/dist/adapters/language-adapter.d.ts.map +1 -1
- package/dist/adapters/python-adapter.d.ts.map +1 -1
- package/dist/adapters/python-adapter.js +11 -2
- package/dist/adapters/python-adapter.js.map +1 -1
- package/dist/batch/batch-orchestrator.d.ts +44 -1
- package/dist/batch/batch-orchestrator.d.ts.map +1 -1
- package/dist/batch/batch-orchestrator.js +312 -76
- package/dist/batch/batch-orchestrator.js.map +1 -1
- package/dist/batch/delta-regenerator.d.ts.map +1 -1
- package/dist/batch/delta-regenerator.js +21 -28
- package/dist/batch/delta-regenerator.js.map +1 -1
- package/dist/batch/module-grouper.d.ts +7 -0
- package/dist/batch/module-grouper.d.ts.map +1 -1
- package/dist/batch/module-grouper.js +2 -0
- package/dist/batch/module-grouper.js.map +1 -1
- package/dist/batch/regen-plan.d.ts +78 -0
- package/dist/batch/regen-plan.d.ts.map +1 -0
- package/dist/batch/regen-plan.js +93 -0
- package/dist/batch/regen-plan.js.map +1 -0
- package/dist/cli/commands/batch.d.ts.map +1 -1
- package/dist/cli/commands/batch.js +36 -8
- package/dist/cli/commands/batch.js.map +1 -1
- package/dist/cli/commands/scaffold-kb.d.ts +9 -0
- package/dist/cli/commands/scaffold-kb.d.ts.map +1 -0
- package/dist/cli/commands/scaffold-kb.js +179 -0
- package/dist/cli/commands/scaffold-kb.js.map +1 -0
- package/dist/cli/index.js +24 -5
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/utils/error-handler.d.ts +0 -6
- package/dist/cli/utils/error-handler.d.ts.map +1 -1
- package/dist/cli/utils/error-handler.js +0 -13
- package/dist/cli/utils/error-handler.js.map +1 -1
- package/dist/cli/utils/parse-args.d.ts +43 -3
- package/dist/cli/utils/parse-args.d.ts.map +1 -1
- package/dist/cli/utils/parse-args.js +98 -12
- package/dist/cli/utils/parse-args.js.map +1 -1
- package/dist/cli/version-meta.d.ts +16 -0
- package/dist/cli/version-meta.d.ts.map +1 -0
- package/dist/cli/version-meta.js +28 -0
- package/dist/cli/version-meta.js.map +1 -0
- package/dist/core/ast-analyzer.d.ts.map +1 -1
- package/dist/core/ast-analyzer.js +7 -11
- package/dist/core/ast-analyzer.js.map +1 -1
- package/dist/core/import-resolver.d.ts +70 -67
- package/dist/core/import-resolver.d.ts.map +1 -1
- package/dist/core/import-resolver.js +341 -200
- package/dist/core/import-resolver.js.map +1 -1
- package/dist/core/single-spec-orchestrator.d.ts +21 -0
- package/dist/core/single-spec-orchestrator.d.ts.map +1 -1
- package/dist/core/single-spec-orchestrator.js +41 -14
- package/dist/core/single-spec-orchestrator.js.map +1 -1
- package/dist/core/skeleton-hash.d.ts +33 -0
- package/dist/core/skeleton-hash.d.ts.map +1 -0
- package/dist/core/skeleton-hash.js +76 -0
- package/dist/core/skeleton-hash.js.map +1 -0
- package/dist/core/tree-sitter-analyzer.d.ts +3 -2
- package/dist/core/tree-sitter-analyzer.d.ts.map +1 -1
- package/dist/core/tree-sitter-analyzer.js +5 -6
- package/dist/core/tree-sitter-analyzer.js.map +1 -1
- package/dist/core/tree-sitter-fallback.d.ts +7 -2
- package/dist/core/tree-sitter-fallback.d.ts.map +1 -1
- package/dist/core/tree-sitter-fallback.js +18 -15
- package/dist/core/tree-sitter-fallback.js.map +1 -1
- package/dist/extraction/image-extractor.d.ts.map +1 -1
- package/dist/extraction/image-extractor.js +3 -1
- package/dist/extraction/image-extractor.js.map +1 -1
- package/dist/extraction/markdown-extractor.d.ts.map +1 -1
- package/dist/extraction/markdown-extractor.js +3 -1
- package/dist/extraction/markdown-extractor.js.map +1 -1
- package/dist/generator/frontmatter.d.ts +5 -0
- package/dist/generator/frontmatter.d.ts.map +1 -1
- package/dist/generator/frontmatter.js +4 -0
- package/dist/generator/frontmatter.js.map +1 -1
- package/dist/kb-mcp/index.d.ts +14 -0
- package/dist/kb-mcp/index.d.ts.map +1 -0
- package/dist/kb-mcp/index.js +24 -0
- package/dist/kb-mcp/index.js.map +1 -0
- package/dist/kb-mcp/lib/kb-error.d.ts +21 -0
- package/dist/kb-mcp/lib/kb-error.d.ts.map +1 -0
- package/dist/kb-mcp/lib/kb-error.js +28 -0
- package/dist/kb-mcp/lib/kb-error.js.map +1 -0
- package/dist/kb-mcp/lib/kb-locator.d.ts +58 -0
- package/dist/kb-mcp/lib/kb-locator.d.ts.map +1 -0
- package/dist/kb-mcp/lib/kb-locator.js +85 -0
- package/dist/kb-mcp/lib/kb-locator.js.map +1 -0
- package/dist/kb-mcp/lib/result-merger.d.ts +35 -0
- package/dist/kb-mcp/lib/result-merger.d.ts.map +1 -0
- package/dist/kb-mcp/lib/result-merger.js +64 -0
- package/dist/kb-mcp/lib/result-merger.js.map +1 -0
- package/dist/kb-mcp/server.d.ts +12 -0
- package/dist/kb-mcp/server.d.ts.map +1 -0
- package/dist/kb-mcp/server.js +29 -0
- package/dist/kb-mcp/server.js.map +1 -0
- package/dist/kb-mcp/tools/kb-api-lookup.d.ts +28 -0
- package/dist/kb-mcp/tools/kb-api-lookup.d.ts.map +1 -0
- package/dist/kb-mcp/tools/kb-api-lookup.js +237 -0
- package/dist/kb-mcp/tools/kb-api-lookup.js.map +1 -0
- package/dist/kb-mcp/tools/kb-doc-lookup.d.ts +20 -0
- package/dist/kb-mcp/tools/kb-doc-lookup.d.ts.map +1 -0
- package/dist/kb-mcp/tools/kb-doc-lookup.js +92 -0
- package/dist/kb-mcp/tools/kb-doc-lookup.js.map +1 -0
- package/dist/kb-mcp/tools/kb-search.d.ts +23 -0
- package/dist/kb-mcp/tools/kb-search.d.ts.map +1 -0
- package/dist/kb-mcp/tools/kb-search.js +126 -0
- package/dist/kb-mcp/tools/kb-search.js.map +1 -0
- package/dist/knowledge-graph/import-resolver.d.ts +11 -86
- package/dist/knowledge-graph/import-resolver.d.ts.map +1 -1
- package/dist/knowledge-graph/import-resolver.js +10 -333
- package/dist/knowledge-graph/import-resolver.js.map +1 -1
- package/dist/knowledge-graph/incremental.d.ts +25 -4
- package/dist/knowledge-graph/incremental.d.ts.map +1 -1
- package/dist/knowledge-graph/incremental.js +37 -15
- package/dist/knowledge-graph/incremental.js.map +1 -1
- package/dist/knowledge-graph/index.d.ts +11 -0
- package/dist/knowledge-graph/index.d.ts.map +1 -1
- package/dist/knowledge-graph/index.js +43 -1
- package/dist/knowledge-graph/index.js.map +1 -1
- package/dist/knowledge-graph/module-derivation.d.ts +14 -0
- package/dist/knowledge-graph/module-derivation.d.ts.map +1 -1
- package/dist/knowledge-graph/module-derivation.js +44 -44
- package/dist/knowledge-graph/module-derivation.js.map +1 -1
- package/dist/knowledge-graph/persistence.d.ts +56 -16
- package/dist/knowledge-graph/persistence.d.ts.map +1 -1
- package/dist/knowledge-graph/persistence.js +86 -32
- package/dist/knowledge-graph/persistence.js.map +1 -1
- package/dist/knowledge-graph/query-helpers.d.ts +39 -12
- package/dist/knowledge-graph/query-helpers.d.ts.map +1 -1
- package/dist/knowledge-graph/query-helpers.js +199 -34
- package/dist/knowledge-graph/query-helpers.js.map +1 -1
- package/dist/knowledge-graph/relativize.d.ts +51 -0
- package/dist/knowledge-graph/relativize.d.ts.map +1 -0
- package/dist/knowledge-graph/relativize.js +104 -0
- package/dist/knowledge-graph/relativize.js.map +1 -0
- package/dist/mcp/agent-context-tools.d.ts +3 -54
- package/dist/mcp/agent-context-tools.d.ts.map +1 -1
- package/dist/mcp/agent-context-tools.js +141 -189
- package/dist/mcp/agent-context-tools.js.map +1 -1
- package/dist/mcp/file-nav-tools.d.ts +42 -0
- package/dist/mcp/file-nav-tools.d.ts.map +1 -0
- package/dist/mcp/file-nav-tools.js +318 -0
- package/dist/mcp/file-nav-tools.js.map +1 -0
- package/dist/mcp/graph-tools.d.ts +2 -0
- package/dist/mcp/graph-tools.d.ts.map +1 -1
- package/dist/mcp/graph-tools.js +134 -123
- package/dist/mcp/graph-tools.js.map +1 -1
- package/dist/mcp/lib/file-nav-helpers.d.ts +128 -0
- package/dist/mcp/lib/file-nav-helpers.d.ts.map +1 -0
- package/dist/mcp/lib/file-nav-helpers.js +305 -0
- package/dist/mcp/lib/file-nav-helpers.js.map +1 -0
- package/dist/mcp/lib/telemetry.d.ts +69 -0
- package/dist/mcp/lib/telemetry.d.ts.map +1 -0
- package/dist/mcp/lib/telemetry.js +113 -0
- package/dist/mcp/lib/telemetry.js.map +1 -0
- package/dist/mcp/lib/tool-response.d.ts +46 -0
- package/dist/mcp/lib/tool-response.d.ts.map +1 -0
- package/dist/mcp/lib/tool-response.js +83 -0
- package/dist/mcp/lib/tool-response.js.map +1 -0
- package/dist/mcp/server.d.ts +13 -2
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +209 -150
- package/dist/mcp/server.js.map +1 -1
- package/dist/models/code-skeleton.d.ts +4 -2
- package/dist/models/code-skeleton.d.ts.map +1 -1
- package/dist/models/code-skeleton.js +2 -1
- package/dist/models/code-skeleton.js.map +1 -1
- package/dist/models/module-spec.d.ts +18 -0
- package/dist/models/module-spec.d.ts.map +1 -1
- package/dist/models/module-spec.js +6 -0
- package/dist/models/module-spec.js.map +1 -1
- package/dist/panoramic/builders/doc-graph-builder.d.ts +13 -0
- package/dist/panoramic/builders/doc-graph-builder.d.ts.map +1 -1
- package/dist/panoramic/builders/doc-graph-builder.js +20 -0
- package/dist/panoramic/builders/doc-graph-builder.js.map +1 -1
- package/dist/panoramic/graph/graph-builder.d.ts +72 -2
- package/dist/panoramic/graph/graph-builder.d.ts.map +1 -1
- package/dist/panoramic/graph/graph-builder.js +257 -51
- package/dist/panoramic/graph/graph-builder.js.map +1 -1
- package/dist/panoramic/graph/graph-query.d.ts +16 -0
- package/dist/panoramic/graph/graph-query.d.ts.map +1 -1
- package/dist/panoramic/graph/graph-query.js +49 -1
- package/dist/panoramic/graph/graph-query.js.map +1 -1
- package/dist/panoramic/graph/index.d.ts +2 -1
- package/dist/panoramic/graph/index.d.ts.map +1 -1
- package/dist/panoramic/graph/index.js +1 -1
- package/dist/panoramic/graph/index.js.map +1 -1
- package/dist/panoramic/pipelines/adr-evidence-verifier.d.ts.map +1 -1
- package/dist/panoramic/pipelines/adr-evidence-verifier.js +1 -29
- package/dist/panoramic/pipelines/adr-evidence-verifier.js.map +1 -1
- package/dist/panoramic/query.d.ts +1 -0
- package/dist/panoramic/query.d.ts.map +1 -1
- package/dist/panoramic/query.js +5 -0
- package/dist/panoramic/query.js.map +1 -1
- package/dist/scaffold-kb/api-entities-serializer.d.ts +12 -0
- package/dist/scaffold-kb/api-entities-serializer.d.ts.map +1 -0
- package/dist/scaffold-kb/api-entities-serializer.js +144 -0
- package/dist/scaffold-kb/api-entities-serializer.js.map +1 -0
- package/dist/scaffold-kb/arbitration.d.ts +44 -0
- package/dist/scaffold-kb/arbitration.d.ts.map +1 -0
- package/dist/scaffold-kb/arbitration.js +144 -0
- package/dist/scaffold-kb/arbitration.js.map +1 -0
- package/dist/scaffold-kb/chunk-splitter.d.ts +20 -0
- package/dist/scaffold-kb/chunk-splitter.d.ts.map +1 -0
- package/dist/scaffold-kb/chunk-splitter.js +309 -0
- package/dist/scaffold-kb/chunk-splitter.js.map +1 -0
- package/dist/scaffold-kb/doc-graph-builder.d.ts +26 -0
- package/dist/scaffold-kb/doc-graph-builder.d.ts.map +1 -0
- package/dist/scaffold-kb/doc-graph-builder.js +72 -0
- package/dist/scaffold-kb/doc-graph-builder.js.map +1 -0
- package/dist/scaffold-kb/entity-extractor.d.ts +40 -0
- package/dist/scaffold-kb/entity-extractor.d.ts.map +1 -0
- package/dist/scaffold-kb/entity-extractor.js +0 -0
- package/dist/scaffold-kb/entity-extractor.js.map +1 -0
- package/dist/scaffold-kb/entity-heuristic.d.ts +14 -0
- package/dist/scaffold-kb/entity-heuristic.d.ts.map +1 -0
- package/dist/scaffold-kb/entity-heuristic.js +107 -0
- package/dist/scaffold-kb/entity-heuristic.js.map +1 -0
- package/dist/scaffold-kb/entity-matcher.d.ts +20 -0
- package/dist/scaffold-kb/entity-matcher.d.ts.map +1 -0
- package/dist/scaffold-kb/entity-matcher.js +53 -0
- package/dist/scaffold-kb/entity-matcher.js.map +1 -0
- package/dist/scaffold-kb/entity-util.d.ts +11 -0
- package/dist/scaffold-kb/entity-util.d.ts.map +1 -0
- package/dist/scaffold-kb/entity-util.js +23 -0
- package/dist/scaffold-kb/entity-util.js.map +1 -0
- package/dist/scaffold-kb/evidence-envelope.d.ts +17 -0
- package/dist/scaffold-kb/evidence-envelope.d.ts.map +1 -0
- package/dist/scaffold-kb/evidence-envelope.js +32 -0
- package/dist/scaffold-kb/evidence-envelope.js.map +1 -0
- package/dist/scaffold-kb/index.d.ts +20 -0
- package/dist/scaffold-kb/index.d.ts.map +1 -0
- package/dist/scaffold-kb/index.js +102 -0
- package/dist/scaffold-kb/index.js.map +1 -0
- package/dist/scaffold-kb/ingest/ingest-core.d.ts +47 -0
- package/dist/scaffold-kb/ingest/ingest-core.d.ts.map +1 -0
- package/dist/scaffold-kb/ingest/ingest-core.js +231 -0
- package/dist/scaffold-kb/ingest/ingest-core.js.map +1 -0
- package/dist/scaffold-kb/ingest/office-parser.d.ts +33 -0
- package/dist/scaffold-kb/ingest/office-parser.d.ts.map +1 -0
- package/dist/scaffold-kb/ingest/office-parser.js +187 -0
- package/dist/scaffold-kb/ingest/office-parser.js.map +1 -0
- package/dist/scaffold-kb/ingest/url-fetcher.d.ts +43 -0
- package/dist/scaffold-kb/ingest/url-fetcher.d.ts.map +1 -0
- package/dist/scaffold-kb/ingest/url-fetcher.js +245 -0
- package/dist/scaffold-kb/ingest/url-fetcher.js.map +1 -0
- package/dist/scaffold-kb/ingester.d.ts +38 -0
- package/dist/scaffold-kb/ingester.d.ts.map +1 -0
- package/dist/scaffold-kb/ingester.js +261 -0
- package/dist/scaffold-kb/ingester.js.map +1 -0
- package/dist/scaffold-kb/injection-format.d.ts +25 -0
- package/dist/scaffold-kb/injection-format.d.ts.map +1 -0
- package/dist/scaffold-kb/injection-format.js +55 -0
- package/dist/scaffold-kb/injection-format.js.map +1 -0
- package/dist/scaffold-kb/kb-writer.d.ts +13 -0
- package/dist/scaffold-kb/kb-writer.d.ts.map +1 -0
- package/dist/scaffold-kb/kb-writer.js +69 -0
- package/dist/scaffold-kb/kb-writer.js.map +1 -0
- package/dist/scaffold-kb/keyword-extract.d.ts +16 -0
- package/dist/scaffold-kb/keyword-extract.d.ts.map +1 -0
- package/dist/scaffold-kb/keyword-extract.js +49 -0
- package/dist/scaffold-kb/keyword-extract.js.map +1 -0
- package/dist/scaffold-kb/query-sanitizer.d.ts +28 -0
- package/dist/scaffold-kb/query-sanitizer.d.ts.map +1 -0
- package/dist/scaffold-kb/query-sanitizer.js +38 -0
- package/dist/scaffold-kb/query-sanitizer.js.map +1 -0
- package/dist/scaffold-kb/recall-eval.d.ts +48 -0
- package/dist/scaffold-kb/recall-eval.d.ts.map +1 -0
- package/dist/scaffold-kb/recall-eval.js +55 -0
- package/dist/scaffold-kb/recall-eval.js.map +1 -0
- package/dist/scaffold-kb/schema-compat.d.ts +22 -0
- package/dist/scaffold-kb/schema-compat.d.ts.map +1 -0
- package/dist/scaffold-kb/schema-compat.js +32 -0
- package/dist/scaffold-kb/schema-compat.js.map +1 -0
- package/dist/scaffold-kb/search-core.d.ts +42 -0
- package/dist/scaffold-kb/search-core.d.ts.map +1 -0
- package/dist/scaffold-kb/search-core.js +89 -0
- package/dist/scaffold-kb/search-core.js.map +1 -0
- package/dist/scaffold-kb/sqlite-engine.d.ts +58 -0
- package/dist/scaffold-kb/sqlite-engine.d.ts.map +1 -0
- package/dist/scaffold-kb/sqlite-engine.js +61 -0
- package/dist/scaffold-kb/sqlite-engine.js.map +1 -0
- package/dist/scaffold-kb/sqlite-writer.d.ts +18 -0
- package/dist/scaffold-kb/sqlite-writer.d.ts.map +1 -0
- package/dist/scaffold-kb/sqlite-writer.js +76 -0
- package/dist/scaffold-kb/sqlite-writer.js.map +1 -0
- package/dist/scaffold-kb/tokenizer.d.ts +26 -0
- package/dist/scaffold-kb/tokenizer.d.ts.map +1 -0
- package/dist/scaffold-kb/tokenizer.js +97 -0
- package/dist/scaffold-kb/tokenizer.js.map +1 -0
- package/dist/scaffold-kb/types.d.ts +157 -0
- package/dist/scaffold-kb/types.d.ts.map +1 -0
- package/dist/scaffold-kb/types.js +5 -0
- package/dist/scaffold-kb/types.js.map +1 -0
- package/dist/utils/file-scanner.d.ts +15 -0
- package/dist/utils/file-scanner.d.ts.map +1 -1
- package/dist/utils/file-scanner.js +17 -0
- package/dist/utils/file-scanner.js.map +1 -1
- package/dist/utils/string-distance.d.ts +20 -0
- package/dist/utils/string-distance.d.ts.map +1 -0
- package/dist/utils/string-distance.js +38 -0
- package/dist/utils/string-distance.js.map +1 -0
- package/package.json +11 -2
- package/plugins/demo-kb-en/.claude-plugin/plugin.json +11 -0
- package/plugins/demo-kb-en/.mcp.json +8 -0
- package/plugins/demo-kb-en/FIXTURE.json +23 -0
- package/plugins/demo-kb-en/FIXTURE.md +53 -0
- package/plugins/demo-kb-en/ingest-samples/meeting-notes.md +13 -0
- package/plugins/demo-kb-en/kb/api-entities.json +3445 -0
- package/plugins/demo-kb-en/kb/chunks.sqlite +0 -0
- package/plugins/demo-kb-en/kb/doc-graph.json +113 -0
- package/plugins/demo-kb-zh/.claude-plugin/plugin.json +11 -0
- package/plugins/demo-kb-zh/.mcp.json +8 -0
- package/plugins/demo-kb-zh/FIXTURE.json +23 -0
- package/plugins/demo-kb-zh/FIXTURE.md +63 -0
- package/plugins/demo-kb-zh/ingest-samples/meeting-notes.md +13 -0
- package/plugins/demo-kb-zh/kb/api-entities.json +990 -0
- package/plugins/demo-kb-zh/kb/chunks.sqlite +0 -0
- package/plugins/demo-kb-zh/kb/doc-graph.json +113 -0
- package/plugins/spec-driver/.claude-plugin/plugin.json +1 -1
- package/plugins/spec-driver/README.md +33 -1
- package/plugins/spec-driver/agents/verify.md +88 -0
- package/plugins/spec-driver/config/orchestration.yaml +39 -31
- package/plugins/spec-driver/contracts/orchestration-overrides-contract.yaml +27 -0
- package/plugins/spec-driver/contracts/orchestration-schema.mjs +272 -227
- package/plugins/spec-driver/contracts/wrapper-source-of-truth.yaml +3 -0
- package/plugins/spec-driver/hooks/hooks.json +9 -0
- package/plugins/spec-driver/hooks/stop-fix-compliance-check.sh +36 -0
- package/plugins/spec-driver/lib/delegation-contract.mjs +84 -0
- package/plugins/spec-driver/lib/orchestration-resolver.mjs +49 -0
- package/plugins/spec-driver/lib/orchestrator.mjs +29 -1
- package/plugins/spec-driver/scripts/codex-skills.sh +28 -28
- package/plugins/spec-driver/scripts/dev/spike-fix-compliance-e2e.mjs +130 -0
- package/plugins/spec-driver/scripts/fix-compliance-judge.mjs +415 -0
- package/plugins/spec-driver/scripts/generate-adoption-insights.mjs +48 -1
- package/plugins/spec-driver/scripts/goal-loop-cli.mjs +307 -0
- package/plugins/spec-driver/scripts/init-project.sh +56 -0
- package/plugins/spec-driver/scripts/kb-prequery.mjs +125 -0
- package/plugins/spec-driver/scripts/lib/config-schema.mjs +200 -94
- package/plugins/spec-driver/scripts/lib/ensure-gitignore.sh +277 -0
- package/plugins/spec-driver/scripts/lib/extract-wrapper-body.mjs +136 -0
- package/plugins/spec-driver/scripts/lib/fix-compliance-core.mjs +434 -0
- package/plugins/spec-driver/scripts/lib/fix-compliance-io.mjs +335 -0
- package/plugins/spec-driver/scripts/lib/goal-loop-core.mjs +783 -0
- package/plugins/spec-driver/scripts/lib/init-project-output.sh +28 -0
- package/plugins/spec-driver/scripts/lib/load-zod.mjs +62 -0
- package/plugins/spec-driver/scripts/lib/project-profile-resolver.mjs +141 -49
- package/plugins/spec-driver/scripts/lib/project-profile-schema.mjs +88 -54
- package/plugins/spec-driver/scripts/postinstall.sh +13 -1
- package/plugins/spec-driver/scripts/record-workflow-run.mjs +75 -0
- package/plugins/spec-driver/scripts/sync-delegation-contract.mjs +187 -0
- package/plugins/spec-driver/scripts/validate-orchestrator-models.mjs +156 -0
- package/plugins/spec-driver/scripts/validate-wrapper-sources.mjs +46 -1
- package/plugins/spec-driver/skills/spec-driver-feature/SKILL.md +292 -0
- package/plugins/spec-driver/skills/spec-driver-fix/SKILL.md +87 -8
- package/plugins/spec-driver/skills/spec-driver-implement/SKILL.md +8 -0
- package/plugins/spec-driver/skills/spec-driver-resume/SKILL.md +12 -1
- package/plugins/spec-driver/skills/spec-driver-story/SKILL.md +20 -0
- package/plugins/spec-driver/templates/delegation-contract.md +27 -0
- package/plugins/spec-driver/templates/goal-loop-override-template.yaml +284 -0
- package/plugins/spec-driver/templates/specify-base/project-context-template.yaml +10 -0
- package/plugins/spectra/.claude-plugin/plugin.json +1 -1
- package/plugins/spectra/README.md +1 -1
- package/templates/module-spec.hbs +1 -0
|
@@ -11,101 +11,156 @@
|
|
|
11
11
|
* - resolveEffectiveConfig() effective config 合并
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import {
|
|
14
|
+
import { loadZod } from './load-zod.mjs';
|
|
15
15
|
|
|
16
|
-
//
|
|
17
|
-
|
|
18
|
-
// ────────────────────────────────────────
|
|
19
|
-
|
|
20
|
-
const modelNameSchema = z.enum(['opus', 'sonnet', 'haiku'])
|
|
21
|
-
.or(z.string().min(1));
|
|
22
|
-
|
|
23
|
-
const agentOverrideSchema = z.object({
|
|
24
|
-
model: modelNameSchema.optional(),
|
|
25
|
-
}).strict();
|
|
26
|
-
|
|
27
|
-
const agentsSchema = z.record(
|
|
28
|
-
z.string(),
|
|
29
|
-
agentOverrideSchema,
|
|
30
|
-
).optional();
|
|
31
|
-
|
|
32
|
-
const aliasMapSchema = z.record(z.string(), z.string());
|
|
33
|
-
|
|
34
|
-
const modelCompatSchema = z.object({
|
|
35
|
-
runtime: z.enum(['auto', 'claude', 'codex']).default('auto'),
|
|
36
|
-
aliases: z.object({
|
|
37
|
-
codex: aliasMapSchema.optional(),
|
|
38
|
-
claude: aliasMapSchema.optional(),
|
|
39
|
-
}).optional(),
|
|
40
|
-
defaults: z.object({
|
|
41
|
-
codex: z.string().optional(),
|
|
42
|
-
claude: z.string().optional(),
|
|
43
|
-
}).optional(),
|
|
44
|
-
}).optional();
|
|
45
|
-
|
|
46
|
-
const codexSchema = z.object({
|
|
47
|
-
service_tier: z.enum(['fast', 'standard', 'flex']).default('fast'),
|
|
48
|
-
}).optional();
|
|
49
|
-
|
|
50
|
-
const codexThinkingSchema = z.object({
|
|
51
|
-
default_level: z.enum(['low', 'medium', 'high', 'xhigh']).default('xhigh'),
|
|
52
|
-
level_map: z.record(z.string(), z.enum(['low', 'medium', 'high', 'xhigh'])).optional(),
|
|
53
|
-
}).optional();
|
|
54
|
-
|
|
55
|
-
const researchSchema = z.object({
|
|
56
|
-
default_mode: z.enum([
|
|
57
|
-
'auto', 'full', 'tech-only', 'product-only',
|
|
58
|
-
'codebase-scan', 'skip', 'custom',
|
|
59
|
-
]).default('auto'),
|
|
60
|
-
custom_steps: z.array(z.string()).default([]),
|
|
61
|
-
}).optional();
|
|
62
|
-
|
|
63
|
-
const verificationSchema = z.object({
|
|
64
|
-
commands: z.record(z.string(), z.record(z.string(), z.string())).default({}),
|
|
65
|
-
monorepo: z.object({
|
|
66
|
-
enabled: z.boolean().default(true),
|
|
67
|
-
}).optional(),
|
|
68
|
-
timeout: z.number().int().positive().optional().default(300),
|
|
69
|
-
}).optional();
|
|
70
|
-
|
|
71
|
-
const gatePauseSchema = z.object({
|
|
72
|
-
pause: z.enum(['always', 'auto', 'on_failure']),
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
const gatesSchema = z.record(z.string(), gatePauseSchema).optional();
|
|
76
|
-
|
|
77
|
-
const qualityGatesSchema = z.object({
|
|
78
|
-
auto_continue_on_warning: z.boolean().default(true),
|
|
79
|
-
pause_on_critical: z.boolean().default(true),
|
|
80
|
-
}).optional();
|
|
81
|
-
|
|
82
|
-
const retrySchema = z.object({
|
|
83
|
-
max_attempts: z.number().int().min(0).max(10).default(2),
|
|
84
|
-
}).optional();
|
|
85
|
-
|
|
86
|
-
const progressSchema = z.object({
|
|
87
|
-
show_stage_progress: z.boolean().default(true),
|
|
88
|
-
show_stage_summary: z.boolean().default(true),
|
|
89
|
-
}).optional();
|
|
16
|
+
// 经共享 helper 同步加载 zod;缺失时 zodAvailable=false,模块加载不崩
|
|
17
|
+
const { z, available: zodAvailable } = loadZod();
|
|
90
18
|
|
|
91
19
|
// ────────────────────────────────────────
|
|
92
|
-
// 顶层 Schema
|
|
20
|
+
// 子 Schema + 顶层 Schema
|
|
93
21
|
// ────────────────────────────────────────
|
|
22
|
+
//
|
|
23
|
+
// 全部 schema 求值必须包进 zodAvailable 守卫:缺 zod 时模块体完全不触碰 z,
|
|
24
|
+
// 否则会从 MODULE_NOT_FOUND 退化为 ReferenceError —— 等于没修。
|
|
25
|
+
// ESM 语法限制 export const 不能进 if 块,故 specDriverConfigSchema 用 let + 末尾 export。
|
|
26
|
+
|
|
27
|
+
let specDriverConfigSchema = null;
|
|
28
|
+
|
|
29
|
+
if (zodAvailable) {
|
|
30
|
+
const modelNameSchema = z.enum(['opus', 'sonnet', 'haiku'])
|
|
31
|
+
.or(z.string().min(1));
|
|
32
|
+
|
|
33
|
+
const agentOverrideSchema = z.object({
|
|
34
|
+
model: modelNameSchema.optional(),
|
|
35
|
+
}).strict();
|
|
36
|
+
|
|
37
|
+
const agentsSchema = z.record(
|
|
38
|
+
z.string(),
|
|
39
|
+
agentOverrideSchema,
|
|
40
|
+
).optional();
|
|
41
|
+
|
|
42
|
+
const aliasMapSchema = z.record(z.string(), z.string());
|
|
43
|
+
|
|
44
|
+
const modelCompatSchema = z.object({
|
|
45
|
+
runtime: z.enum(['auto', 'claude', 'codex']).default('auto'),
|
|
46
|
+
aliases: z.object({
|
|
47
|
+
codex: aliasMapSchema.optional(),
|
|
48
|
+
claude: aliasMapSchema.optional(),
|
|
49
|
+
}).optional(),
|
|
50
|
+
defaults: z.object({
|
|
51
|
+
codex: z.string().optional(),
|
|
52
|
+
claude: z.string().optional(),
|
|
53
|
+
}).optional(),
|
|
54
|
+
}).optional();
|
|
55
|
+
|
|
56
|
+
const codexSchema = z.object({
|
|
57
|
+
service_tier: z.enum(['fast', 'standard', 'flex']).default('fast'),
|
|
58
|
+
}).optional();
|
|
59
|
+
|
|
60
|
+
const codexThinkingSchema = z.object({
|
|
61
|
+
default_level: z.enum(['low', 'medium', 'high', 'xhigh']).default('xhigh'),
|
|
62
|
+
level_map: z.record(z.string(), z.enum(['low', 'medium', 'high', 'xhigh'])).optional(),
|
|
63
|
+
}).optional();
|
|
64
|
+
|
|
65
|
+
const researchSchema = z.object({
|
|
66
|
+
default_mode: z.enum([
|
|
67
|
+
'auto', 'full', 'tech-only', 'product-only',
|
|
68
|
+
'codebase-scan', 'skip', 'custom',
|
|
69
|
+
]).default('auto'),
|
|
70
|
+
custom_steps: z.array(z.string()).default([]),
|
|
71
|
+
}).optional();
|
|
72
|
+
|
|
73
|
+
const verificationSchema = z.object({
|
|
74
|
+
commands: z.record(z.string(), z.record(z.string(), z.string())).default({}),
|
|
75
|
+
monorepo: z.object({
|
|
76
|
+
enabled: z.boolean().default(true),
|
|
77
|
+
}).optional(),
|
|
78
|
+
timeout: z.number().int().positive().optional().default(300),
|
|
79
|
+
}).optional();
|
|
80
|
+
|
|
81
|
+
const gatePauseSchema = z.object({
|
|
82
|
+
pause: z.enum(['always', 'auto', 'on_failure']),
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const gatesSchema = z.record(z.string(), gatePauseSchema).optional();
|
|
86
|
+
|
|
87
|
+
const qualityGatesSchema = z.object({
|
|
88
|
+
auto_continue_on_warning: z.boolean().default(true),
|
|
89
|
+
pause_on_critical: z.boolean().default(true),
|
|
90
|
+
}).optional();
|
|
91
|
+
|
|
92
|
+
const retrySchema = z.object({
|
|
93
|
+
max_attempts: z.number().int().min(0).max(10).default(2),
|
|
94
|
+
}).optional();
|
|
95
|
+
|
|
96
|
+
const progressSchema = z.object({
|
|
97
|
+
show_stage_progress: z.boolean().default(true),
|
|
98
|
+
show_stage_summary: z.boolean().default(true),
|
|
99
|
+
}).optional();
|
|
100
|
+
|
|
101
|
+
// goal_loop 段(Feature 201):feature mode goal_loop agent_mode 的预算/迭代配置。
|
|
102
|
+
// 用 .default({}) 而非 .optional():省略该段时 z.object 会用四项各自 default 填全,
|
|
103
|
+
// 使 validateConfig 输出始终含 goal_loop.* 全默认值(W2 修复——避免运行时读到 undefined 而非 5)。
|
|
104
|
+
// 四项均带 default —— 不破坏现有 config 校验(未声明该段的现有 config 仍通过,只是补默认)。
|
|
105
|
+
// 仅在 .specify/orchestration-overrides.yaml 启用 goal_loop 时被编排器读取(FR-005/006/007)。
|
|
106
|
+
const goalLoopSchema = z.object({
|
|
107
|
+
// 最大迭代轮数上限(FR-005),到顶且未达标则停止进 GATE_VERIFY
|
|
108
|
+
max_iterations: z.number().int().min(1).default(5),
|
|
109
|
+
// 连续无进展(五维 delta 全 0)轮数触发早停(FR-006)
|
|
110
|
+
no_progress_max_rounds: z.number().int().min(1).default(2),
|
|
111
|
+
// 单轮 verify 墙钟上限(秒,FR-007)——timeout 强制、可机器校验,主预算
|
|
112
|
+
max_verify_seconds: z.number().positive().default(300),
|
|
113
|
+
// 单轮编排器可见委派/调用数上限(FR-007)——best-effort 粗粒度安全网,辅助上限
|
|
114
|
+
max_tool_invocations: z.number().int().min(1).default(50),
|
|
115
|
+
// 命令集完整性校验(F204):full 报告必须包含这些 kind 类别的 PASS 命令才能 REACHED_GOAL。
|
|
116
|
+
// 默认 [](跳过校验,保向后兼容);项目级 opt-in:['build','test','lint','check']。
|
|
117
|
+
full_required_kinds: z.array(z.enum(['build', 'test', 'lint', 'check'])).default([]),
|
|
118
|
+
}).default({});
|
|
119
|
+
|
|
120
|
+
// Spectra batch 并发配置(Feature 146)。concurrency 的接受集与运行时
|
|
121
|
+
// src/config/spec-driver-config.ts:readBatchConcurrency 对齐:有限 number 或可解析为
|
|
122
|
+
// 有限数字的字符串(如 quoted "3")。整数化与越界裁剪交由运行时 normalizeConcurrency
|
|
123
|
+
// 统一处理,故此处不加 int()/positive(),避免 validate gate 与运行时对同一配置判定相反。
|
|
124
|
+
const concurrencySchema = z.union([
|
|
125
|
+
z.number().finite(),
|
|
126
|
+
z.string().refine(
|
|
127
|
+
(s) => { const t = s.trim(); return t.length > 0 && Number.isFinite(Number(t)); },
|
|
128
|
+
{ message: '必须是可解析为有限数字的字符串(如 "3")' },
|
|
129
|
+
),
|
|
130
|
+
]);
|
|
131
|
+
const batchSchema = z.object({
|
|
132
|
+
concurrency: concurrencySchema.optional(),
|
|
133
|
+
}).strict().optional();
|
|
134
|
+
|
|
135
|
+
// fix_compliance 段(Feature 208,FR-015):fix 模式流程依从性强制程度。
|
|
136
|
+
// 用 .default({}) 而非 .optional():省略该段时 enforcement 被 default 填为 'block',
|
|
137
|
+
// 使 validateConfig 输出始终含 fix_compliance.enforcement(与 goal_loop 同惯例)。
|
|
138
|
+
// 注意:Stop hook 判定路径刻意不 import 本模块(走 simple-yaml 非抛出式读取,见
|
|
139
|
+
// contracts/fix-compliance-config-field.md);本 schema 只服务 validate-config 声明式校验。
|
|
140
|
+
const fixComplianceSchema = z.object({
|
|
141
|
+
enforcement: z.enum(['block', 'warn', 'off']).default('block'),
|
|
142
|
+
}).default({});
|
|
143
|
+
|
|
144
|
+
specDriverConfigSchema = z.object({
|
|
145
|
+
preset: z.enum(['balanced', 'quality-first', 'cost-efficient']).default('balanced'),
|
|
146
|
+
batch: batchSchema,
|
|
147
|
+
agents: agentsSchema,
|
|
148
|
+
model_compat: modelCompatSchema,
|
|
149
|
+
codex: codexSchema,
|
|
150
|
+
codex_thinking: codexThinkingSchema,
|
|
151
|
+
research: researchSchema,
|
|
152
|
+
verification: verificationSchema,
|
|
153
|
+
quality_gates: qualityGatesSchema,
|
|
154
|
+
gate_policy: z.enum(['strict', 'balanced', 'autonomous']).default('balanced'),
|
|
155
|
+
gates: gatesSchema,
|
|
156
|
+
retry: retrySchema,
|
|
157
|
+
progress: progressSchema,
|
|
158
|
+
goal_loop: goalLoopSchema,
|
|
159
|
+
fix_compliance: fixComplianceSchema,
|
|
160
|
+
}).strict();
|
|
161
|
+
}
|
|
94
162
|
|
|
95
|
-
export
|
|
96
|
-
preset: z.enum(['balanced', 'quality-first', 'cost-efficient']).default('balanced'),
|
|
97
|
-
agents: agentsSchema,
|
|
98
|
-
model_compat: modelCompatSchema,
|
|
99
|
-
codex: codexSchema,
|
|
100
|
-
codex_thinking: codexThinkingSchema,
|
|
101
|
-
research: researchSchema,
|
|
102
|
-
verification: verificationSchema,
|
|
103
|
-
quality_gates: qualityGatesSchema,
|
|
104
|
-
gate_policy: z.enum(['strict', 'balanced', 'autonomous']).default('balanced'),
|
|
105
|
-
gates: gatesSchema,
|
|
106
|
-
retry: retrySchema,
|
|
107
|
-
progress: progressSchema,
|
|
108
|
-
}).strict();
|
|
163
|
+
export { specDriverConfigSchema, zodAvailable };
|
|
109
164
|
|
|
110
165
|
// ────────────────────────────────────────
|
|
111
166
|
// 常量
|
|
@@ -127,6 +182,16 @@ export const BUILTIN_DEFAULTS = {
|
|
|
127
182
|
'model_compat.runtime': 'auto',
|
|
128
183
|
'codex.service_tier': 'fast',
|
|
129
184
|
'codex_thinking.default_level': 'xhigh',
|
|
185
|
+
// goal_loop 默认值(Feature 201,W2 修复)——与 goalLoopSchema 各项 default 一致,
|
|
186
|
+
// 使 resolveEffectiveConfig 在 config 省略 goal_loop 段时也 surface 默认值。
|
|
187
|
+
'goal_loop.max_iterations': 5,
|
|
188
|
+
'goal_loop.no_progress_max_rounds': 2,
|
|
189
|
+
'goal_loop.max_verify_seconds': 300,
|
|
190
|
+
'goal_loop.max_tool_invocations': 50,
|
|
191
|
+
// F204:full 轮必需命令 kind 类别,默认 [](跳过校验,保向后兼容)。
|
|
192
|
+
'goal_loop.full_required_kinds': [],
|
|
193
|
+
// fix_compliance 默认值(Feature 208,FR-015)——block 为缺省强制程度。
|
|
194
|
+
'fix_compliance.enforcement': 'block',
|
|
130
195
|
};
|
|
131
196
|
|
|
132
197
|
/** preset 默认值表 */
|
|
@@ -231,9 +296,9 @@ export function suggestField(unknown, knownFields) {
|
|
|
231
296
|
|
|
232
297
|
/** 顶层 Schema 合法字段名列表 */
|
|
233
298
|
const KNOWN_TOP_LEVEL_FIELDS = [
|
|
234
|
-
'preset', 'agents', 'model_compat', 'codex', 'codex_thinking',
|
|
299
|
+
'preset', 'batch', 'agents', 'model_compat', 'codex', 'codex_thinking',
|
|
235
300
|
'research', 'verification', 'quality_gates', 'gate_policy',
|
|
236
|
-
'gates', 'retry', 'progress',
|
|
301
|
+
'gates', 'retry', 'progress', 'goal_loop', 'fix_compliance',
|
|
237
302
|
];
|
|
238
303
|
|
|
239
304
|
/**
|
|
@@ -249,6 +314,21 @@ export function validateConfig(parsedYaml) {
|
|
|
249
314
|
return { success: true, data: {}, diagnostics };
|
|
250
315
|
}
|
|
251
316
|
|
|
317
|
+
// 缺 zod 降级:无法做 schema 校验,best-effort 接受原样配置并标 degraded。
|
|
318
|
+
// success=true 保证 validate-config.mjs 不误判为校验失败退非 0(其退出码只看 error 级诊断)。
|
|
319
|
+
if (!zodAvailable) {
|
|
320
|
+
return {
|
|
321
|
+
success: true,
|
|
322
|
+
data: (parsedYaml && typeof parsedYaml === 'object') ? parsedYaml : {},
|
|
323
|
+
degraded: true,
|
|
324
|
+
diagnostics: [{
|
|
325
|
+
level: 'warning',
|
|
326
|
+
code: 'config.zod-unavailable',
|
|
327
|
+
message: '未能加载 zod,已跳过 spec-driver.config.yaml schema 校验并 best-effort 接受配置;如需完整校验请在已安装依赖的目录运行(npm i)或从仓内源路径运行',
|
|
328
|
+
}],
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
|
|
252
332
|
const result = specDriverConfigSchema.safeParse(parsedYaml);
|
|
253
333
|
|
|
254
334
|
if (result.success) {
|
|
@@ -302,11 +382,13 @@ export function validateConfig(parsedYaml) {
|
|
|
302
382
|
path,
|
|
303
383
|
});
|
|
304
384
|
} else if (issue.code === 'too_small' && issue.type === 'number') {
|
|
305
|
-
//
|
|
385
|
+
// 数值下界校验(如 timeout / goal_loop.* 预算项)。
|
|
386
|
+
// 部分字段允许小数(如 goal_loop.max_verify_seconds 用 z.number().positive()),
|
|
387
|
+
// 故文案统一用"正数"而非"正整数",避免误导(W3 修复)。
|
|
306
388
|
diagnostics.push({
|
|
307
389
|
level: 'error',
|
|
308
390
|
code: 'config.invalid-positive-int',
|
|
309
|
-
message: `\`${path}\`
|
|
391
|
+
message: `\`${path}\` 必须为正数,当前值: ${issue.minimum !== undefined ? `< ${issue.minimum}` : '非法'}`,
|
|
310
392
|
path,
|
|
311
393
|
});
|
|
312
394
|
} else {
|
|
@@ -389,6 +471,12 @@ export function resolveEffectiveConfig(options) {
|
|
|
389
471
|
'model_compat.runtime',
|
|
390
472
|
'codex.service_tier',
|
|
391
473
|
'codex_thinking.default_level',
|
|
474
|
+
'goal_loop.max_iterations',
|
|
475
|
+
'goal_loop.no_progress_max_rounds',
|
|
476
|
+
'goal_loop.max_verify_seconds',
|
|
477
|
+
'goal_loop.max_tool_invocations',
|
|
478
|
+
'goal_loop.full_required_kinds',
|
|
479
|
+
'fix_compliance.enforcement',
|
|
392
480
|
];
|
|
393
481
|
|
|
394
482
|
for (const dotPath of nestedKeys) {
|
|
@@ -400,6 +488,24 @@ export function resolveEffectiveConfig(options) {
|
|
|
400
488
|
}
|
|
401
489
|
}
|
|
402
490
|
|
|
491
|
+
// Spectra batch 子系统旋钮(Feature 146)——刻意与上方编排器 nestedKeys 分开处理。
|
|
492
|
+
// why 单独成块、且只在用户显式配置时才展示:
|
|
493
|
+
// 1) batch.concurrency 虽落在同一份 spec-driver.config.yaml,却由 Spectra batch 运行时消费,
|
|
494
|
+
// 而非 spec-driver 编排器;标注子系统来源以免被误读成编排器并发旋钮
|
|
495
|
+
// (编排器并发是 orchestration-overrides.yaml 的 parallel_scheduling.max_concurrent_tasks)。
|
|
496
|
+
// 2) 其默认值 3 的 canonical source 在 src/ 运行时侧(cli/commands/batch.ts:resolveBatchConcurrency
|
|
497
|
+
// 与 batch-orchestrator.ts 的 `?? 3`),不在本插件脚本;故刻意不进 BUILTIN_DEFAULTS——
|
|
498
|
+
// 在此 hardcode 3 会与运行时默认形成双源,一旦运行时漂移就显示过期值,比"不展示"更误导
|
|
499
|
+
// (见 111423f Codex R1 WARNING 2 的取舍)。
|
|
500
|
+
const batchConcurrency = getNestedValue(configYaml, 'batch.concurrency');
|
|
501
|
+
if (batchConcurrency !== undefined) {
|
|
502
|
+
entries.push({
|
|
503
|
+
key: 'batch.concurrency',
|
|
504
|
+
value: batchConcurrency,
|
|
505
|
+
source: 'config.yaml (spectra batch)',
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
|
|
403
509
|
// preset 默认值提供的 agent 模型配置
|
|
404
510
|
const presetDefaults = PRESET_DEFAULTS[effectivePreset] || {};
|
|
405
511
|
const agents = configYaml.agents || {};
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Spec Driver - ignore 自举共享库(Feature 207)
|
|
3
|
+
# 由 init-project.sh 与 postinstall.sh 共同 source。
|
|
4
|
+
# 职责:确保 Spec Driver 落盘产物的 4 条 ignore 规则就位,避免脚手架与本机绝对路径
|
|
5
|
+
# 污染宿主 repo 的 git diff / 交付 patch。提供两个入口:
|
|
6
|
+
# - ensure_spec_driver_gitignore → 写宿主 <project_root>/.gitignore(团队共享)
|
|
7
|
+
# - ensure_spec_driver_git_exclude → 写 <project_root>/.git/info/exclude(非 tracked,零 diff)
|
|
8
|
+
# 两者共享内部注入实现 _spec_driver_inject_entries,行为契约一致(幂等、CRLF/NUL/symlink/
|
|
9
|
+
# negation 防御、mkdir 原子锁保护并发)。
|
|
10
|
+
#
|
|
11
|
+
# 注意:本文件只定义函数,不含 set -e / 顶层执行逻辑,避免被 source 时影响调用方主流程。
|
|
12
|
+
# 函数内所有写入均显式检查返回值,不依赖 trap EXIT(避免污染被 source 进
|
|
13
|
+
# set -euo pipefail 调用方的退出钩子)。
|
|
14
|
+
|
|
15
|
+
# Spec Driver 落盘产物的固定 4 条 ignore 条目(与开发仓库 .gitignore 一致,顺序即注入顺序):
|
|
16
|
+
# .specify/.spec-driver-path — 机器绝对路径(隐私 / 可移植)
|
|
17
|
+
# .specify/runs/ — 本地运行态
|
|
18
|
+
# .specify/scorecards/ — 插件默认品复制,可再生
|
|
19
|
+
# .specify/templates/ — 插件默认品复制,可再生
|
|
20
|
+
# 写死为字面量作为唯一事实来源,不从外部读取,避免引入新的路径依赖。
|
|
21
|
+
SPEC_DRIVER_GITIGNORE_ENTRIES=(
|
|
22
|
+
".specify/.spec-driver-path"
|
|
23
|
+
".specify/runs/"
|
|
24
|
+
".specify/scorecards/"
|
|
25
|
+
".specify/templates/"
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
# ── 并发取舍说明(W1 修复,Codex 四轮)─────────────────────────────────────
|
|
29
|
+
# 本库刻意**不做 stale 锁抢占**:曾有的 mtime 阈值 + rmdir 重试逻辑存在 ABA 竞态——
|
|
30
|
+
# 预置过期锁 + 高并发时,多个进程会同判 stale、rmdir 互删对方刚建的新锁,导致多个进程
|
|
31
|
+
# 同时进入临界区重复注入。故抢锁失败一律直接 skipped:0。
|
|
32
|
+
# 残余风险:进程在临界区内被 SIGKILL → 孤儿锁使该 repo 永久 skip 注入。此风险经权衡接受:
|
|
33
|
+
# (1) 临界区为毫秒级窗口,命中概率极低;
|
|
34
|
+
# (2) 孤儿锁只造成静默跳过(主流程零影响,下次会话无法自愈但不报错),
|
|
35
|
+
# 危害远小于 ABA 重复写入把用户 .gitignore 堆出重复行。
|
|
36
|
+
# ─────────────────────────────────────────────────────────────────────────
|
|
37
|
+
|
|
38
|
+
# _spec_driver_has_nul <file>
|
|
39
|
+
# 检测文件是否含 NUL 字节(0x00)。含 NUL 的文件不是合法文本,装载为 shell 变量会截断,
|
|
40
|
+
# 无法可靠做整行匹配 → 一律拒写。跨平台实现:比较原始字节数与去除 NUL 后的字节数。
|
|
41
|
+
# 返回 0(真)表示含 NUL,返回 1(假)表示不含或读取失败。
|
|
42
|
+
_spec_driver_has_nul() {
|
|
43
|
+
local file="$1"
|
|
44
|
+
local raw stripped
|
|
45
|
+
raw="$(wc -c < "$file" 2>/dev/null)" || return 1
|
|
46
|
+
# LC_ALL=C 确保 tr 按字节而非多字节字符处理;去掉所有 NUL 后再计字节数
|
|
47
|
+
stripped="$(LC_ALL=C tr -d '\000' < "$file" 2>/dev/null | wc -c)" || return 1
|
|
48
|
+
# 归一化空白(wc 输出可能带前导空格)
|
|
49
|
+
raw="${raw//[[:space:]]/}"
|
|
50
|
+
stripped="${stripped//[[:space:]]/}"
|
|
51
|
+
[[ "$raw" != "$stripped" ]]
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
# _spec_driver_inject_entries <target_file> <with_header>
|
|
55
|
+
#
|
|
56
|
+
# 向 <target_file> 注入 SPEC_DRIVER_GITIGNORE_ENTRIES 中缺失的条目(幂等)。
|
|
57
|
+
# 调用方须在持锁状态下调用;本函数不负责加锁/解锁(由入口函数统一管理)。
|
|
58
|
+
#
|
|
59
|
+
# 参数:
|
|
60
|
+
# target_file - 目标 ignore 文件绝对路径(.gitignore 或 .git/info/exclude)
|
|
61
|
+
# with_header - "1" 表示新建文件时写注释头;"0" 表示不写(exclude 场景不带头)
|
|
62
|
+
#
|
|
63
|
+
# 行为契约:
|
|
64
|
+
# - target_file 为 symlink(含 dangling)→ 一律不写,stdout: failed:0
|
|
65
|
+
# (-L 检测必须先于 -f 分支:dangling symlink 时 [[ ! -f ]] 为真会走创建分支,
|
|
66
|
+
# `>` 重定向会跟随 symlink 在 symlink 指向的外部路径创建文件——安全隐患)
|
|
67
|
+
# - target_file 含 NUL 字节 → 拒写,stdout: failed:0(文件不被改动)
|
|
68
|
+
# - target_file 不存在 → 创建(with_header=1 时带注释头 + 4 行条目),stdout: created:N
|
|
69
|
+
# - target_file 存在但缺条目 → 精确整行匹配缺失项追加到末尾,stdout: appended:N
|
|
70
|
+
# - target_file 存在且 4 条全就位 → 不写文件(mtime 不变),stdout: ready:0
|
|
71
|
+
# - negation 尊重:归一化视图中若存在以 '!' 开头、去掉 '!' 后等于该条目或以该条目为
|
|
72
|
+
# 前缀的行(如 !.specify/templates/ 或 !.specify/templates/**),该条目跳过不追加
|
|
73
|
+
# (尊重用户显式 un-ignore 意图),跳过条目不计入 appended:N
|
|
74
|
+
# - 匹配采用 CRLF 归一化视图(tr -d '\r'):CRLF 行尾文件不被误判为缺失而重复追加;
|
|
75
|
+
# 但不改写原文件行尾
|
|
76
|
+
# - 写入失败(只读 FS 等)→ stdout: failed:0
|
|
77
|
+
# - 始终 return 0
|
|
78
|
+
_spec_driver_inject_entries() {
|
|
79
|
+
local target_file="$1"
|
|
80
|
+
local with_header="$2"
|
|
81
|
+
|
|
82
|
+
# symlink 防御(含 dangling):必须先于 -f 分支判断
|
|
83
|
+
if [[ -L "$target_file" ]]; then
|
|
84
|
+
printf 'failed:0\n'
|
|
85
|
+
return 0
|
|
86
|
+
fi
|
|
87
|
+
|
|
88
|
+
# NUL 防御:仅对已存在的普通文件检测(不存在时无从检测,创建分支会写纯文本)
|
|
89
|
+
if [[ -f "$target_file" ]] && _spec_driver_has_nul "$target_file"; then
|
|
90
|
+
printf 'failed:0\n'
|
|
91
|
+
return 0
|
|
92
|
+
fi
|
|
93
|
+
|
|
94
|
+
# 场景 1:文件不存在 → 创建新文件,stdout: created:N
|
|
95
|
+
if [[ ! -f "$target_file" ]]; then
|
|
96
|
+
# 关键 bash 地雷(W-A 修复):绝不用「复合命令组 { …; } > file」形式写入。
|
|
97
|
+
# 1. 命令组的重定向失败退出码在 `if ! { …; } > file` 形式下会被吞掉——
|
|
98
|
+
# 直接跑 rc=1,但 `if !` 包裹时不进 then 分支 → failed 分支永不触发 →
|
|
99
|
+
# 落到 created:N 误报(.git/info 被普通文件占用时 `> exclude` 必然失败)。
|
|
100
|
+
# 2. `> file 2>/dev/null` 中 stdout 重定向失败发生在 `2>/dev/null` 生效之前 →
|
|
101
|
+
# "Not a directory" 等 stderr 泄漏。
|
|
102
|
+
# 正解:内容先攒进变量,再用「简单命令」写入,且把 `2>/dev/null` 置于 `>` 之前
|
|
103
|
+
# 先吞 stderr。简单命令的 `if !` 重定向失败已实测能被捕获(bash 3.2 与 5.x 一致)。
|
|
104
|
+
local content=""
|
|
105
|
+
if [[ "$with_header" == "1" ]]; then
|
|
106
|
+
content="# Spec Driver 本地缓存与运行态(自动注入,可手动调整顺序)"$'\n'
|
|
107
|
+
fi
|
|
108
|
+
local entry
|
|
109
|
+
for entry in "${SPEC_DRIVER_GITIGNORE_ENTRIES[@]}"; do
|
|
110
|
+
content+="${entry}"$'\n'
|
|
111
|
+
done
|
|
112
|
+
if ! printf '%s' "$content" 2>/dev/null > "$target_file"; then
|
|
113
|
+
printf 'failed:0\n'
|
|
114
|
+
return 0
|
|
115
|
+
fi
|
|
116
|
+
printf 'created:%d\n' "${#SPEC_DRIVER_GITIGNORE_ENTRIES[@]}"
|
|
117
|
+
return 0
|
|
118
|
+
fi
|
|
119
|
+
|
|
120
|
+
# 场景 2/3:文件已存在 → 逐条精确整行匹配(CRLF 归一化视图),收集缺失项
|
|
121
|
+
# tr -d '\r' 剥离视图内的 CR,使 CRLF 行尾文件不被误判为缺失;不改写原文件。
|
|
122
|
+
local normalized_view
|
|
123
|
+
normalized_view="$(tr -d '\r' < "$target_file" 2>/dev/null)" || normalized_view=""
|
|
124
|
+
local missing=()
|
|
125
|
+
local entry
|
|
126
|
+
for entry in "${SPEC_DRIVER_GITIGNORE_ENTRIES[@]}"; do
|
|
127
|
+
# -F 纯字符串、-x 整行匹配:避免 .specify/runs/ 被 .specify/runs/xxx 宽松误判为已存在
|
|
128
|
+
# 用 here-string 而非管道喂 grep:调用方多为 set -o pipefail,大文件(>64KB pipe buffer)
|
|
129
|
+
# 且目标条目在文件前部时,grep -q 匹配即退会令 printf 写端收 SIGPIPE,管道退出码 141
|
|
130
|
+
# 被 pipefail 放大成整体失败 → 已存在条目被误判缺失而重复追加。here-string 无管道,规避此问题。
|
|
131
|
+
if grep -qxF -- "$entry" <<< "$normalized_view"; then
|
|
132
|
+
continue
|
|
133
|
+
fi
|
|
134
|
+
# negation 冲突检测:用户显式 un-ignore(! 前缀)时尊重其意图,跳过该条目。
|
|
135
|
+
# 匹配 !<entry> 精确等,或 !<entry>* 前缀形式(如 !.specify/templates/**)。
|
|
136
|
+
if _spec_driver_has_negation "$normalized_view" "$entry"; then
|
|
137
|
+
continue
|
|
138
|
+
fi
|
|
139
|
+
missing+=("$entry")
|
|
140
|
+
done
|
|
141
|
+
|
|
142
|
+
# 全部就位(或被 negation 尊重跳过)→ 不触碰文件(幂等契约:mtime 不变),stdout: ready:0
|
|
143
|
+
if [[ ${#missing[@]} -eq 0 ]]; then
|
|
144
|
+
printf 'ready:0\n'
|
|
145
|
+
return 0
|
|
146
|
+
fi
|
|
147
|
+
|
|
148
|
+
# 部分缺失 → 追加缺失条目到文件末尾(不带注释头,追加场景保持幂等最简)
|
|
149
|
+
# 末尾无换行边界:若最后一个字节非换行符,先补一个 \n 再追加,避免与最后一行粘连
|
|
150
|
+
if [[ -s "$target_file" ]]; then
|
|
151
|
+
local last_char
|
|
152
|
+
last_char="$(tail -c 1 "$target_file" 2>/dev/null)"
|
|
153
|
+
if [[ -n "$last_char" ]]; then
|
|
154
|
+
# 2>/dev/null 置于 >> 之前先吞 stderr(同 W-A:避免重定向失败时 "Not a directory" 泄漏)
|
|
155
|
+
if ! printf '\n' 2>/dev/null >> "$target_file"; then
|
|
156
|
+
printf 'failed:0\n'
|
|
157
|
+
return 0
|
|
158
|
+
fi
|
|
159
|
+
fi
|
|
160
|
+
fi
|
|
161
|
+
|
|
162
|
+
for entry in "${missing[@]}"; do
|
|
163
|
+
# 同上:2>/dev/null 前置于 >>,消除 stderr 泄漏
|
|
164
|
+
if ! printf '%s\n' "$entry" 2>/dev/null >> "$target_file"; then
|
|
165
|
+
printf 'failed:0\n'
|
|
166
|
+
return 0
|
|
167
|
+
fi
|
|
168
|
+
done
|
|
169
|
+
|
|
170
|
+
printf 'appended:%d\n' "${#missing[@]}"
|
|
171
|
+
return 0
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
# _spec_driver_has_negation <normalized_view> <entry>
|
|
175
|
+
# 判断归一化视图中是否存在尊重该条目的 negation 行(以 '!' 开头)。
|
|
176
|
+
# 命中形式:!<entry>(精确)或 !<entry> 后接任意字符(前缀,如 !.specify/templates/**)。
|
|
177
|
+
# 返回 0(真)表示存在 negation,返回 1(假)表示不存在。
|
|
178
|
+
_spec_driver_has_negation() {
|
|
179
|
+
local view="$1"
|
|
180
|
+
local entry="$2"
|
|
181
|
+
local line stripped
|
|
182
|
+
while IFS= read -r line; do
|
|
183
|
+
[[ "$line" == '!'* ]] || continue
|
|
184
|
+
stripped="${line#!}"
|
|
185
|
+
# 精确等 或 以 entry 为前缀(覆盖 !<entry> / !<entry>** / !<entry>foo)
|
|
186
|
+
if [[ "$stripped" == "$entry" || "$stripped" == "$entry"* ]]; then
|
|
187
|
+
return 0
|
|
188
|
+
fi
|
|
189
|
+
done <<< "$view"
|
|
190
|
+
return 1
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
# _spec_driver_acquire_lock <lock_dir>
|
|
194
|
+
# 尝试获取 mkdir 原子锁。成功返回 0;失败(已被占用)返回 1。
|
|
195
|
+
# 不做 stale 抢占(见文件顶部并发取舍说明)。
|
|
196
|
+
_spec_driver_acquire_lock() {
|
|
197
|
+
mkdir "$1" 2>/dev/null
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
# ensure_spec_driver_gitignore <project_root>
|
|
201
|
+
#
|
|
202
|
+
# 写宿主 <project_root>/.gitignore(团队共享的 ignore 配置)。
|
|
203
|
+
# 行为契约见 plan.md §1.1:
|
|
204
|
+
# - 三态输出 created:N / appended:N / ready:0 / failed:0 / skipped:0
|
|
205
|
+
# - symlink / NUL / negation 防御见 _spec_driver_inject_entries
|
|
206
|
+
# - 非 git 目录也照常写入(用户后续 git init 即生效)
|
|
207
|
+
# - 并发:mkdir 原子锁保护临界区;抢锁失败 → skipped:0(不 stale 抢占)
|
|
208
|
+
# - 始终 return 0;仅 project_root 为空或非目录时 return 1
|
|
209
|
+
ensure_spec_driver_gitignore() {
|
|
210
|
+
local project_root="$1"
|
|
211
|
+
if [[ -z "$project_root" || ! -d "$project_root" ]]; then
|
|
212
|
+
return 1
|
|
213
|
+
fi
|
|
214
|
+
|
|
215
|
+
local gitignore_file="${project_root}/.gitignore"
|
|
216
|
+
local lock_dir="${project_root}/.specify/.ensure-gitignore.lock"
|
|
217
|
+
|
|
218
|
+
# 锁父路径 .specify/ 兜底创建(.specify/ 是 Spec Driver 自身域,不越界)
|
|
219
|
+
mkdir -p "${project_root}/.specify" 2>/dev/null || true
|
|
220
|
+
|
|
221
|
+
if ! _spec_driver_acquire_lock "$lock_dir"; then
|
|
222
|
+
printf 'skipped:0\n'
|
|
223
|
+
return 0
|
|
224
|
+
fi
|
|
225
|
+
|
|
226
|
+
local result
|
|
227
|
+
result="$(_spec_driver_inject_entries "$gitignore_file" "1")"
|
|
228
|
+
rmdir "$lock_dir" 2>/dev/null || true
|
|
229
|
+
printf '%s\n' "$result"
|
|
230
|
+
return 0
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
# ensure_spec_driver_git_exclude <project_root>
|
|
234
|
+
#
|
|
235
|
+
# 写 <project_root>/.git/info/exclude(Git 原生的非 tracked 项目级 ignore)。
|
|
236
|
+
# 动机:用户 repo 的 .gitignore 几乎都被 track,注入产生 `M .gitignore` 仍进 patch;
|
|
237
|
+
# 写 .git/info/exclude 零 diff 污染。这是分层防线的主防线。
|
|
238
|
+
# 行为契约:
|
|
239
|
+
# - 仅当 <project_root>/.git 是**目录**时执行;.git 为文件(worktree/submodule 的
|
|
240
|
+
# gitdir 指针)→ skipped:0(gitdir 解析复杂且 exclude 共享主仓库,.gitignore 路径覆盖)
|
|
241
|
+
# - 非 git repo(无 .git)→ skipped:0
|
|
242
|
+
# - .git/info/ 不存在则 mkdir -p 创建
|
|
243
|
+
# - 三态输出与防御同 _spec_driver_inject_entries(不带注释头)
|
|
244
|
+
# - 复用同一把锁(与 gitignore 各自独立拿/放锁,无死锁)
|
|
245
|
+
# - 始终 return 0;仅 project_root 为空或非目录时 return 1
|
|
246
|
+
ensure_spec_driver_git_exclude() {
|
|
247
|
+
local project_root="$1"
|
|
248
|
+
if [[ -z "$project_root" || ! -d "$project_root" ]]; then
|
|
249
|
+
return 1
|
|
250
|
+
fi
|
|
251
|
+
|
|
252
|
+
local git_dir="${project_root}/.git"
|
|
253
|
+
# .git 必须是目录:非 git repo 或 worktree/submodule(.git 为文件)→ 跳过
|
|
254
|
+
if [[ ! -d "$git_dir" ]]; then
|
|
255
|
+
printf 'skipped:0\n'
|
|
256
|
+
return 0
|
|
257
|
+
fi
|
|
258
|
+
|
|
259
|
+
local exclude_file="${git_dir}/info/exclude"
|
|
260
|
+
local lock_dir="${project_root}/.specify/.ensure-gitignore.lock"
|
|
261
|
+
|
|
262
|
+
# info/ 目录兜底创建(标准 git repo 一般已有,但防御缺失)
|
|
263
|
+
mkdir -p "${git_dir}/info" 2>/dev/null || true
|
|
264
|
+
# 锁父路径 .specify/ 兜底创建
|
|
265
|
+
mkdir -p "${project_root}/.specify" 2>/dev/null || true
|
|
266
|
+
|
|
267
|
+
if ! _spec_driver_acquire_lock "$lock_dir"; then
|
|
268
|
+
printf 'skipped:0\n'
|
|
269
|
+
return 0
|
|
270
|
+
fi
|
|
271
|
+
|
|
272
|
+
local result
|
|
273
|
+
result="$(_spec_driver_inject_entries "$exclude_file" "0")"
|
|
274
|
+
rmdir "$lock_dir" 2>/dev/null || true
|
|
275
|
+
printf '%s\n' "$result"
|
|
276
|
+
return 0
|
|
277
|
+
}
|