xcoder-dna 0.0.0-stage → 1.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/LICENSE +66 -0
- package/README.md +87 -2
- package/dist/agents/ai-chat-debate.d.ts +36 -0
- package/dist/agents/ai-chat-debate.js +300 -0
- package/dist/agents/ai-chat-report-writer.d.ts +7 -0
- package/dist/agents/ai-chat-report-writer.js +100 -0
- package/dist/agents/duplicate-detector.d.ts +41 -0
- package/dist/agents/duplicate-detector.js +65 -0
- package/dist/agents/genesis-agent.d.ts +61 -0
- package/dist/agents/genesis-agent.js +340 -0
- package/dist/agents/samurai-agent.d.ts +53 -0
- package/dist/agents/samurai-agent.js +279 -0
- package/dist/agents/sub-agent.d.ts +56 -0
- package/dist/agents/sub-agent.js +353 -0
- package/dist/backlog/backlog-requirement-validator.d.ts +10 -0
- package/dist/backlog/backlog-requirement-validator.js +60 -0
- package/dist/backlog/backlog-store.d.ts +29 -0
- package/dist/backlog/backlog-store.js +148 -0
- package/dist/backlog/po-agent.d.ts +16 -0
- package/dist/backlog/po-agent.js +121 -0
- package/dist/backlog/types.d.ts +25 -0
- package/dist/backlog/types.js +4 -0
- package/dist/bench/run.d.ts +11 -0
- package/dist/bench/run.js +185 -0
- package/dist/bench/types.d.ts +50 -0
- package/dist/bench/types.js +1 -0
- package/dist/cli/swarm.d.ts +10 -0
- package/dist/cli/swarm.js +100 -0
- package/dist/cli/task.d.ts +14 -0
- package/dist/cli/task.js +121 -0
- package/dist/cli/wbc-arg.d.ts +6 -0
- package/dist/cli/wbc-arg.js +40 -0
- package/dist/cli/wbc.d.ts +45 -0
- package/dist/cli/wbc.js +98 -0
- package/dist/commands/license.d.ts +9 -0
- package/dist/commands/license.js +140 -0
- package/dist/commands/release.d.ts +44 -0
- package/dist/commands/release.js +180 -0
- package/dist/commands/test.d.ts +50 -0
- package/dist/commands/test.js +385 -0
- package/dist/config/loadConfig.d.ts +82 -0
- package/dist/config/loadConfig.js +240 -0
- package/dist/config/secure-mode.d.ts +29 -0
- package/dist/config/secure-mode.js +63 -0
- package/dist/config/user-config.d.ts +39 -0
- package/dist/config/user-config.js +202 -0
- package/dist/engine/reasoning-engine.d.ts +21 -0
- package/dist/engine/reasoning-engine.js +59 -0
- package/dist/events/bus.d.ts +22 -0
- package/dist/events/bus.js +18 -0
- package/dist/index.d.ts +79 -0
- package/dist/index.js +77 -0
- package/dist/indexing/ignoreRules.d.ts +1 -0
- package/dist/indexing/ignoreRules.js +53 -0
- package/dist/instance-swarm/instance-swarm-orchestrator.d.ts +57 -0
- package/dist/instance-swarm/instance-swarm-orchestrator.js +204 -0
- package/dist/instance-swarm/protocol-name-resolver.d.ts +23 -0
- package/dist/instance-swarm/protocol-name-resolver.js +64 -0
- package/dist/license/gate.d.ts +31 -0
- package/dist/license/gate.js +188 -0
- package/dist/license/http-client.d.ts +43 -0
- package/dist/license/http-client.js +195 -0
- package/dist/license/manager.d.ts +39 -0
- package/dist/license/manager.js +175 -0
- package/dist/license/receipt.d.ts +27 -0
- package/dist/license/receipt.js +108 -0
- package/dist/license/state-codec.d.ts +72 -0
- package/dist/license/state-codec.js +218 -0
- package/dist/license/state-paths.d.ts +17 -0
- package/dist/license/state-paths.js +64 -0
- package/dist/license/store.d.ts +44 -0
- package/dist/license/store.js +223 -0
- package/dist/license/trust-anchor.d.ts +24 -0
- package/dist/license/trust-anchor.js +74 -0
- package/dist/license/types.d.ts +34 -0
- package/dist/license/types.js +19 -0
- package/dist/llm/budget.d.ts +23 -0
- package/dist/llm/budget.js +58 -0
- package/dist/llm/client.d.ts +53 -0
- package/dist/llm/client.js +122 -0
- package/dist/llm/factory.d.ts +14 -0
- package/dist/llm/factory.js +32 -0
- package/dist/orchestrator/defect-logger.d.ts +31 -0
- package/dist/orchestrator/defect-logger.js +101 -0
- package/dist/orchestrator/defect-manager.d.ts +118 -0
- package/dist/orchestrator/defect-manager.js +338 -0
- package/dist/orchestrator/defect-migration.d.ts +14 -0
- package/dist/orchestrator/defect-migration.js +174 -0
- package/dist/orchestrator/defect-store.d.ts +48 -0
- package/dist/orchestrator/defect-store.js +76 -0
- package/dist/orchestrator/healing.d.ts +14 -0
- package/dist/orchestrator/healing.js +11 -0
- package/dist/orchestrator/interactive-steering.d.ts +64 -0
- package/dist/orchestrator/interactive-steering.js +107 -0
- package/dist/orchestrator/interrupt-handler.d.ts +30 -0
- package/dist/orchestrator/interrupt-handler.js +70 -0
- package/dist/orchestrator/lesson-manager.d.ts +25 -0
- package/dist/orchestrator/lesson-manager.js +122 -0
- package/dist/orchestrator/progress-summary.d.ts +15 -0
- package/dist/orchestrator/progress-summary.js +106 -0
- package/dist/orchestrator/reasoning-score.d.ts +49 -0
- package/dist/orchestrator/reasoning-score.js +117 -0
- package/dist/orchestrator/swarm.d.ts +36 -0
- package/dist/orchestrator/swarm.js +77 -0
- package/dist/orchestrator/task-factory.d.ts +15 -0
- package/dist/orchestrator/task-factory.js +176 -0
- package/dist/orchestrator/validation-gate.d.ts +23 -0
- package/dist/orchestrator/validation-gate.js +155 -0
- package/dist/protocol/react-contract.d.ts +136 -0
- package/dist/protocol/react-contract.js +698 -0
- package/dist/protocol/wks-protocol-manager.d.ts +22 -0
- package/dist/protocol/wks-protocol-manager.js +72 -0
- package/dist/sdlc-runtime/conversation-runner.d.ts +8 -0
- package/dist/sdlc-runtime/conversation-runner.js +64 -0
- package/dist/sdlc-runtime/dag-utils.d.ts +15 -0
- package/dist/sdlc-runtime/dag-utils.js +67 -0
- package/dist/sdlc-runtime/defect-integration.d.ts +7 -0
- package/dist/sdlc-runtime/defect-integration.js +99 -0
- package/dist/sdlc-runtime/execution-loop.d.ts +38 -0
- package/dist/sdlc-runtime/execution-loop.js +274 -0
- package/dist/sdlc-runtime/lesson-recorder.d.ts +9 -0
- package/dist/sdlc-runtime/lesson-recorder.js +27 -0
- package/dist/sdlc-runtime/plan-initializer.d.ts +47 -0
- package/dist/sdlc-runtime/plan-initializer.js +135 -0
- package/dist/sdlc-runtime/progress-persister.d.ts +10 -0
- package/dist/sdlc-runtime/progress-persister.js +34 -0
- package/dist/sdlc-runtime/protocol-loader.d.ts +10 -0
- package/dist/sdlc-runtime/protocol-loader.js +21 -0
- package/dist/sdlc-runtime/reporting.d.ts +24 -0
- package/dist/sdlc-runtime/reporting.js +68 -0
- package/dist/sdlc-runtime/sdlc-orchestrator.d.ts +59 -0
- package/dist/sdlc-runtime/sdlc-orchestrator.js +170 -0
- package/dist/sdlc-runtime/sdlc-skill.d.ts +44 -0
- package/dist/sdlc-runtime/sdlc-skill.js +148 -0
- package/dist/sdlc-runtime/steering-handler.d.ts +14 -0
- package/dist/sdlc-runtime/steering-handler.js +51 -0
- package/dist/sdlc-runtime/trace-recorder.d.ts +10 -0
- package/dist/sdlc-runtime/trace-recorder.js +7 -0
- package/dist/security/index.d.ts +16 -0
- package/dist/security/index.js +12 -0
- package/dist/security/input-screen.d.ts +30 -0
- package/dist/security/input-screen.js +75 -0
- package/dist/security/tool-call-guard.d.ts +16 -0
- package/dist/security/tool-call-guard.js +163 -0
- package/dist/security/untrusted-content.d.ts +34 -0
- package/dist/security/untrusted-content.js +63 -0
- package/dist/skills/advanced-dfir/SKILL.md +85 -0
- package/dist/skills/advanced-pentest-lab/SKILL.md +88 -0
- package/dist/skills/analyst/SKILL.md +55 -0
- package/dist/skills/architect/SKILL.md +54 -0
- package/dist/skills/aws/SKILL.md +92 -0
- package/dist/skills/azure/SKILL.md +90 -0
- package/dist/skills/bylaw-context.d.ts +45 -0
- package/dist/skills/bylaw-context.js +61 -0
- package/dist/skills/bylaw-review.d.ts +20 -0
- package/dist/skills/bylaw-review.js +101 -0
- package/dist/skills/conversation/SKILL.md +29 -0
- package/dist/skills/design-patterns/SKILL.md +190 -0
- package/dist/skills/devops/SKILL.md +55 -0
- package/dist/skills/directives.d.ts +20 -0
- package/dist/skills/directives.js +313 -0
- package/dist/skills/docker/SKILL.md +76 -0
- package/dist/skills/docker-expert/SKILL.md +56 -0
- package/dist/skills/enterprise-systems-analyst/SKILL.md +83 -0
- package/dist/skills/enterprise-systems-analyst/references/crm.md +41 -0
- package/dist/skills/enterprise-systems-analyst/references/erp.md +40 -0
- package/dist/skills/enterprise-systems-analyst/references/financial-systems.md +35 -0
- package/dist/skills/enterprise-systems-analyst/references/hcm.md +38 -0
- package/dist/skills/enterprise-systems-analyst/references/mrp.md +37 -0
- package/dist/skills/enterprise-systems-analyst/references/scm.md +32 -0
- package/dist/skills/ethical-hacking-methodology/SKILL.md +119 -0
- package/dist/skills/externalSkillLoader.d.ts +25 -0
- package/dist/skills/externalSkillLoader.js +65 -0
- package/dist/skills/externalSkillRegistry.d.ts +33 -0
- package/dist/skills/externalSkillRegistry.js +94 -0
- package/dist/skills/filesystem-management/SKILL.md +61 -0
- package/dist/skills/first-run-seed.d.ts +26 -0
- package/dist/skills/first-run-seed.js +75 -0
- package/dist/skills/git-vcs/SKILL.md +84 -0
- package/dist/skills/kafka/SKILL.md +89 -0
- package/dist/skills/kubernetes/SKILL.md +93 -0
- package/dist/skills/kubernetes-expert/SKILL.md +54 -0
- package/dist/skills/openshift/SKILL.md +93 -0
- package/dist/skills/pentester/SKILL.md +59 -0
- package/dist/skills/performance-tester/SKILL.md +57 -0
- package/dist/skills/playwright-ui-tester/SKILL.md +67 -0
- package/dist/skills/programmer/SKILL.md +58 -0
- package/dist/skills/project-scaffolding/SKILL.md +133 -0
- package/dist/skills/qa-engineer/SKILL.md +91 -0
- package/dist/skills/rca/SKILL.md +58 -0
- package/dist/skills/redhat/SKILL.md +92 -0
- package/dist/skills/rosa/SKILL.md +80 -0
- package/dist/skills/scrum-framework/SKILL.md +74 -0
- package/dist/skills/scrum-framework/references/artifacts_and_quality.md +79 -0
- package/dist/skills/scrum-framework/references/roles_and_events.md +105 -0
- package/dist/skills/scrum-master-agent/SKILL.md +74 -0
- package/dist/skills/scrum-master-agent/references/behaviors.md +56 -0
- package/dist/skills/sdlc-skill.d.ts +1 -0
- package/dist/skills/sdlc-skill.js +4 -0
- package/dist/skills/secops/SKILL.md +52 -0
- package/dist/skills/secure-architect.bylaws.d.ts +68 -0
- package/dist/skills/secure-architect.bylaws.js +525 -0
- package/dist/skills/seed-skills.d.ts +48 -0
- package/dist/skills/seed-skills.js +213 -0
- package/dist/skills/skill-authoring/SKILL.md +123 -0
- package/dist/skills/skill-header.d.ts +51 -0
- package/dist/skills/skill-header.js +163 -0
- package/dist/skills/skill-path-guard.d.ts +41 -0
- package/dist/skills/skill-path-guard.js +95 -0
- package/dist/skills/skillRouter.d.ts +51 -0
- package/dist/skills/skillRouter.js +90 -0
- package/dist/skills/skills-root.d.ts +64 -0
- package/dist/skills/skills-root.js +168 -0
- package/dist/skills/software-architect/SKILL.md +77 -0
- package/dist/skills/software-engineer/SKILL.md +74 -0
- package/dist/skills/solid-principles/SKILL.md +94 -0
- package/dist/skills/task-planning/SKILL.md +115 -0
- package/dist/skills/tester/SKILL.md +54 -0
- package/dist/skills/ubuntu/SKILL.md +87 -0
- package/dist/skills/ui-ux-design/SKILL.md +114 -0
- package/dist/skills/workspace-context/SKILL.md +103 -0
- package/dist/sprint/controller.d.ts +37 -0
- package/dist/sprint/controller.js +220 -0
- package/dist/sprint/dag-builder.d.ts +14 -0
- package/dist/sprint/dag-builder.js +105 -0
- package/dist/sprint/reporter.d.ts +11 -0
- package/dist/sprint/reporter.js +97 -0
- package/dist/sprint/state-store.d.ts +10 -0
- package/dist/sprint/state-store.js +71 -0
- package/dist/sprint/types.d.ts +24 -0
- package/dist/sprint/types.js +4 -0
- package/dist/store/file-version-store.d.ts +24 -0
- package/dist/store/file-version-store.js +73 -0
- package/dist/store/task-store.d.ts +90 -0
- package/dist/store/task-store.js +166 -0
- package/dist/task-ingest/architect-detailer.d.ts +43 -0
- package/dist/task-ingest/architect-detailer.js +141 -0
- package/dist/task-ingest/detail-schema.d.ts +9 -0
- package/dist/task-ingest/detail-schema.js +195 -0
- package/dist/task-ingest/detailed-run.d.ts +36 -0
- package/dist/task-ingest/detailed-run.js +108 -0
- package/dist/task-ingest/extract-id.d.ts +9 -0
- package/dist/task-ingest/extract-id.js +24 -0
- package/dist/task-ingest/index.d.ts +23 -0
- package/dist/task-ingest/index.js +14 -0
- package/dist/task-ingest/ingest.d.ts +45 -0
- package/dist/task-ingest/ingest.js +163 -0
- package/dist/task-ingest/list-ingested.d.ts +13 -0
- package/dist/task-ingest/list-ingested.js +30 -0
- package/dist/task-ingest/pending-filter.d.ts +30 -0
- package/dist/task-ingest/pending-filter.js +59 -0
- package/dist/task-ingest/scanner.d.ts +56 -0
- package/dist/task-ingest/scanner.js +216 -0
- package/dist/task-ingest/source-reader.d.ts +25 -0
- package/dist/task-ingest/source-reader.js +93 -0
- package/dist/task-ingest/spec-builder.d.ts +34 -0
- package/dist/task-ingest/spec-builder.js +89 -0
- package/dist/task-ingest/wbs-extract.d.ts +50 -0
- package/dist/task-ingest/wbs-extract.js +226 -0
- package/dist/task-ingest/wbs-parser.d.ts +36 -0
- package/dist/task-ingest/wbs-parser.js +170 -0
- package/dist/tools/apiTestTool.d.ts +37 -0
- package/dist/tools/apiTestTool.js +128 -0
- package/dist/tools/ast-tools.d.ts +183 -0
- package/dist/tools/ast-tools.js +853 -0
- package/dist/tools/browse-tool.d.ts +105 -0
- package/dist/tools/browse-tool.js +394 -0
- package/dist/tools/credential.d.ts +17 -0
- package/dist/tools/credential.js +32 -0
- package/dist/tools/dependency-graph.d.ts +23 -0
- package/dist/tools/dependency-graph.js +164 -0
- package/dist/tools/edit-tool.d.ts +34 -0
- package/dist/tools/edit-tool.js +95 -0
- package/dist/tools/file-header-manager.d.ts +27 -0
- package/dist/tools/file-header-manager.js +153 -0
- package/dist/tools/fs-tools.d.ts +154 -0
- package/dist/tools/fs-tools.js +337 -0
- package/dist/tools/git-tools.d.ts +79 -0
- package/dist/tools/git-tools.js +348 -0
- package/dist/tools/github-tool.d.ts +48 -0
- package/dist/tools/github-tool.js +147 -0
- package/dist/tools/globTool.d.ts +20 -0
- package/dist/tools/globTool.js +34 -0
- package/dist/tools/grepTool.d.ts +19 -0
- package/dist/tools/grepTool.js +44 -0
- package/dist/tools/index-freshness.d.ts +58 -0
- package/dist/tools/index-freshness.js +190 -0
- package/dist/tools/index-glob.d.ts +48 -0
- package/dist/tools/index-glob.js +175 -0
- package/dist/tools/load-skill-tool.d.ts +15 -0
- package/dist/tools/load-skill-tool.js +114 -0
- package/dist/tools/patch-tools.d.ts +58 -0
- package/dist/tools/patch-tools.js +153 -0
- package/dist/tools/playwright-tool.d.ts +66 -0
- package/dist/tools/playwright-tool.js +121 -0
- package/dist/tools/project-detect.d.ts +20 -0
- package/dist/tools/project-detect.js +87 -0
- package/dist/tools/prompt-guidance.d.ts +22 -0
- package/dist/tools/prompt-guidance.js +23 -0
- package/dist/tools/scheduleTool.d.ts +19 -0
- package/dist/tools/scheduleTool.js +181 -0
- package/dist/tools/search-tools.d.ts +27 -0
- package/dist/tools/search-tools.js +78 -0
- package/dist/tools/sed-tool.d.ts +66 -0
- package/dist/tools/sed-tool.js +402 -0
- package/dist/tools/shell-result.d.ts +11 -0
- package/dist/tools/shell-result.js +1 -0
- package/dist/tools/shell-tool.d.ts +45 -0
- package/dist/tools/shell-tool.js +141 -0
- package/dist/tools/siteCrawlerTool.d.ts +32 -0
- package/dist/tools/siteCrawlerTool.js +205 -0
- package/dist/tools/tool-registry.d.ts +67 -0
- package/dist/tools/tool-registry.js +596 -0
- package/dist/tools/validate-tool.d.ts +23 -0
- package/dist/tools/validate-tool.js +201 -0
- package/dist/tools/workspace-index.d.ts +46 -0
- package/dist/tools/workspace-index.js +173 -0
- package/dist/types.d.ts +222 -0
- package/dist/types.js +7 -0
- package/dist/updater/index.d.ts +14 -0
- package/dist/updater/index.js +120 -0
- package/dist/util/banner.d.ts +25 -0
- package/dist/util/banner.js +140 -0
- package/dist/util/command-suggester.d.ts +6 -0
- package/dist/util/command-suggester.js +25 -0
- package/dist/util/confirm-interrupt.d.ts +10 -0
- package/dist/util/confirm-interrupt.js +31 -0
- package/dist/util/continue-trigger.d.ts +11 -0
- package/dist/util/continue-trigger.js +20 -0
- package/dist/util/env-info.d.ts +18 -0
- package/dist/util/env-info.js +37 -0
- package/dist/util/file-access-log.d.ts +35 -0
- package/dist/util/file-access-log.js +140 -0
- package/dist/util/footer-manager.d.ts +124 -0
- package/dist/util/footer-manager.js +195 -0
- package/dist/util/footer-renderer.d.ts +67 -0
- package/dist/util/footer-renderer.js +319 -0
- package/dist/util/footer-state.d.ts +34 -0
- package/dist/util/footer-state.js +18 -0
- package/dist/util/footer-text.d.ts +51 -0
- package/dist/util/footer-text.js +74 -0
- package/dist/util/ids.d.ts +1 -0
- package/dist/util/ids.js +4 -0
- package/dist/util/levenshtein.d.ts +6 -0
- package/dist/util/levenshtein.js +34 -0
- package/dist/util/logger.d.ts +10 -0
- package/dist/util/logger.js +41 -0
- package/dist/util/logs.d.ts +26 -0
- package/dist/util/logs.js +88 -0
- package/dist/util/output-handler.d.ts +89 -0
- package/dist/util/output-handler.js +193 -0
- package/dist/util/output-interceptor.d.ts +18 -0
- package/dist/util/output-interceptor.js +49 -0
- package/dist/util/output-paths.d.ts +43 -0
- package/dist/util/output-paths.js +121 -0
- package/dist/util/package-json.d.ts +6 -0
- package/dist/util/package-json.js +36 -0
- package/dist/util/process-kill.d.ts +27 -0
- package/dist/util/process-kill.js +148 -0
- package/dist/util/protocol-error-logger.d.ts +33 -0
- package/dist/util/protocol-error-logger.js +73 -0
- package/dist/util/redact.d.ts +25 -0
- package/dist/util/redact.js +103 -0
- package/dist/util/scroll-region-guard.d.ts +33 -0
- package/dist/util/scroll-region-guard.js +63 -0
- package/dist/util/spinner.d.ts +15 -0
- package/dist/util/spinner.js +49 -0
- package/dist/util/terminal-output.d.ts +34 -0
- package/dist/util/terminal-output.js +1 -0
- package/dist/util/terminal-silent.d.ts +6 -0
- package/dist/util/terminal-silent.js +20 -0
- package/dist/util/terminal.d.ts +56 -0
- package/dist/util/terminal.js +124 -0
- package/dist/util/text-sanitize.d.ts +8 -0
- package/dist/util/text-sanitize.js +45 -0
- package/dist/util/thinking-spinner.d.ts +27 -0
- package/dist/util/thinking-spinner.js +189 -0
- package/dist/util/tool-error-logger.d.ts +33 -0
- package/dist/util/tool-error-logger.js +77 -0
- package/dist/util/tty.d.ts +13 -0
- package/dist/util/tty.js +16 -0
- package/dist/util/xcoder-paths.d.ts +56 -0
- package/dist/util/xcoder-paths.js +63 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.js +4 -0
- package/dist/worker-agent/to-subtask-node.d.ts +35 -0
- package/dist/worker-agent/to-subtask-node.js +41 -0
- package/dist/worker-agent/worker-orchestrator.d.ts +104 -0
- package/dist/worker-agent/worker-orchestrator.js +201 -0
- package/package.json +39 -4
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
// xcoder:version 4 | xcoder:task task-7a4850 | xcoder:updated 2026-10-25T16:26:42.659Z | xcoder:subtask code-st-892a22
|
|
2
|
+
// C3 — orchestrate `xcoder --wbc <file>`: read, parse, filter, detail, validate,
|
|
3
|
+
// build, then persist ONE task run per PENDING WBS item.
|
|
4
|
+
//
|
|
5
|
+
// Failure model: every step can fail, none can half-write. Every task is built
|
|
6
|
+
// and validated IN MEMORY before the first `saveTaskRun()`, so a schema failure
|
|
7
|
+
// on item 7 of 9 writes 0 files, not 6 — a half-ingested WBS would be
|
|
8
|
+
// indistinguishable from "the user already finished the first six items".
|
|
9
|
+
//
|
|
10
|
+
// Sibling of `ingest.ts`, not a mode flag on it: `task add --from` must keep
|
|
11
|
+
// calling `buildIngestedRun` byte-identically.
|
|
12
|
+
import path from "node:path";
|
|
13
|
+
import { listTaskRuns, loadTaskRun, saveTaskRun } from "../store/task-store.js";
|
|
14
|
+
import { readSource } from "./source-reader.js";
|
|
15
|
+
import { parseWbs, MAX_ITEMS } from "./wbs-parser.js";
|
|
16
|
+
import { selectPendingItems } from "./pending-filter.js";
|
|
17
|
+
import { deriveItemTaskId } from "./extract-id.js";
|
|
18
|
+
import { createDeterministicDetailer, createLlmDetailer, } from "./architect-detailer.js";
|
|
19
|
+
import { validateArchitectDetail } from "./detail-schema.js";
|
|
20
|
+
import { buildDetailedRun } from "./detailed-run.js";
|
|
21
|
+
import { assertValidRun } from "./ingest.js";
|
|
22
|
+
const DEFAULT_MAX_ITEMS = 50;
|
|
23
|
+
const MAX_CONCURRENCY = 8;
|
|
24
|
+
const PROGRESS = new Set(["done", "in_progress", "validating", "healing"]);
|
|
25
|
+
function fail(code, message, extra = {}) {
|
|
26
|
+
return { ok: false, exitCode: 1, code, message, ...extra };
|
|
27
|
+
}
|
|
28
|
+
function hasProgress(run) {
|
|
29
|
+
return run.dag.some((n) => PROGRESS.has(n.status));
|
|
30
|
+
}
|
|
31
|
+
/** Bounded-parallel map that aborts on the FIRST rejection. */
|
|
32
|
+
async function mapBounded(items, limit, fn) {
|
|
33
|
+
const results = new Array(items.length);
|
|
34
|
+
let next = 0;
|
|
35
|
+
let failure = null;
|
|
36
|
+
const worker = async () => {
|
|
37
|
+
while (!failure) {
|
|
38
|
+
const index = next++;
|
|
39
|
+
if (index >= items.length)
|
|
40
|
+
return;
|
|
41
|
+
try {
|
|
42
|
+
results[index] = await fn(items[index]);
|
|
43
|
+
}
|
|
44
|
+
catch (e) {
|
|
45
|
+
failure = failure ?? e;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
await Promise.all(Array.from({ length: Math.max(1, Math.min(limit, items.length)) }, worker));
|
|
50
|
+
if (failure)
|
|
51
|
+
throw failure;
|
|
52
|
+
return results;
|
|
53
|
+
}
|
|
54
|
+
function sameIdentity(run, relPathPosix, normalized) {
|
|
55
|
+
return run.spec.provenance?.sourcePath === relPathPosix && run.spec.inputs.freeText === normalized;
|
|
56
|
+
}
|
|
57
|
+
/** Derive the id for one item, escalating 8→12 hex on a foreign collision. */
|
|
58
|
+
async function deriveForItem(root, relPathPosix, item) {
|
|
59
|
+
for (const length of [8, 12]) {
|
|
60
|
+
const taskId = deriveItemTaskId(relPathPosix, item.normalized, length);
|
|
61
|
+
const existing = await loadTaskRun(root, taskId);
|
|
62
|
+
if (!existing)
|
|
63
|
+
return { ok: true, taskId, existing: null };
|
|
64
|
+
if (sameIdentity(existing, relPathPosix, item.normalized))
|
|
65
|
+
return { ok: true, taskId, existing };
|
|
66
|
+
}
|
|
67
|
+
return { ok: false };
|
|
68
|
+
}
|
|
69
|
+
export async function extractWbsTasks(opts) {
|
|
70
|
+
const root = path.resolve(opts.workspaceRoot);
|
|
71
|
+
const maxItems = opts.maxItems === undefined ? DEFAULT_MAX_ITEMS : Math.min(Math.floor(opts.maxItems), MAX_ITEMS);
|
|
72
|
+
const concurrency = opts.concurrency === undefined ? 3 : Math.floor(opts.concurrency);
|
|
73
|
+
if (!Number.isFinite(maxItems) || maxItems < 1)
|
|
74
|
+
return fail("bad-options", "--max-items must be a positive integer");
|
|
75
|
+
if (!Number.isFinite(concurrency) || concurrency < 1 || concurrency > MAX_CONCURRENCY) {
|
|
76
|
+
return fail("bad-options", `--concurrency must be between 1 and ${MAX_CONCURRENCY}`);
|
|
77
|
+
}
|
|
78
|
+
// 1-2. Read + confine, then parse (inert).
|
|
79
|
+
const source = await readSource(root, opts.from);
|
|
80
|
+
if (!source.ok)
|
|
81
|
+
return fail(source.code, source.message);
|
|
82
|
+
const src = source.value;
|
|
83
|
+
const stem = path.basename(src.relPathPosix, path.extname(src.relPathPosix));
|
|
84
|
+
const parsed = parseWbs(src.bytes.toString("utf8"), stem);
|
|
85
|
+
if (!parsed.ok)
|
|
86
|
+
return fail(parsed.code, parsed.message, parsed.line ? { line: parsed.line } : {});
|
|
87
|
+
// 3. Pending only — checked item TEXT never leaves this step.
|
|
88
|
+
const selection = selectPendingItems(parsed.value);
|
|
89
|
+
const base = { sourcePath: src.relPathPosix, sourceSha256: src.sha256, dryRun: opts.dryRun === true };
|
|
90
|
+
// 4. Nothing pending is a success, not a failure.
|
|
91
|
+
if (selection.pending.length === 0) {
|
|
92
|
+
return { ...base, ok: true, counts: { pending: 0, checked: selection.checkedCount, created: 0, unchanged: 0, superseded: 0 }, tasks: [] };
|
|
93
|
+
}
|
|
94
|
+
// 5. Bounded spend — refuse before spending a token.
|
|
95
|
+
if (selection.pending.length > maxItems) {
|
|
96
|
+
return fail("too-many-pending", `${selection.pending.length} pending items exceed --max-items ${maxItems} (use --max-items ${MAX_ITEMS} to take them all)`);
|
|
97
|
+
}
|
|
98
|
+
// 6. Existing-record probe → new | unchanged | blocked-by-progress.
|
|
99
|
+
const existingIds = new Set(await listTaskRuns(root));
|
|
100
|
+
const plans = [];
|
|
101
|
+
const liveIds = new Set();
|
|
102
|
+
for (const [index, item] of selection.pending.entries()) {
|
|
103
|
+
const derived = await deriveForItem(root, src.relPathPosix, item);
|
|
104
|
+
if (!derived.ok) {
|
|
105
|
+
return fail("id-collision", "a derived task id already belongs to a different work item", { line: item.line });
|
|
106
|
+
}
|
|
107
|
+
if (derived.existing && hasProgress(derived.existing) && !opts.force) {
|
|
108
|
+
return fail("has-progress", `${derived.taskId} already has completed or in-flight subtasks — re-run with --force to replace it`, { line: item.line, taskId: derived.taskId });
|
|
109
|
+
}
|
|
110
|
+
liveIds.add(derived.taskId);
|
|
111
|
+
plans.push({
|
|
112
|
+
item,
|
|
113
|
+
order: index + 1,
|
|
114
|
+
taskId: derived.taskId,
|
|
115
|
+
created: derived.existing === null,
|
|
116
|
+
existing: derived.existing ?? undefined,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
// Superseded: records from this same source whose item text no longer exists.
|
|
120
|
+
let superseded = 0;
|
|
121
|
+
for (const id of existingIds) {
|
|
122
|
+
if (liveIds.has(id))
|
|
123
|
+
continue;
|
|
124
|
+
const run = await loadTaskRun(root, id);
|
|
125
|
+
if (run?.spec.provenance?.sourcePath === src.relPathPosix)
|
|
126
|
+
superseded += 1;
|
|
127
|
+
}
|
|
128
|
+
// 7. Detail — bounded, abort on first failure, hold every result in memory.
|
|
129
|
+
const toDetail = plans.filter((p) => p.created || opts.force === true);
|
|
130
|
+
const detailer = opts.detailer === null ? createDeterministicDetailer() : opts.detailer ?? createLlmDetailer();
|
|
131
|
+
try {
|
|
132
|
+
await mapBounded(toDetail, concurrency, async (plan) => {
|
|
133
|
+
plan.detail = await detailer.detail({
|
|
134
|
+
itemText: plan.item.normalized,
|
|
135
|
+
line: plan.item.line,
|
|
136
|
+
wbsTitle: parsed.value.title,
|
|
137
|
+
wbsGoal: parsed.value.goal,
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
catch (e) {
|
|
142
|
+
return fail("architect-failed", `architect detail failed: ${e.message}`);
|
|
143
|
+
}
|
|
144
|
+
// 8. Validate + build EVERY task in memory before the first write.
|
|
145
|
+
const createdAt = (opts.now ?? (() => new Date()))().toISOString();
|
|
146
|
+
const staged = [];
|
|
147
|
+
const extracted = [];
|
|
148
|
+
for (const plan of plans) {
|
|
149
|
+
if (!plan.detail) {
|
|
150
|
+
const existing = plan.existing;
|
|
151
|
+
extracted.push({
|
|
152
|
+
taskId: plan.taskId,
|
|
153
|
+
line: plan.item.line,
|
|
154
|
+
order: plan.order,
|
|
155
|
+
created: false,
|
|
156
|
+
nodeCount: existing.dag.length,
|
|
157
|
+
stages: existing.dag.map((n) => n.stage),
|
|
158
|
+
nextCommand: `xcoder resume ${plan.taskId}`,
|
|
159
|
+
});
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
const validation = validateArchitectDetail(plan.detail);
|
|
163
|
+
if (!validation.ok) {
|
|
164
|
+
return fail("architect-schema-invalid", `architect output for the item at line ${plan.item.line} is off-contract: ${validation.problems.join("; ")}`, { line: plan.item.line, taskId: plan.taskId });
|
|
165
|
+
}
|
|
166
|
+
const note = plan.item.after === undefined
|
|
167
|
+
? undefined
|
|
168
|
+
: `follow WBS item #${plan.item.after} if it is still pending (advisory only)`;
|
|
169
|
+
const { spec, dag } = buildDetailedRun({
|
|
170
|
+
item: plan.item,
|
|
171
|
+
detail: validation.value,
|
|
172
|
+
source: { relPathPosix: src.relPathPosix, sha256: src.sha256 },
|
|
173
|
+
wbsTitle: parsed.value.title,
|
|
174
|
+
wbsGoal: parsed.value.goal,
|
|
175
|
+
taskId: plan.taskId,
|
|
176
|
+
createdAt: plan.existing?.spec.createdAt ?? createdAt,
|
|
177
|
+
concurrency,
|
|
178
|
+
sequencingNote: note,
|
|
179
|
+
});
|
|
180
|
+
const run = {
|
|
181
|
+
spec,
|
|
182
|
+
dag,
|
|
183
|
+
status: "halted",
|
|
184
|
+
escalations: [],
|
|
185
|
+
orchestratorTrace: [],
|
|
186
|
+
finalDeliverable: `Extracted from ${src.relPathPosix}:${plan.item.line} by --wbc — ` +
|
|
187
|
+
`${dag.length} subtask${dag.length === 1 ? "" : "s"}, not yet started. ` +
|
|
188
|
+
`Resume with: xcoder resume ${plan.taskId}`,
|
|
189
|
+
totalTokenUsage: { inputTokens: 0, outputTokens: 0 },
|
|
190
|
+
};
|
|
191
|
+
assertValidRun(run);
|
|
192
|
+
staged.push(run);
|
|
193
|
+
extracted.push({
|
|
194
|
+
taskId: plan.taskId,
|
|
195
|
+
line: plan.item.line,
|
|
196
|
+
order: plan.order,
|
|
197
|
+
created: true,
|
|
198
|
+
nodeCount: dag.length,
|
|
199
|
+
stages: dag.map((n) => n.stage),
|
|
200
|
+
nextCommand: `xcoder resume ${plan.taskId}`,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
// 9. Persist — only after every task validated. Skipped under --dry-run.
|
|
204
|
+
if (opts.dryRun !== true) {
|
|
205
|
+
try {
|
|
206
|
+
for (const run of staged)
|
|
207
|
+
await saveTaskRun(root, run);
|
|
208
|
+
}
|
|
209
|
+
catch (e) {
|
|
210
|
+
return fail("write-failed", `could not write task records: ${e.message}`);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
// 10. Summary.
|
|
214
|
+
return {
|
|
215
|
+
...base,
|
|
216
|
+
ok: true,
|
|
217
|
+
counts: {
|
|
218
|
+
pending: selection.pending.length,
|
|
219
|
+
checked: selection.checkedCount,
|
|
220
|
+
created: opts.dryRun === true ? 0 : staged.length,
|
|
221
|
+
unchanged: extracted.filter((t) => !t.created).length,
|
|
222
|
+
superseded,
|
|
223
|
+
},
|
|
224
|
+
tasks: extracted,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { SdlcStage } from "../types.js";
|
|
2
|
+
export interface WbsItem {
|
|
3
|
+
/** 0-based source order. */
|
|
4
|
+
index: number;
|
|
5
|
+
/** Verbatim item text, minus the list marker and any trailing tags. */
|
|
6
|
+
text: string;
|
|
7
|
+
checked: boolean;
|
|
8
|
+
stage?: SdlcStage;
|
|
9
|
+
after?: number;
|
|
10
|
+
/** 1-based line number, for error messages. */
|
|
11
|
+
line: number;
|
|
12
|
+
}
|
|
13
|
+
export interface ParsedWbs {
|
|
14
|
+
title: string;
|
|
15
|
+
goal: string;
|
|
16
|
+
items: WbsItem[];
|
|
17
|
+
frontMatter: Record<string, string>;
|
|
18
|
+
}
|
|
19
|
+
export type ParseErrorCode = "no-items" | "bad-front-matter" | "too-many-items";
|
|
20
|
+
export type ParseResult = {
|
|
21
|
+
ok: true;
|
|
22
|
+
value: ParsedWbs;
|
|
23
|
+
} | {
|
|
24
|
+
ok: false;
|
|
25
|
+
code: ParseErrorCode;
|
|
26
|
+
message: string;
|
|
27
|
+
line?: number;
|
|
28
|
+
};
|
|
29
|
+
/** Closed list — a front-matter key outside this set is dropped, never acted on. */
|
|
30
|
+
export declare const ALLOWED_FRONT_MATTER_KEYS: readonly ["title", "goal", "id", "status", "priority", "owner", "tags"];
|
|
31
|
+
export declare const MAX_ITEMS = 200;
|
|
32
|
+
/**
|
|
33
|
+
* Parse `text` as a WBS document. `fileStem` is used as a fallback title so a
|
|
34
|
+
* document without front matter still produces a human-meaningful goal.
|
|
35
|
+
*/
|
|
36
|
+
export declare function parseWbs(text: string, fileStem: string): ParseResult;
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
// xcoder:version 2 | xcoder:task task-a91099 | xcoder:updated 2026-10-23T10:55:01.913Z | xcoder:subtask code-st-03748c
|
|
2
|
+
// C2 — parse a markdown/WBS source into a bounded, inert item list.
|
|
3
|
+
//
|
|
4
|
+
// Threat model: the file is hostile (PRM-01). Two deliberate design choices
|
|
5
|
+
// keep it inert:
|
|
6
|
+
// 1. Front matter is a hand-written strict `key: value` reader over a closed
|
|
7
|
+
// allow-list — no YAML library (no alias-expansion DoS, no tag surface, no
|
|
8
|
+
// implicit type coercion). Anchors and block scalars are rejected outright.
|
|
9
|
+
// 2. Nothing a file says can set a path, a shell command, a URL, a credential
|
|
10
|
+
// ref or a budget. The parser cannot even represent those fields.
|
|
11
|
+
/** Closed list — a front-matter key outside this set is dropped, never acted on. */
|
|
12
|
+
export const ALLOWED_FRONT_MATTER_KEYS = ["title", "goal", "id", "status", "priority", "owner", "tags"];
|
|
13
|
+
const SDLC_STAGES = [
|
|
14
|
+
"requirements",
|
|
15
|
+
"design",
|
|
16
|
+
"code",
|
|
17
|
+
"refactor",
|
|
18
|
+
"test",
|
|
19
|
+
"ui_api_test",
|
|
20
|
+
"fix_defect",
|
|
21
|
+
"document",
|
|
22
|
+
"deploy",
|
|
23
|
+
"conversation",
|
|
24
|
+
"quality_gate",
|
|
25
|
+
];
|
|
26
|
+
export const MAX_ITEMS = 200;
|
|
27
|
+
function bad(message, line) {
|
|
28
|
+
return { ok: false, code: "bad-front-matter", message, line };
|
|
29
|
+
}
|
|
30
|
+
function parseFrontMatter(lines) {
|
|
31
|
+
const data = {};
|
|
32
|
+
if (lines.length === 0 || lines[0].trim() !== "---") {
|
|
33
|
+
return { ok: true, data, bodyStart: 0 };
|
|
34
|
+
}
|
|
35
|
+
let close = -1;
|
|
36
|
+
for (let i = 1; i < lines.length; i++) {
|
|
37
|
+
if (lines[i].trim() === "---") {
|
|
38
|
+
close = i;
|
|
39
|
+
break;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
if (close === -1) {
|
|
43
|
+
return { ok: false, result: bad("front matter opened with '---' but never closed", 1) };
|
|
44
|
+
}
|
|
45
|
+
for (let i = 1; i < close; i++) {
|
|
46
|
+
const raw = lines[i];
|
|
47
|
+
const lineNo = i + 1;
|
|
48
|
+
if (raw.trim() === "")
|
|
49
|
+
continue;
|
|
50
|
+
if (/^\s/.test(raw)) {
|
|
51
|
+
return { ok: false, result: bad("nested or indented front-matter values are not supported", lineNo) };
|
|
52
|
+
}
|
|
53
|
+
const colon = raw.indexOf(":");
|
|
54
|
+
if (colon === -1) {
|
|
55
|
+
return { ok: false, result: bad("front-matter line has no 'key: value' colon", lineNo) };
|
|
56
|
+
}
|
|
57
|
+
const key = raw.slice(0, colon).trim().toLowerCase();
|
|
58
|
+
const value = raw.slice(colon + 1).trim();
|
|
59
|
+
if (key.length === 0) {
|
|
60
|
+
return { ok: false, result: bad("front-matter key is empty", lineNo) };
|
|
61
|
+
}
|
|
62
|
+
if (/[&*]/.test(value)) {
|
|
63
|
+
return { ok: false, result: bad("YAML anchors and aliases are not supported", lineNo) };
|
|
64
|
+
}
|
|
65
|
+
if (/^[|>{[]/.test(value)) {
|
|
66
|
+
return { ok: false, result: bad("block scalars and flow collections are not supported", lineNo) };
|
|
67
|
+
}
|
|
68
|
+
if (Object.prototype.hasOwnProperty.call(data, key)) {
|
|
69
|
+
return { ok: false, result: bad(`duplicate front-matter key "${key}"`, lineNo) };
|
|
70
|
+
}
|
|
71
|
+
if (ALLOWED_FRONT_MATTER_KEYS.includes(key)) {
|
|
72
|
+
data[key] = value.replace(/^["']|["']$/g, "");
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return { ok: true, data, bodyStart: close + 1 };
|
|
76
|
+
}
|
|
77
|
+
const BULLET_ITEM = /^ {0,3}[-*+]\s+\[( |x|X)\]\s+(\S.*)$/;
|
|
78
|
+
const ORDERED_ITEM = /^ {0,3}(\d{1,9})[.)]\s+(\S.*)$/;
|
|
79
|
+
const STAGE_TAG = /\[stage:\s*([a-z_]+)\s*\]/i;
|
|
80
|
+
const AFTER_TAG = /\[after:\s*(\d+)\s*\]/i;
|
|
81
|
+
function stripTags(text) {
|
|
82
|
+
let stage;
|
|
83
|
+
let after;
|
|
84
|
+
const sm = STAGE_TAG.exec(text);
|
|
85
|
+
if (sm) {
|
|
86
|
+
const candidate = sm[1].toLowerCase();
|
|
87
|
+
if (SDLC_STAGES.includes(candidate))
|
|
88
|
+
stage = candidate;
|
|
89
|
+
}
|
|
90
|
+
const am = AFTER_TAG.exec(text);
|
|
91
|
+
if (am) {
|
|
92
|
+
const n = parseInt(am[1], 10);
|
|
93
|
+
if (Number.isFinite(n))
|
|
94
|
+
after = n;
|
|
95
|
+
}
|
|
96
|
+
const cleaned = text.replace(STAGE_TAG, " ").replace(AFTER_TAG, " ").replace(/\s{2,}/g, " ").trim();
|
|
97
|
+
return { text: cleaned, stage, after };
|
|
98
|
+
}
|
|
99
|
+
function extractItems(lines, bodyStart) {
|
|
100
|
+
const items = [];
|
|
101
|
+
// Track WHICH fence opened the block: a ``` block is not closed by ~~~.
|
|
102
|
+
let fence = null;
|
|
103
|
+
for (let i = bodyStart; i < lines.length; i++) {
|
|
104
|
+
const line = lines[i];
|
|
105
|
+
const trimmed = line.trim();
|
|
106
|
+
if (trimmed.startsWith("```") || trimmed.startsWith("~~~")) {
|
|
107
|
+
const marker = trimmed.startsWith("```") ? "```" : "~~~";
|
|
108
|
+
if (fence === null)
|
|
109
|
+
fence = marker;
|
|
110
|
+
else if (fence === marker)
|
|
111
|
+
fence = null;
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (fence !== null)
|
|
115
|
+
continue;
|
|
116
|
+
if (/^\s{4,}\S/.test(line))
|
|
117
|
+
continue;
|
|
118
|
+
let checked = false;
|
|
119
|
+
let text = null;
|
|
120
|
+
const bullet = BULLET_ITEM.exec(line);
|
|
121
|
+
if (bullet) {
|
|
122
|
+
checked = bullet[1].toLowerCase() === "x";
|
|
123
|
+
text = bullet[2];
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
const ordered = ORDERED_ITEM.exec(line);
|
|
127
|
+
if (ordered)
|
|
128
|
+
text = ordered[2];
|
|
129
|
+
}
|
|
130
|
+
if (text === null)
|
|
131
|
+
continue;
|
|
132
|
+
const stripped = stripTags(text);
|
|
133
|
+
if (stripped.text.length === 0)
|
|
134
|
+
continue;
|
|
135
|
+
items.push({
|
|
136
|
+
index: items.length,
|
|
137
|
+
text: stripped.text,
|
|
138
|
+
checked,
|
|
139
|
+
stage: stripped.stage,
|
|
140
|
+
after: stripped.after,
|
|
141
|
+
line: i + 1,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
return items;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Parse `text` as a WBS document. `fileStem` is used as a fallback title so a
|
|
148
|
+
* document without front matter still produces a human-meaningful goal.
|
|
149
|
+
*/
|
|
150
|
+
export function parseWbs(text, fileStem) {
|
|
151
|
+
const lines = text.split(/\r?\n/);
|
|
152
|
+
const fm = parseFrontMatter(lines);
|
|
153
|
+
if (!fm.ok)
|
|
154
|
+
return fm.result;
|
|
155
|
+
const items = extractItems(lines, fm.bodyStart);
|
|
156
|
+
if (items.length === 0) {
|
|
157
|
+
return { ok: false, code: "no-items", message: "no work items found — expected '- [ ] text' or '1. text' lines" };
|
|
158
|
+
}
|
|
159
|
+
if (items.length > MAX_ITEMS) {
|
|
160
|
+
return {
|
|
161
|
+
ok: false,
|
|
162
|
+
code: "too-many-items",
|
|
163
|
+
message: `found ${items.length} work items, over the ${MAX_ITEMS}-item limit`,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
const firstHeading = lines.slice(fm.bodyStart).find((l) => /^#{1,6}\s+\S/.test(l.trim()));
|
|
167
|
+
const title = fm.data.title ?? (firstHeading ? firstHeading.trim().replace(/^#{1,6}\s+/, "") : fileStem);
|
|
168
|
+
const goal = fm.data.goal ?? title;
|
|
169
|
+
return { ok: true, value: { title, goal, items, frontMatter: fm.data } };
|
|
170
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export interface ApiTestResult {
|
|
2
|
+
url: string;
|
|
3
|
+
method: string;
|
|
4
|
+
statusCode: number;
|
|
5
|
+
statusText: string;
|
|
6
|
+
headers: Record<string, string>;
|
|
7
|
+
body: unknown;
|
|
8
|
+
bodyTruncated: boolean;
|
|
9
|
+
durationMs: number;
|
|
10
|
+
contentType: string;
|
|
11
|
+
}
|
|
12
|
+
export interface ApiTestOptions {
|
|
13
|
+
url: string;
|
|
14
|
+
method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD" | "OPTIONS";
|
|
15
|
+
/** Optional query parameters appended to the URL */
|
|
16
|
+
queryParams?: Record<string, string>;
|
|
17
|
+
/** Request headers */
|
|
18
|
+
headers?: Record<string, string>;
|
|
19
|
+
/** Request body — string, object (serialized as JSON), or omitted */
|
|
20
|
+
body?: string | Record<string, unknown>;
|
|
21
|
+
/** How to encode the body. Defaults to 'json' if body is an object, 'text' if string. */
|
|
22
|
+
bodyType?: "json" | "text" | "form";
|
|
23
|
+
/** Max response body length in characters before truncation (default: 10000) */
|
|
24
|
+
maxBodyLength?: number;
|
|
25
|
+
/** Timeout in milliseconds (default: 30000) */
|
|
26
|
+
timeout?: number;
|
|
27
|
+
/** If set, the tool will assert the response status matches this value and return an error if not */
|
|
28
|
+
expectStatus?: number;
|
|
29
|
+
/** If set, the tool will assert the response body contains this string and return an error if not */
|
|
30
|
+
expectBodyContains?: string;
|
|
31
|
+
/** Reference to an environment variable holding a credential — resolved at tool level and NEVER returned to the LLM. Injected as Authorization: Bearer <value>. */
|
|
32
|
+
credentialRef?: string;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Execute an HTTP request against an API endpoint and return structured results.
|
|
36
|
+
*/
|
|
37
|
+
export declare function testApiEndpoint(opts: ApiTestOptions): Promise<ApiTestResult>;
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
// xcoder:version 4 | xcoder:task task-456d3f | xcoder:updated 2026-08-03T15:52:16.274Z | xcoder:subtask code-st-97a4d4
|
|
2
|
+
/**
|
|
3
|
+
* apiTestTool — Make HTTP requests to test API endpoints.
|
|
4
|
+
*
|
|
5
|
+
* Supports GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS with custom headers,
|
|
6
|
+
* JSON/Form/Text bodies, query parameters, and optional response validation.
|
|
7
|
+
* Returns structured results including status, headers, body, and timing.
|
|
8
|
+
*/
|
|
9
|
+
import { resolveCredential } from "./credential.js";
|
|
10
|
+
/**
|
|
11
|
+
* Execute an HTTP request against an API endpoint and return structured results.
|
|
12
|
+
*/
|
|
13
|
+
export async function testApiEndpoint(opts) {
|
|
14
|
+
const { url, method, queryParams, headers: customHeaders, body, bodyType, maxBodyLength = 10000, timeout = 30000, } = opts;
|
|
15
|
+
// Build URL with query params
|
|
16
|
+
let targetUrl = url;
|
|
17
|
+
if (queryParams && Object.keys(queryParams).length > 0) {
|
|
18
|
+
const params = new URLSearchParams(queryParams);
|
|
19
|
+
targetUrl += (url.includes("?") ? "&" : "?") + params.toString();
|
|
20
|
+
}
|
|
21
|
+
// Build fetch options
|
|
22
|
+
const fetchHeaders = {
|
|
23
|
+
...customHeaders,
|
|
24
|
+
};
|
|
25
|
+
// Resolve credentialRef at tool level — the raw value never leaves this function
|
|
26
|
+
if (opts.credentialRef) {
|
|
27
|
+
fetchHeaders["Authorization"] = `Bearer ${resolveCredential(opts.credentialRef)}`;
|
|
28
|
+
}
|
|
29
|
+
let fetchBody;
|
|
30
|
+
if (body !== undefined) {
|
|
31
|
+
if (bodyType === "form") {
|
|
32
|
+
const formBody = typeof body === "string" ? body : new URLSearchParams(body).toString();
|
|
33
|
+
fetchBody = formBody;
|
|
34
|
+
if (!fetchHeaders["Content-Type"]) {
|
|
35
|
+
fetchHeaders["Content-Type"] = "application/x-www-form-urlencoded";
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
else if (bodyType === "text") {
|
|
39
|
+
fetchBody = typeof body === "string" ? body : JSON.stringify(body);
|
|
40
|
+
if (!fetchHeaders["Content-Type"]) {
|
|
41
|
+
fetchHeaders["Content-Type"] = "text/plain";
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
// Default: json
|
|
46
|
+
fetchBody = typeof body === "string" ? body : JSON.stringify(body);
|
|
47
|
+
if (!fetchHeaders["Content-Type"]) {
|
|
48
|
+
fetchHeaders["Content-Type"] = "application/json";
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
// HEAD requests cannot have a body
|
|
53
|
+
if (method === "HEAD" || method === "OPTIONS") {
|
|
54
|
+
fetchBody = undefined;
|
|
55
|
+
}
|
|
56
|
+
const startTime = Date.now();
|
|
57
|
+
const controller = new AbortController();
|
|
58
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
59
|
+
try {
|
|
60
|
+
const response = await fetch(targetUrl, {
|
|
61
|
+
method,
|
|
62
|
+
headers: fetchHeaders,
|
|
63
|
+
body: fetchBody,
|
|
64
|
+
signal: controller.signal,
|
|
65
|
+
redirect: "follow",
|
|
66
|
+
});
|
|
67
|
+
const durationMs = Date.now() - startTime;
|
|
68
|
+
// Collect response headers
|
|
69
|
+
const responseHeaders = {};
|
|
70
|
+
response.headers.forEach((value, key) => {
|
|
71
|
+
responseHeaders[key] = value;
|
|
72
|
+
});
|
|
73
|
+
const contentType = responseHeaders["content-type"] ?? "";
|
|
74
|
+
// Read body (HEAD and OPTIONS requests have no body)
|
|
75
|
+
let bodyResult = null;
|
|
76
|
+
let bodyTruncated = false;
|
|
77
|
+
if (method !== "HEAD" && method !== "OPTIONS") {
|
|
78
|
+
const rawText = await response.text();
|
|
79
|
+
if (rawText.length > maxBodyLength) {
|
|
80
|
+
bodyTruncated = true;
|
|
81
|
+
}
|
|
82
|
+
const truncated = rawText.slice(0, maxBodyLength);
|
|
83
|
+
// Try to parse as JSON if content-type suggests it
|
|
84
|
+
if (contentType.includes("application/json") || contentType.includes("application/problem+json")) {
|
|
85
|
+
try {
|
|
86
|
+
bodyResult = JSON.parse(truncated);
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
bodyResult = truncated;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
bodyResult = truncated;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
// Validate expectStatus if provided
|
|
97
|
+
if (opts.expectStatus !== undefined && response.status !== opts.expectStatus) {
|
|
98
|
+
const bodyStr = bodyResult !== null ? JSON.stringify(bodyResult).slice(0, 200) : "(no body)";
|
|
99
|
+
throw new Error(`Expected status ${opts.expectStatus} but got ${response.status} (${response.statusText}). Body preview: ${bodyStr}`);
|
|
100
|
+
}
|
|
101
|
+
// Validate expectBodyContains if provided
|
|
102
|
+
if (opts.expectBodyContains !== undefined) {
|
|
103
|
+
const rawBody = bodyResult !== null ? (typeof bodyResult === "string" ? bodyResult : JSON.stringify(bodyResult)) : "";
|
|
104
|
+
if (!rawBody.includes(opts.expectBodyContains)) {
|
|
105
|
+
throw new Error(`Expected body to contain "${opts.expectBodyContains}" but it did not.`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
url: targetUrl,
|
|
110
|
+
method,
|
|
111
|
+
statusCode: response.status,
|
|
112
|
+
statusText: response.statusText,
|
|
113
|
+
headers: responseHeaders,
|
|
114
|
+
body: bodyResult,
|
|
115
|
+
bodyTruncated,
|
|
116
|
+
durationMs,
|
|
117
|
+
contentType,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
catch (err) {
|
|
121
|
+
const durationMs = Date.now() - startTime;
|
|
122
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
123
|
+
throw new Error(`API request failed after ${durationMs}ms: ${message}`);
|
|
124
|
+
}
|
|
125
|
+
finally {
|
|
126
|
+
clearTimeout(timeoutId);
|
|
127
|
+
}
|
|
128
|
+
}
|