productfoo 0.0.0-stage → 1.0.1
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 +201 -0
- package/README.md +338 -2
- package/bin/foo +3 -0
- package/bin/foo.mjs +42 -0
- package/commands/bug.md +31 -0
- package/commands/discovery.md +28 -0
- package/commands/ideate.md +27 -0
- package/commands/init.md +28 -0
- package/commands/pitch.md +27 -0
- package/commands/preview.md +25 -0
- package/commands/research.md +31 -0
- package/commands/roast.md +27 -0
- package/commands/spec.md +30 -0
- package/config/pricing.yaml +198 -0
- package/config/routing-policy.yaml +171 -0
- package/dist-public/src/activeModel.d.ts +83 -0
- package/dist-public/src/activeModel.js +331 -0
- package/dist-public/src/agentAdapters.d.ts +122 -0
- package/dist-public/src/agentAdapters.js +178 -0
- package/dist-public/src/agentState.d.ts +43 -0
- package/dist-public/src/agentState.js +177 -0
- package/dist-public/src/bugCapsule.d.ts +94 -0
- package/dist-public/src/bugCapsule.js +227 -0
- package/dist-public/src/bugCapture.d.ts +26 -0
- package/dist-public/src/bugCapture.js +251 -0
- package/dist-public/src/bugOwnerLog.d.ts +93 -0
- package/dist-public/src/bugOwnerLog.js +304 -0
- package/dist-public/src/bugReportStore.d.ts +48 -0
- package/dist-public/src/bugReportStore.js +860 -0
- package/dist-public/src/bugSpecContext.d.ts +7 -0
- package/dist-public/src/bugSpecContext.js +129 -0
- package/dist-public/src/channelFreshness.d.ts +25 -0
- package/dist-public/src/channelFreshness.js +194 -0
- package/dist-public/src/cli.d.ts +30 -0
- package/dist-public/src/cli.js +1667 -0
- package/dist-public/src/cliArgs.d.ts +7 -0
- package/dist-public/src/cliArgs.js +44 -0
- package/dist-public/src/codeCatalog.d.ts +41 -0
- package/dist-public/src/codeCatalog.js +197 -0
- package/dist-public/src/commandContract.d.ts +84 -0
- package/dist-public/src/commandContract.js +390 -0
- package/dist-public/src/commandUsage.d.ts +3 -0
- package/dist-public/src/commandUsage.js +30 -0
- package/dist-public/src/commands/agent.d.ts +60 -0
- package/dist-public/src/commands/agent.js +1353 -0
- package/dist-public/src/commands/asp.d.ts +8 -0
- package/dist-public/src/commands/asp.js +669 -0
- package/dist-public/src/commands/evalCmd.d.ts +4 -0
- package/dist-public/src/commands/evalCmd.js +405 -0
- package/dist-public/src/commands/login.d.ts +25 -0
- package/dist-public/src/commands/login.js +94 -0
- package/dist-public/src/commands/loop.d.ts +16 -0
- package/dist-public/src/commands/loop.js +619 -0
- package/dist-public/src/commands/reconcile.d.ts +41 -0
- package/dist-public/src/commands/reconcile.js +292 -0
- package/dist-public/src/commands/runtime.d.ts +1 -0
- package/dist-public/src/commands/runtime.js +36 -0
- package/dist-public/src/commands/scanRedact.d.ts +8 -0
- package/dist-public/src/commands/scanRedact.js +266 -0
- package/dist-public/src/commands/setup.d.ts +28 -0
- package/dist-public/src/commands/setup.js +398 -0
- package/dist-public/src/commands/shared.d.ts +17 -0
- package/dist-public/src/commands/shared.js +147 -0
- package/dist-public/src/commands/source.d.ts +1 -0
- package/dist-public/src/commands/source.js +134 -0
- package/dist-public/src/commands/specCmd.d.ts +24 -0
- package/dist-public/src/commands/specCmd.js +794 -0
- package/dist-public/src/commands/update.d.ts +61 -0
- package/dist-public/src/commands/update.js +244 -0
- package/dist-public/src/comprehensionSignal.d.ts +20 -0
- package/dist-public/src/comprehensionSignal.js +166 -0
- package/dist-public/src/conductor/diversity.d.ts +48 -0
- package/dist-public/src/conductor/diversity.js +423 -0
- package/dist-public/src/conductor/engine.d.ts +7 -0
- package/dist-public/src/conductor/engine.js +295 -0
- package/dist-public/src/conductor/families/ideate.d.ts +2 -0
- package/dist-public/src/conductor/families/ideate.js +255 -0
- package/dist-public/src/conductor/families/interview.d.ts +2 -0
- package/dist-public/src/conductor/families/interview.js +39 -0
- package/dist-public/src/conductor/families/specSwarm.d.ts +2 -0
- package/dist-public/src/conductor/families/specSwarm.js +146 -0
- package/dist-public/src/conductor/familyDelivery.d.ts +6 -0
- package/dist-public/src/conductor/familyDelivery.js +37 -0
- package/dist-public/src/conductor/familyPackArtifact.d.ts +4 -0
- package/dist-public/src/conductor/familyPackArtifact.js +260 -0
- package/dist-public/src/conductor/ideateDecision.d.ts +6 -0
- package/dist-public/src/conductor/ideateDecision.js +237 -0
- package/dist-public/src/conductor/pack.d.ts +26 -0
- package/dist-public/src/conductor/pack.js +75 -0
- package/dist-public/src/conductor/types.d.ts +111 -0
- package/dist-public/src/conductor/types.js +7 -0
- package/dist-public/src/configPath.d.ts +2 -0
- package/dist-public/src/configPath.js +7 -0
- package/dist-public/src/discoveryArtifacts.d.ts +54 -0
- package/dist-public/src/discoveryArtifacts.js +467 -0
- package/dist-public/src/discoveryCommand.d.ts +127 -0
- package/dist-public/src/discoveryCommand.js +2148 -0
- package/dist-public/src/discoveryConnections.d.ts +339 -0
- package/dist-public/src/discoveryConnections.js +613 -0
- package/dist-public/src/discoveryDocs.d.ts +129 -0
- package/dist-public/src/discoveryDocs.js +577 -0
- package/dist-public/src/discoveryEvidence.d.ts +59 -0
- package/dist-public/src/discoveryEvidence.js +198 -0
- package/dist-public/src/discoveryFireflies.d.ts +25 -0
- package/dist-public/src/discoveryFireflies.js +551 -0
- package/dist-public/src/discoveryGmail.d.ts +12 -0
- package/dist-public/src/discoveryGmail.js +466 -0
- package/dist-public/src/discoveryGong.d.ts +12 -0
- package/dist-public/src/discoveryGong.js +491 -0
- package/dist-public/src/discoveryLearningRouting.d.ts +90 -0
- package/dist-public/src/discoveryLearningRouting.js +405 -0
- package/dist-public/src/discoveryMatching.d.ts +26 -0
- package/dist-public/src/discoveryMatching.js +73 -0
- package/dist-public/src/discoveryOutlook.d.ts +12 -0
- package/dist-public/src/discoveryOutlook.js +492 -0
- package/dist-public/src/discoveryProcess.d.ts +221 -0
- package/dist-public/src/discoveryProcess.js +703 -0
- package/dist-public/src/discoveryProof.d.ts +143 -0
- package/dist-public/src/discoveryProof.js +529 -0
- package/dist-public/src/discoveryProviders.d.ts +152 -0
- package/dist-public/src/discoveryProviders.js +756 -0
- package/dist-public/src/discoveryReadiness.d.ts +219 -0
- package/dist-public/src/discoveryReadiness.js +672 -0
- package/dist-public/src/discoveryReconcile.d.ts +57 -0
- package/dist-public/src/discoveryReconcile.js +424 -0
- package/dist-public/src/discoverySlack.d.ts +12 -0
- package/dist-public/src/discoverySlack.js +452 -0
- package/dist-public/src/discoverySources.d.ts +56 -0
- package/dist-public/src/discoverySources.js +322 -0
- package/dist-public/src/discoverySync.d.ts +52 -0
- package/dist-public/src/discoverySync.js +151 -0
- package/dist-public/src/discoverySynthesis.d.ts +57 -0
- package/dist-public/src/discoverySynthesis.js +215 -0
- package/dist-public/src/discoveryTeams.d.ts +12 -0
- package/dist-public/src/discoveryTeams.js +516 -0
- package/dist-public/src/discoveryZoom.d.ts +12 -0
- package/dist-public/src/discoveryZoom.js +523 -0
- package/dist-public/src/elicitationSurface.d.ts +13 -0
- package/dist-public/src/elicitationSurface.js +77 -0
- package/dist-public/src/errorReport.d.ts +99 -0
- package/dist-public/src/errorReport.js +443 -0
- package/dist-public/src/errorTaxonomy.d.ts +84 -0
- package/dist-public/src/errorTaxonomy.js +189 -0
- package/dist-public/src/evalCalibration.d.ts +82 -0
- package/dist-public/src/evalCalibration.js +169 -0
- package/dist-public/src/evalChecks.d.ts +21 -0
- package/dist-public/src/evalChecks.js +295 -0
- package/dist-public/src/evalCompile.d.ts +56 -0
- package/dist-public/src/evalCompile.js +58 -0
- package/dist-public/src/evalImprove.d.ts +60 -0
- package/dist-public/src/evalImprove.js +175 -0
- package/dist-public/src/evalJudge.d.ts +47 -0
- package/dist-public/src/evalJudge.js +143 -0
- package/dist-public/src/evalLabels.d.ts +100 -0
- package/dist-public/src/evalLabels.js +287 -0
- package/dist-public/src/evalRecord.d.ts +68 -0
- package/dist-public/src/evalRecord.js +165 -0
- package/dist-public/src/evalRoast.d.ts +88 -0
- package/dist-public/src/evalRoast.js +249 -0
- package/dist-public/src/evidenceIntake.d.ts +66 -0
- package/dist-public/src/evidenceIntake.js +356 -0
- package/dist-public/src/feedback.d.ts +78 -0
- package/dist-public/src/feedback.js +294 -0
- package/dist-public/src/firefliesClient.d.ts +106 -0
- package/dist-public/src/firefliesClient.js +258 -0
- package/dist-public/src/firstMoveTensions.d.ts +10 -0
- package/dist-public/src/firstMoveTensions.js +93 -0
- package/dist-public/src/gitRun.d.ts +21 -0
- package/dist-public/src/gitRun.js +50 -0
- package/dist-public/src/githubReconcile.d.ts +94 -0
- package/dist-public/src/githubReconcile.js +205 -0
- package/dist-public/src/gmailNango.d.ts +131 -0
- package/dist-public/src/gmailNango.js +329 -0
- package/dist-public/src/gongNango.d.ts +118 -0
- package/dist-public/src/gongNango.js +308 -0
- package/dist-public/src/grammarGate.d.ts +13 -0
- package/dist-public/src/grammarGate.js +72 -0
- package/dist-public/src/harnessGates.d.ts +65 -0
- package/dist-public/src/harnessGates.js +168 -0
- package/dist-public/src/humanSurfaceEval.d.ts +19 -0
- package/dist-public/src/humanSurfaceEval.js +156 -0
- package/dist-public/src/ideate.d.ts +43 -0
- package/dist-public/src/ideate.js +255 -0
- package/dist-public/src/ideateContext.d.ts +11 -0
- package/dist-public/src/ideateContext.js +83 -0
- package/dist-public/src/index.d.ts +2 -0
- package/dist-public/src/index.js +4 -0
- package/dist-public/src/initCommand.d.ts +25 -0
- package/dist-public/src/initCommand.js +558 -0
- package/dist-public/src/interviewGate.d.ts +25 -0
- package/dist-public/src/interviewGate.js +126 -0
- package/dist-public/src/jsonFile.d.ts +2 -0
- package/dist-public/src/jsonFile.js +22 -0
- package/dist-public/src/loopBudget.d.ts +182 -0
- package/dist-public/src/loopBudget.js +494 -0
- package/dist-public/src/loopContracts.d.ts +35 -0
- package/dist-public/src/loopContracts.js +364 -0
- package/dist-public/src/machineState.d.ts +1 -0
- package/dist-public/src/machineState.js +9 -0
- package/dist-public/src/notionNango.d.ts +93 -0
- package/dist-public/src/notionNango.js +279 -0
- package/dist-public/src/orchestrationResult.d.ts +11 -0
- package/dist-public/src/orchestrationResult.js +73 -0
- package/dist-public/src/outlookNango.d.ts +124 -0
- package/dist-public/src/outlookNango.js +327 -0
- package/dist-public/src/packetCorrections.d.ts +40 -0
- package/dist-public/src/packetCorrections.js +172 -0
- package/dist-public/src/packetGlossary.d.ts +6 -0
- package/dist-public/src/packetGlossary.js +42 -0
- package/dist-public/src/packetIndex.d.ts +54 -0
- package/dist-public/src/packetIndex.js +744 -0
- package/dist-public/src/packetIndexEntry.d.ts +3 -0
- package/dist-public/src/packetIndexEntry.js +128 -0
- package/dist-public/src/packetIndexServe.d.ts +13 -0
- package/dist-public/src/packetIndexServe.js +113 -0
- package/dist-public/src/packetIndexWorker.d.ts +7 -0
- package/dist-public/src/packetIndexWorker.js +45 -0
- package/dist-public/src/packetLifecycle.d.ts +30 -0
- package/dist-public/src/packetLifecycle.js +127 -0
- package/dist-public/src/packetReconcile.d.ts +73 -0
- package/dist-public/src/packetReconcile.js +386 -0
- package/dist-public/src/packetStatus.d.ts +125 -0
- package/dist-public/src/packetStatus.js +770 -0
- package/dist-public/src/packetValidation.d.ts +17 -0
- package/dist-public/src/packetValidation.js +333 -0
- package/dist-public/src/packetVerify.d.ts +73 -0
- package/dist-public/src/packetVerify.js +303 -0
- package/dist-public/src/packetWrite.d.ts +52 -0
- package/dist-public/src/packetWrite.js +448 -0
- package/dist-public/src/pitch.d.ts +38 -0
- package/dist-public/src/pitch.js +154 -0
- package/dist-public/src/processMemo.d.ts +8 -0
- package/dist-public/src/processMemo.js +23 -0
- package/dist-public/src/productDetect.d.ts +30 -0
- package/dist-public/src/productDetect.js +376 -0
- package/dist-public/src/profile.d.ts +62 -0
- package/dist-public/src/profile.js +334 -0
- package/dist-public/src/questionModel.d.ts +46 -0
- package/dist-public/src/questionModel.js +74 -0
- package/dist-public/src/questionText.d.ts +7 -0
- package/dist-public/src/questionText.js +38 -0
- package/dist-public/src/redactConfig.d.ts +2 -0
- package/dist-public/src/redactConfig.js +11 -0
- package/dist-public/src/redactEngine.d.ts +59 -0
- package/dist-public/src/redactEngine.js +509 -0
- package/dist-public/src/redactGate.d.ts +63 -0
- package/dist-public/src/redactGate.js +209 -0
- package/dist-public/src/repoRead.d.ts +29 -0
- package/dist-public/src/repoRead.js +160 -0
- package/dist-public/src/research.d.ts +242 -0
- package/dist-public/src/research.js +1314 -0
- package/dist-public/src/researchCompletion.d.ts +34 -0
- package/dist-public/src/researchCompletion.js +123 -0
- package/dist-public/src/researchExecution.d.ts +47 -0
- package/dist-public/src/researchExecution.js +309 -0
- package/dist-public/src/researchFormat.d.ts +21 -0
- package/dist-public/src/researchFormat.js +77 -0
- package/dist-public/src/researchGate.d.ts +86 -0
- package/dist-public/src/researchGate.js +1036 -0
- package/dist-public/src/researchReceipt.d.ts +76 -0
- package/dist-public/src/researchReceipt.js +148 -0
- package/dist-public/src/researchRender.d.ts +2 -0
- package/dist-public/src/researchRender.js +267 -0
- package/dist-public/src/researchSigningKey.d.ts +19 -0
- package/dist-public/src/researchSigningKey.js +95 -0
- package/dist-public/src/riskClassify.d.ts +42 -0
- package/dist-public/src/riskClassify.js +149 -0
- package/dist-public/src/routines.d.ts +42 -0
- package/dist-public/src/routines.js +269 -0
- package/dist-public/src/routingPolicy.d.ts +172 -0
- package/dist-public/src/routingPolicy.js +646 -0
- package/dist-public/src/secretInput.d.ts +8 -0
- package/dist-public/src/secretInput.js +89 -0
- package/dist-public/src/selfCheck.d.ts +7 -0
- package/dist-public/src/selfCheck.js +473 -0
- package/dist-public/src/selfCheckCache.d.ts +29 -0
- package/dist-public/src/selfCheckCache.js +147 -0
- package/dist-public/src/severityClass.d.ts +15 -0
- package/dist-public/src/severityClass.js +22 -0
- package/dist-public/src/skillClientPolicy.d.ts +8 -0
- package/dist-public/src/skillClientPolicy.js +16 -0
- package/dist-public/src/skillLocal.d.ts +98 -0
- package/dist-public/src/skillLocal.js +463 -0
- package/dist-public/src/skillTreeDigest.d.ts +9 -0
- package/dist-public/src/skillTreeDigest.js +64 -0
- package/dist-public/src/slackNango.d.ts +62 -0
- package/dist-public/src/slackNango.js +208 -0
- package/dist-public/src/social/arxiv.d.ts +19 -0
- package/dist-public/src/social/arxiv.js +96 -0
- package/dist-public/src/social/github.d.ts +2 -0
- package/dist-public/src/social/github.js +78 -0
- package/dist-public/src/social/hackernews.d.ts +2 -0
- package/dist-public/src/social/hackernews.js +67 -0
- package/dist-public/src/social/index.d.ts +10 -0
- package/dist-public/src/social/index.js +79 -0
- package/dist-public/src/social/reddit.d.ts +2 -0
- package/dist-public/src/social/reddit.js +125 -0
- package/dist-public/src/social/types.d.ts +73 -0
- package/dist-public/src/social/types.js +61 -0
- package/dist-public/src/social/xml.d.ts +10 -0
- package/dist-public/src/social/xml.js +49 -0
- package/dist-public/src/social/youtube.d.ts +7 -0
- package/dist-public/src/social/youtube.js +217 -0
- package/dist-public/src/sourceIngress.d.ts +488 -0
- package/dist-public/src/sourceIngress.js +1651 -0
- package/dist-public/src/specAfter.d.ts +187 -0
- package/dist-public/src/specAfter.js +814 -0
- package/dist-public/src/specBudget.d.ts +124 -0
- package/dist-public/src/specBudget.js +424 -0
- package/dist-public/src/specDecisionSignals.d.ts +38 -0
- package/dist-public/src/specDecisionSignals.js +121 -0
- package/dist-public/src/specLinkedCommits.d.ts +7 -0
- package/dist-public/src/specLinkedCommits.js +107 -0
- package/dist-public/src/specSections.d.ts +105 -0
- package/dist-public/src/specSections.js +718 -0
- package/dist-public/src/statusGlyphs.d.ts +18 -0
- package/dist-public/src/statusGlyphs.js +75 -0
- package/dist-public/src/swarmCommand.d.ts +10 -0
- package/dist-public/src/swarmCommand.js +96 -0
- package/dist-public/src/teamsNango.d.ts +114 -0
- package/dist-public/src/teamsNango.js +301 -0
- package/dist-public/src/tokenStore.d.ts +10 -0
- package/dist-public/src/tokenStore.js +77 -0
- package/dist-public/src/trustEnvelope.d.ts +78 -0
- package/dist-public/src/trustEnvelope.js +392 -0
- package/dist-public/src/untrustedText.d.ts +6 -0
- package/dist-public/src/untrustedText.js +20 -0
- package/dist-public/src/updateNotice.d.ts +41 -0
- package/dist-public/src/updateNotice.js +290 -0
- package/dist-public/src/vendorYaml.d.ts +1 -0
- package/dist-public/src/vendorYaml.js +7374 -0
- package/dist-public/src/workflowRunActuals.d.ts +67 -0
- package/dist-public/src/workflowRunActuals.js +184 -0
- package/dist-public/src/zendeskDiscovery.d.ts +12 -0
- package/dist-public/src/zendeskDiscovery.js +513 -0
- package/dist-public/src/zendeskNango.d.ts +131 -0
- package/dist-public/src/zendeskNango.js +344 -0
- package/dist-public/src/zoomNango.d.ts +106 -0
- package/dist-public/src/zoomNango.js +351 -0
- package/docs/CHANGELOG.md +32 -0
- package/docs/THIRD_PARTY_NOTICES.md +36 -0
- package/package.json +71 -4
- package/schemas/bug-capsule-receipt.schema.json +54 -0
- package/schemas/bug-capsule.schema.json +189 -0
- package/schemas/discovery-evidence-envelope.schema.json +79 -0
- package/schemas/discovery-processing-receipt.schema.json +71 -0
- package/schemas/discovery-service-contract.schema.json +189 -0
- package/skills/AGENTS.md +32 -0
- package/skills/README.md +287 -0
- package/skills/bug/SKILL.md +49 -0
- package/skills/build-preview-spec/SKILL.md +103 -0
- package/skills/capture-external-artifact/SKILL.md +23 -0
- package/skills/capture-external-artifact/agents/openai.yaml +4 -0
- package/skills/capture-external-artifact/references/extractor-libraries.md +27 -0
- package/skills/check-spec-health/SKILL.md +45 -0
- package/skills/check-spec-health/agents/openai.yaml +4 -0
- package/skills/competitive-positioning/SKILL.md +86 -0
- package/skills/competitive-positioning/agents/openai.yaml +4 -0
- package/skills/cpo-lens/SKILL.md +398 -0
- package/skills/cpo-lens/agents/openai.yaml +4 -0
- package/skills/create-prd/SKILL.md +26 -0
- package/skills/create-prd/agents/openai.yaml +4 -0
- package/skills/customer-discovery/SKILL.md +192 -0
- package/skills/design-system/SKILL.md +49 -0
- package/skills/design-system/agents/openai.yaml +4 -0
- package/skills/discussion-guide/SKILL.md +75 -0
- package/skills/discussion-guide/agents/openai.yaml +4 -0
- package/skills/firecrawl-research-papers/SKILL.md +96 -0
- package/skills/firecrawl-research-papers/agents/openai.yaml +4 -0
- package/skills/foo-roast/SKILL.md +325 -0
- package/skills/generate-gtm-brief/SKILL.md +41 -0
- package/skills/generate-gtm-brief/agents/openai.yaml +4 -0
- package/skills/generate-gtm-brief/references/gtm-brief-template.md +56 -0
- package/skills/gong-quotes/SKILL.md +43 -0
- package/skills/gong-quotes/agents/openai.yaml +4 -0
- package/skills/grill-me/SKILL.md +27 -0
- package/skills/grill-me/agents/openai.yaml +4 -0
- package/skills/ideate/SKILL.md +180 -0
- package/skills/ideate/references/family-packs.md +5 -0
- package/skills/initiative-tracker/SKILL.md +23 -0
- package/skills/initiative-tracker/agents/openai.yaml +4 -0
- package/skills/interop-review/SKILL.md +25 -0
- package/skills/interop-review/agents/openai.yaml +4 -0
- package/skills/interview-me/SKILL.md +404 -0
- package/skills/interview-me/agents/openai.yaml +4 -0
- package/skills/loop-library-audit/SKILL.md +123 -0
- package/skills/pitch/SKILL.md +110 -0
- package/skills/pitch/references/opportunity-assessment.md +136 -0
- package/skills/pitch/references/prfaq-template.md +54 -0
- package/skills/pmm-release-briefing/SKILL.md +27 -0
- package/skills/pmm-release-briefing/agents/openai.yaml +4 -0
- package/skills/pmm-release-briefing/references/variant-generalized.md +16 -0
- package/skills/positioning-messaging/SKILL.md +70 -0
- package/skills/positioning-messaging/agents/openai.yaml +4 -0
- package/skills/positioning-messaging/references/guest-insights.md +1400 -0
- package/skills/preview-swarm/SKILL.md +418 -0
- package/skills/preview-swarm/agents/openai.yaml +4 -0
- package/skills/preview-swarm/references/html-effectiveness-build-previews.md +168 -0
- package/skills/product-copywriting/SKILL.md +127 -0
- package/skills/product-copywriting/agents/openai.yaml +4 -0
- package/skills/product-copywriting/references/foo-voice.md +26 -0
- package/skills/product-shape-interview/SKILL.md +219 -0
- package/skills/product-shape-interview/agents/openai.yaml +4 -0
- package/skills/ready-for-release/SKILL.md +39 -0
- package/skills/ready-for-release/agents/openai.yaml +4 -0
- package/skills/redact-transcripts/SKILL.md +23 -0
- package/skills/redact-transcripts/agents/openai.yaml +4 -0
- package/skills/research-brief/SKILL.md +48 -0
- package/skills/research-brief/agents/openai.yaml +4 -0
- package/skills/research-refresh/SKILL.md +63 -0
- package/skills/research-refresh/references/worked-cases.md +44 -0
- package/skills/review-loop/SKILL.md +200 -0
- package/skills/review-spec/SKILL.md +32 -0
- package/skills/review-spec/agents/openai.yaml +4 -0
- package/skills/rfc-from-research/SKILL.md +42 -0
- package/skills/rfc-from-research/agents/openai.yaml +4 -0
- package/skills/rfc-from-research/references/rfc-pattern.md +69 -0
- package/skills/skill-registry.yaml +810 -0
- package/skills/spec-cleanup/SKILL.md +103 -0
- package/skills/spec-cleanup/agents/openai.yaml +4 -0
- package/skills/spec-swarm/SKILL.md +87 -0
- package/skills/spec-swarm/agents/openai.yaml +4 -0
- package/skills/spec-swarm/template/SKILL.template.md +94 -0
- package/skills/spec-swarm/template/personas/accessibility-inclusion.md +40 -0
- package/skills/spec-swarm/template/personas/ai-technical-pm.md +69 -0
- package/skills/spec-swarm/template/personas/analytics-engineering.md +39 -0
- package/skills/spec-swarm/template/personas/applied-ai-eval.md +64 -0
- package/skills/spec-swarm/template/personas/business-gm-pm.md +67 -0
- package/skills/spec-swarm/template/personas/consumer-craft-pm.md +68 -0
- package/skills/spec-swarm/template/personas/content-design.md +40 -0
- package/skills/spec-swarm/template/personas/cpo.md +42 -0
- package/skills/spec-swarm/template/personas/data-science.md +42 -0
- package/skills/spec-swarm/template/personas/developer-experience.md +65 -0
- package/skills/spec-swarm/template/personas/dx-product.md +70 -0
- package/skills/spec-swarm/template/personas/field.md +43 -0
- package/skills/spec-swarm/template/personas/finance-cfo.md +40 -0
- package/skills/spec-swarm/template/personas/growth-pm.md +67 -0
- package/skills/spec-swarm/template/personas/platform-pm.md +67 -0
- package/skills/spec-swarm/template/personas/pmm.md +43 -0
- package/skills/spec-swarm/template/personas/principal-product-designer.md +42 -0
- package/skills/spec-swarm/template/personas/product-ops-program.md +41 -0
- package/skills/spec-swarm/template/personas/qa-release.md +41 -0
- package/skills/spec-swarm/template/personas/responsible-ai-safety.md +64 -0
- package/skills/spec-swarm/template/personas/security-privacy-legal.md +40 -0
- package/skills/spec-swarm/template/personas/strategy-competitive.md +41 -0
- package/skills/spec-swarm/template/personas/technical-architect.md +42 -0
- package/skills/spec-swarm/template/personas/technical-lead.md +43 -0
- package/skills/spec-swarm/template/personas/user-research.md +41 -0
- package/skills/spec-swarm-accessibility-inclusion/SKILL.md +95 -0
- package/skills/spec-swarm-accessibility-inclusion/agents/openai.yaml +4 -0
- package/skills/spec-swarm-ai-technical-pm/SKILL.md +124 -0
- package/skills/spec-swarm-analytics-engineering/SKILL.md +94 -0
- package/skills/spec-swarm-analytics-engineering/agents/openai.yaml +4 -0
- package/skills/spec-swarm-applied-ai-eval/SKILL.md +120 -0
- package/skills/spec-swarm-business-gm-pm/SKILL.md +122 -0
- package/skills/spec-swarm-consumer-craft-pm/SKILL.md +122 -0
- package/skills/spec-swarm-content-design/SKILL.md +95 -0
- package/skills/spec-swarm-content-design/agents/openai.yaml +4 -0
- package/skills/spec-swarm-cpo/SKILL.md +94 -0
- package/skills/spec-swarm-cpo/agents/openai.yaml +4 -0
- package/skills/spec-swarm-data-science/SKILL.md +95 -0
- package/skills/spec-swarm-data-science/agents/openai.yaml +4 -0
- package/skills/spec-swarm-developer-experience/SKILL.md +121 -0
- package/skills/spec-swarm-dx-product/SKILL.md +126 -0
- package/skills/spec-swarm-field/SKILL.md +96 -0
- package/skills/spec-swarm-field/agents/openai.yaml +4 -0
- package/skills/spec-swarm-finance-cfo/SKILL.md +95 -0
- package/skills/spec-swarm-finance-cfo/agents/openai.yaml +4 -0
- package/skills/spec-swarm-growth-pm/SKILL.md +122 -0
- package/skills/spec-swarm-platform-pm/SKILL.md +122 -0
- package/skills/spec-swarm-pmm/SKILL.md +96 -0
- package/skills/spec-swarm-pmm/agents/openai.yaml +4 -0
- package/skills/spec-swarm-principal-product-designer/SKILL.md +95 -0
- package/skills/spec-swarm-principal-product-designer/agents/openai.yaml +4 -0
- package/skills/spec-swarm-product-ops-program/SKILL.md +96 -0
- package/skills/spec-swarm-product-ops-program/agents/openai.yaml +4 -0
- package/skills/spec-swarm-qa-release/SKILL.md +96 -0
- package/skills/spec-swarm-qa-release/agents/openai.yaml +4 -0
- package/skills/spec-swarm-responsible-ai-safety/SKILL.md +120 -0
- package/skills/spec-swarm-security-privacy-legal/SKILL.md +95 -0
- package/skills/spec-swarm-security-privacy-legal/agents/openai.yaml +4 -0
- package/skills/spec-swarm-strategy-competitive/SKILL.md +96 -0
- package/skills/spec-swarm-strategy-competitive/agents/openai.yaml +4 -0
- package/skills/spec-swarm-technical-architect/SKILL.md +95 -0
- package/skills/spec-swarm-technical-architect/agents/openai.yaml +4 -0
- package/skills/spec-swarm-technical-lead/SKILL.md +96 -0
- package/skills/spec-swarm-technical-lead/agents/openai.yaml +4 -0
- package/skills/spec-swarm-user-research/SKILL.md +95 -0
- package/skills/spec-swarm-user-research/agents/openai.yaml +4 -0
- package/skills/strategy-check/SKILL.md +170 -0
- package/skills/strategy-check/agents/openai.yaml +4 -0
- package/skills/study-digest/SKILL.md +41 -0
- package/skills/synthetic-customer/SKILL.md +68 -0
- package/skills/update-cli-spec/SKILL.md +69 -0
- package/skills/update-cli-spec/agents/openai.yaml +4 -0
- package/skills/voc-researcher/SKILL.md +45 -0
- package/skills/voc-researcher/agents/openai.yaml +4 -0
- package/skills-lock.json +35 -0
|
@@ -0,0 +1,814 @@
|
|
|
1
|
+
// spec-after-capture-v0 — the V0a deterministic spine for `foo spec from <what-you-built>`.
|
|
2
|
+
//
|
|
3
|
+
// V0a is the activation tier: reachable + consented + redacted prompt capture +
|
|
4
|
+
// the diff + a four-bucket drift report rendered for a human. No LLM auto-labeler
|
|
5
|
+
// as authority, no QA panel, no spec write, no canonization (see the packet
|
|
6
|
+
// Decisions). The reconciliation here is a deterministic keyword-overlap
|
|
7
|
+
// SUGGESTION (provenance `suggested-structural`) that surfaces the INTENT-ONLY /
|
|
8
|
+
// CODE-ONLY residue — the aha — but never assigns AGREE/DRIFT; the human does
|
|
9
|
+
// that. Distillation and behavior inference are slope-riding adapters (R-002/R-010).
|
|
10
|
+
//
|
|
11
|
+
// Safety: runSpecAfter remains a pure transformation over prompt text already
|
|
12
|
+
// supplied by a trusted caller. There is no prompt-file ingress: the CLI reads
|
|
13
|
+
// prompts from no file, so untrusted text enters only where a caller passes it.
|
|
14
|
+
// The pure transform still redacts text, never executes it, and never writes it raw.
|
|
15
|
+
import { gitRun, gitRunOrNull } from "./gitRun.js";
|
|
16
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
17
|
+
import path from "node:path";
|
|
18
|
+
import { extractBehaviorStories } from "./codeCatalog.js";
|
|
19
|
+
import { neutralizeUntrustedText } from "./untrustedText.js";
|
|
20
|
+
const SECRET_PATTERNS = [
|
|
21
|
+
{ kind: "private-key", re: /-----BEGIN (?:RSA |EC |OPENSSH |DSA |PGP )?PRIVATE KEY-----[\s\S]*?-----END (?:RSA |EC |OPENSSH |DSA |PGP )?PRIVATE KEY-----/g },
|
|
22
|
+
{ kind: "openai-key", re: /\bsk-[A-Za-z0-9_-]{20,}\b/g },
|
|
23
|
+
{ kind: "aws-access-key", re: /\bAKIA[0-9A-Z]{16}\b/g },
|
|
24
|
+
{ kind: "github-token", re: /\b(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9]{36,}\b/g },
|
|
25
|
+
{ kind: "github-pat", re: /\bgithub_pat_[A-Za-z0-9_]{20,}\b/g },
|
|
26
|
+
{ kind: "slack-token", re: /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/g },
|
|
27
|
+
{ kind: "jwt", re: /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{6,}\.[A-Za-z0-9_-]{6,}\b/g },
|
|
28
|
+
{ kind: "env-secret", re: /\b([A-Z][A-Z0-9_]*(?:KEY|TOKEN|SECRET|PASSWORD|PASSWD|PWD|CREDENTIALS?))\s*[:=]\s*\S{6,}/g },
|
|
29
|
+
{ kind: "bearer", re: /\b(?:Bearer|Authorization)\b\s*[:=]?\s*[A-Za-z0-9._~+/=-]{16,}/gi },
|
|
30
|
+
{ kind: "email", re: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/g },
|
|
31
|
+
];
|
|
32
|
+
const AMBIGUOUS_TOKEN = /[A-Za-z0-9+/_=-]{24,}/g;
|
|
33
|
+
export function redactPromptText(text) {
|
|
34
|
+
const findings = [];
|
|
35
|
+
let out = text;
|
|
36
|
+
for (const { kind, re } of SECRET_PATTERNS) {
|
|
37
|
+
out = out.replace(re, (match) => {
|
|
38
|
+
findings.push({ kind, quarantined: false });
|
|
39
|
+
if (kind === "env-secret") {
|
|
40
|
+
const sep = match.search(/[:=]/);
|
|
41
|
+
return `${match.slice(0, sep + 1)} [REDACTED:env-secret]`;
|
|
42
|
+
}
|
|
43
|
+
return `[REDACTED:${kind}]`;
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
out = out.replace(AMBIGUOUS_TOKEN, (match) => {
|
|
47
|
+
if (match.includes("REDACTED"))
|
|
48
|
+
return match;
|
|
49
|
+
const mixed = /[A-Z]/.test(match) && /[a-z]/.test(match) && /[0-9]/.test(match);
|
|
50
|
+
if (!mixed)
|
|
51
|
+
return match;
|
|
52
|
+
findings.push({ kind: "quarantined-secret-shape", quarantined: true });
|
|
53
|
+
return "[REDACTED:quarantined-secret-shape]";
|
|
54
|
+
});
|
|
55
|
+
return {
|
|
56
|
+
redacted: out,
|
|
57
|
+
report: {
|
|
58
|
+
findings,
|
|
59
|
+
quarantined: findings.some((f) => f.quarantined),
|
|
60
|
+
redacted_count: findings.length,
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
function emptyRedaction() {
|
|
65
|
+
return { findings: [], quarantined: false, redacted_count: 0 };
|
|
66
|
+
}
|
|
67
|
+
function emptyScreening() {
|
|
68
|
+
return { triggered: false, stripped: [] };
|
|
69
|
+
}
|
|
70
|
+
// --- Intent distillation (V0a, deterministic, no LLM) ------------------------
|
|
71
|
+
function splitPromptBlocks(text) {
|
|
72
|
+
const byBlank = text.split(/\n\s*\n/).map((s) => s.trim()).filter(Boolean);
|
|
73
|
+
if (byBlank.length > 1)
|
|
74
|
+
return byBlank;
|
|
75
|
+
return text.split(/\r?\n/).map((s) => s.trim()).filter(Boolean);
|
|
76
|
+
}
|
|
77
|
+
function isNoise(text) {
|
|
78
|
+
return text.length < 4 || /^(ok|okay|yes|no|thanks|thank you|sure|great|cool|nice|done)\.?$/i.test(text);
|
|
79
|
+
}
|
|
80
|
+
export function distillIntent(promptsText) {
|
|
81
|
+
const blocks = splitPromptBlocks(promptsText);
|
|
82
|
+
const items = [];
|
|
83
|
+
const findings = [];
|
|
84
|
+
const stripped = [];
|
|
85
|
+
let quarantined = false;
|
|
86
|
+
let redactedCount = 0;
|
|
87
|
+
let seq = 0;
|
|
88
|
+
blocks.forEach((block, index) => {
|
|
89
|
+
const { redacted, report } = redactPromptText(block);
|
|
90
|
+
findings.push(...report.findings);
|
|
91
|
+
quarantined = quarantined || report.quarantined;
|
|
92
|
+
redactedCount += report.redacted_count;
|
|
93
|
+
// R-006: the prompt stream is untrusted. Neutralize injected directives so
|
|
94
|
+
// prompt text stays evidence about intent, never an instruction to the agent.
|
|
95
|
+
const screen = neutralizeUntrustedText(redacted);
|
|
96
|
+
stripped.push(...screen.stripped_directives);
|
|
97
|
+
// A block that triggered screening is quarantined WHOLE: the un-stripped
|
|
98
|
+
// remainder of an injected block ("...skip the security lens") is still
|
|
99
|
+
// attacker text and must never become distilled intent. Audited, not used.
|
|
100
|
+
if (screen.triggered)
|
|
101
|
+
return;
|
|
102
|
+
const text = screen.inert_text.trim();
|
|
103
|
+
if (!text || isNoise(text))
|
|
104
|
+
return;
|
|
105
|
+
seq += 1;
|
|
106
|
+
items.push({ id: `i-${String(seq).padStart(3, "0")}`, text, source: "prompt", raw_index: index });
|
|
107
|
+
});
|
|
108
|
+
return {
|
|
109
|
+
items,
|
|
110
|
+
redaction: { findings, quarantined, redacted_count: redactedCount },
|
|
111
|
+
screening: { triggered: stripped.length > 0, stripped },
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
// Strip the boilerplate a PR body carries (badges, images, HTML comments, the
|
|
115
|
+
// generated-with footer, horizontal rules) so it does not become intent, and
|
|
116
|
+
// lift the Foo-Spec trailer out as a declared-packet signal rather than intent.
|
|
117
|
+
function cleanPrBody(body) {
|
|
118
|
+
const kept = [];
|
|
119
|
+
let declares = null;
|
|
120
|
+
for (const line of body.split(/\r?\n/)) {
|
|
121
|
+
const t = line.trim();
|
|
122
|
+
const trailer = t.match(/^Foo-Spec:\s*(\S+)/);
|
|
123
|
+
if (trailer) {
|
|
124
|
+
declares = trailer[1];
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
if (!t) {
|
|
128
|
+
kept.push(line);
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
if (/^!?\[[^\]]*\]\([^)]*\)$/.test(t))
|
|
132
|
+
continue; // badge / image markdown
|
|
133
|
+
if (/^\[!\[/.test(t))
|
|
134
|
+
continue; // linked badge image ([](href))
|
|
135
|
+
if (/shields\.io|\/badge\//i.test(t))
|
|
136
|
+
continue; // CI/tooling badges
|
|
137
|
+
if (/^<!--.*-->$/.test(t))
|
|
138
|
+
continue; // html comment
|
|
139
|
+
if (/^-{3,}$/.test(t) || /^={3,}$/.test(t))
|
|
140
|
+
continue; // horizontal rule
|
|
141
|
+
if (/^(?:🤖|generated with|built with|compound engineering|claude code)/i.test(t))
|
|
142
|
+
continue;
|
|
143
|
+
// Checklist items carry intent in their label — keep the label, drop the box.
|
|
144
|
+
if (/^[-*]\s*\[[ xX]\]/.test(t)) {
|
|
145
|
+
kept.push(t.replace(/^[-*]\s*\[[ xX]\]\s*/, ""));
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
kept.push(line);
|
|
149
|
+
}
|
|
150
|
+
return { text: kept.join("\n"), declaresPacket: declares };
|
|
151
|
+
}
|
|
152
|
+
// Redact + injection-screen one free-text block, returning inert intent text or
|
|
153
|
+
// null when the block is a secret quarantine, an injection, or noise. Mirrors the
|
|
154
|
+
// per-block discipline of distillIntent so PR text stays evidence, never an
|
|
155
|
+
// instruction.
|
|
156
|
+
function safeIntentText(block, agg) {
|
|
157
|
+
const { redacted, report } = redactPromptText(block);
|
|
158
|
+
agg.findings.push(...report.findings);
|
|
159
|
+
agg.quarantined.v = agg.quarantined.v || report.quarantined;
|
|
160
|
+
const screen = neutralizeUntrustedText(redacted);
|
|
161
|
+
agg.stripped.push(...screen.stripped_directives);
|
|
162
|
+
if (screen.triggered)
|
|
163
|
+
return null; // a screened block is quarantined whole
|
|
164
|
+
const text = screen.inert_text.trim();
|
|
165
|
+
if (!text || isNoise(text))
|
|
166
|
+
return null;
|
|
167
|
+
return text;
|
|
168
|
+
}
|
|
169
|
+
export function recoverPrIntent(pr) {
|
|
170
|
+
const items = [];
|
|
171
|
+
const agg = { findings: [], stripped: [], quarantined: { v: false } };
|
|
172
|
+
let seq = 0;
|
|
173
|
+
const add = (text, judgment, source) => {
|
|
174
|
+
seq += 1;
|
|
175
|
+
items.push({ id: `ri-${String(seq).padStart(3, "0")}`, text, judgment, source });
|
|
176
|
+
};
|
|
177
|
+
// 1. Title — the author's one-line statement of intent (agent-inference: we are
|
|
178
|
+
// reading their summary, not asserting it as agreed).
|
|
179
|
+
const title = (pr.title ?? "").trim();
|
|
180
|
+
if (title && !isNoise(title) && !/^(wip|update|fix|misc|changes?)$/i.test(title)) {
|
|
181
|
+
const safe = safeIntentText(title, agg);
|
|
182
|
+
if (safe)
|
|
183
|
+
add(safe, "agent-inference", "pr-title");
|
|
184
|
+
}
|
|
185
|
+
// 2. Body — cleaned of boilerplate, distilled block by block.
|
|
186
|
+
const { text: cleanedBody, declaresPacket } = cleanPrBody(pr.body ?? "");
|
|
187
|
+
for (const block of splitPromptBlocks(cleanedBody)) {
|
|
188
|
+
const safe = safeIntentText(block, agg);
|
|
189
|
+
if (safe)
|
|
190
|
+
add(safe, "agent-inference", "pr-body");
|
|
191
|
+
}
|
|
192
|
+
// 3. Linked tracker issue — approved connected-source context, not the agent's
|
|
193
|
+
// inference. Directional, still to be verified against repo-truth.
|
|
194
|
+
const issue = pr.linked_issue;
|
|
195
|
+
if (issue) {
|
|
196
|
+
const issueText = [issue.title, issue.body].filter((s) => Boolean(s && s.trim())).join("\n\n");
|
|
197
|
+
for (const block of splitPromptBlocks(issueText)) {
|
|
198
|
+
const safe = safeIntentText(block, agg);
|
|
199
|
+
if (safe)
|
|
200
|
+
add(safe, "approved-context", "linked-issue");
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
const redaction = {
|
|
204
|
+
findings: agg.findings,
|
|
205
|
+
quarantined: agg.quarantined.v,
|
|
206
|
+
redacted_count: agg.findings.length,
|
|
207
|
+
};
|
|
208
|
+
const screening = { triggered: agg.stripped.length > 0, stripped: agg.stripped };
|
|
209
|
+
return {
|
|
210
|
+
recovered: items.length > 0,
|
|
211
|
+
items,
|
|
212
|
+
redaction,
|
|
213
|
+
screening,
|
|
214
|
+
declares_packet: declaresPacket,
|
|
215
|
+
reason: items.length > 0 ? undefined : "intent could not be recovered — the PR title and body are too thin to distill a contract (this is honest, not a hallucinated intent)",
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
// Compose a spec-after report from recovered PR intent + the PR's changed-file
|
|
219
|
+
// behavior, so `foo reconcile --adopt` can canonize a validating packet through
|
|
220
|
+
// the existing spec-after machinery (T-004). Recovered items enter as the
|
|
221
|
+
// `intent` side; the behavior side is code-derived.
|
|
222
|
+
export function buildReconcileSpecAfterReport(opts) {
|
|
223
|
+
const intent = opts.recovered.items.map((item, index) => ({
|
|
224
|
+
id: item.id,
|
|
225
|
+
text: item.text,
|
|
226
|
+
source: "prompt",
|
|
227
|
+
raw_index: index,
|
|
228
|
+
}));
|
|
229
|
+
return {
|
|
230
|
+
schema_version: "0.1",
|
|
231
|
+
tier: "v0a",
|
|
232
|
+
target: opts.target,
|
|
233
|
+
consent: { available: true, ingested: true },
|
|
234
|
+
prompt_context: { mode: "summary", captured: true, source: opts.source },
|
|
235
|
+
redaction: opts.recovered.redaction,
|
|
236
|
+
screening: opts.recovered.screening,
|
|
237
|
+
intent,
|
|
238
|
+
behavior: opts.behavior,
|
|
239
|
+
coverage: opts.coverage,
|
|
240
|
+
drift: suggestDrift(intent, opts.behavior),
|
|
241
|
+
pull_through: {
|
|
242
|
+
next: `foo asp reconcile ${opts.target}`,
|
|
243
|
+
note: "Adopted from a packet-free reconcile run — the recovered intent is provisional (agent-inference); reconcile keeps it honest to the code.",
|
|
244
|
+
},
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
// --- Deterministic drift suggestion (non-authoritative) ----------------------
|
|
248
|
+
const STOPWORDS = new Set([
|
|
249
|
+
"that", "this", "with", "from", "into", "your", "have", "want", "need", "make", "made", "adds",
|
|
250
|
+
"added", "should", "would", "also", "then", "when", "what", "where", "which", "they", "them",
|
|
251
|
+
"their", "there", "here", "using", "uses", "code", "file", "files", "function", "functions",
|
|
252
|
+
"feature", "change", "changes", "please", "like", "just", "some", "thing", "things", "work",
|
|
253
|
+
]);
|
|
254
|
+
function intentKeywords(text) {
|
|
255
|
+
const tokens = (text.toLowerCase().match(/[a-z][a-z0-9]{3,}/g) ?? []).filter((t) => !STOPWORDS.has(t));
|
|
256
|
+
return new Set(tokens);
|
|
257
|
+
}
|
|
258
|
+
function splitIdent(symbol) {
|
|
259
|
+
const spaced = symbol.replace(/[_-]+/g, " ").replace(/([a-z0-9])([A-Z])/g, "$1 $2").toLowerCase();
|
|
260
|
+
const parts = spaced.split(/\s+/).filter((p) => p.length >= 4);
|
|
261
|
+
return [symbol.toLowerCase(), ...parts];
|
|
262
|
+
}
|
|
263
|
+
function behaviorKeywords(story) {
|
|
264
|
+
const out = new Set();
|
|
265
|
+
for (const symbol of [...story.added_symbols, ...story.removed_symbols]) {
|
|
266
|
+
for (const token of splitIdent(symbol)) {
|
|
267
|
+
if (token.length >= 4 && !STOPWORDS.has(token))
|
|
268
|
+
out.add(token);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
const base = (story.file.split("/").pop() ?? story.file).replace(/\.[^.]+$/, "");
|
|
272
|
+
for (const token of base.split(/[^a-z0-9]+/i)) {
|
|
273
|
+
const t = token.toLowerCase();
|
|
274
|
+
if (t.length >= 4 && !STOPWORDS.has(t))
|
|
275
|
+
out.add(t);
|
|
276
|
+
}
|
|
277
|
+
return out;
|
|
278
|
+
}
|
|
279
|
+
function overlaps(a, b) {
|
|
280
|
+
for (const token of a)
|
|
281
|
+
if (b.has(token))
|
|
282
|
+
return true;
|
|
283
|
+
return false;
|
|
284
|
+
}
|
|
285
|
+
function truncate(text, max = 64) {
|
|
286
|
+
return text.length <= max ? text : `${text.slice(0, max - 1)}…`;
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Deterministic structural drift SUGGESTION. Surfaces the unmatched residue —
|
|
290
|
+
* INTENT-ONLY (asked, not built) and CODE-ONLY (built, not asked) — and leaves
|
|
291
|
+
* every matched pair UNREVIEWED for the human. It never emits AGREE/DRIFT: V0a
|
|
292
|
+
* does not auto-classify (packet Decisions; R-004/R-008).
|
|
293
|
+
*/
|
|
294
|
+
export function suggestDrift(intent, behavior) {
|
|
295
|
+
const intentKw = intent.map((item) => ({ item, kw: intentKeywords(item.text) }));
|
|
296
|
+
const behaviorKw = behavior.map((story) => ({ story, kw: behaviorKeywords(story) }));
|
|
297
|
+
const matched = new Set();
|
|
298
|
+
const items = [];
|
|
299
|
+
for (const { item, kw } of intentKw) {
|
|
300
|
+
const hit = behaviorKw.find((b) => overlaps(kw, b.kw));
|
|
301
|
+
if (hit) {
|
|
302
|
+
matched.add(hit.story.id);
|
|
303
|
+
items.push({
|
|
304
|
+
category: "UNREVIEWED",
|
|
305
|
+
intent_ref: item.id,
|
|
306
|
+
behavior_ref: hit.story.id,
|
|
307
|
+
label: `asked & built — confirm AGREE or flag DRIFT: "${truncate(item.text)}" ~ ${hit.story.title}`,
|
|
308
|
+
provenance: "suggested-structural",
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
else {
|
|
312
|
+
items.push({
|
|
313
|
+
category: "INTENT_ONLY",
|
|
314
|
+
intent_ref: item.id,
|
|
315
|
+
behavior_ref: null,
|
|
316
|
+
label: `you asked for this; not found in the build: "${truncate(item.text)}"`,
|
|
317
|
+
provenance: "suggested-structural",
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
for (const { story } of behaviorKw) {
|
|
322
|
+
if (matched.has(story.id))
|
|
323
|
+
continue;
|
|
324
|
+
items.push({
|
|
325
|
+
category: "CODE_ONLY",
|
|
326
|
+
intent_ref: null,
|
|
327
|
+
behavior_ref: story.id,
|
|
328
|
+
label: `built but never asked for: ${story.title}`,
|
|
329
|
+
provenance: "suggested-structural",
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
return items;
|
|
333
|
+
}
|
|
334
|
+
// --- Core flow + renderer ----------------------------------------------------
|
|
335
|
+
export function runSpecAfter(input) {
|
|
336
|
+
const { stories, coverage } = extractBehaviorStories(input.diff);
|
|
337
|
+
const hasPrompts = Boolean(input.promptsText && input.promptsText.trim());
|
|
338
|
+
const captured = hasPrompts && input.consent;
|
|
339
|
+
let intent = [];
|
|
340
|
+
let redaction = emptyRedaction();
|
|
341
|
+
let screening = emptyScreening();
|
|
342
|
+
if (captured) {
|
|
343
|
+
const distilled = distillIntent(input.promptsText);
|
|
344
|
+
intent = distilled.items;
|
|
345
|
+
redaction = distilled.redaction;
|
|
346
|
+
screening = distilled.screening;
|
|
347
|
+
}
|
|
348
|
+
const drift = suggestDrift(intent, stories).map((item) => !captured && item.category === "CODE_ONLY"
|
|
349
|
+
? { ...item, label: item.label.replace(/^built but never asked for:/, "built; requested intent unavailable:") }
|
|
350
|
+
: item);
|
|
351
|
+
return {
|
|
352
|
+
schema_version: "0.1",
|
|
353
|
+
tier: "v0a",
|
|
354
|
+
target: input.target,
|
|
355
|
+
consent: {
|
|
356
|
+
available: hasPrompts,
|
|
357
|
+
ingested: captured,
|
|
358
|
+
reason: !hasPrompts ? "prompt-file capture unavailable; no approved prompt context" : !input.consent ? "opt-in required (--consent)" : undefined,
|
|
359
|
+
},
|
|
360
|
+
prompt_context: { mode: captured ? "raw" : "metadata", captured, source: input.source },
|
|
361
|
+
redaction,
|
|
362
|
+
screening,
|
|
363
|
+
intent,
|
|
364
|
+
behavior: stories,
|
|
365
|
+
coverage,
|
|
366
|
+
drift,
|
|
367
|
+
pull_through: {
|
|
368
|
+
next: "foo asp reconcile <packet>",
|
|
369
|
+
note: "The win isn't this one-shot report — it's advancing into the reconcile loop so the spec stays honest to the code as it changes.",
|
|
370
|
+
},
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
function driftSection(report) {
|
|
374
|
+
const lines = [
|
|
375
|
+
report.prompt_context.captured
|
|
376
|
+
? "Drift report — what your agent built, and where it diverged from what you asked:"
|
|
377
|
+
: "Drift report — behavior found in the local Git diff; requested intent is unavailable:",
|
|
378
|
+
];
|
|
379
|
+
const order = ["INTENT_ONLY", "CODE_ONLY", "UNREVIEWED"];
|
|
380
|
+
const marks = { INTENT_ONLY: " ⚠ ", CODE_ONLY: " ⚠ ", UNREVIEWED: " • " };
|
|
381
|
+
let any = false;
|
|
382
|
+
for (const category of order) {
|
|
383
|
+
for (const item of report.drift.filter((d) => d.category === category)) {
|
|
384
|
+
any = true;
|
|
385
|
+
lines.push(`${marks[category]}[${category}] ${item.label}`);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
if (!any)
|
|
389
|
+
lines.push(" (no intent or behavior to reconcile yet)");
|
|
390
|
+
lines.push(" note: deterministic keyword-overlap suggestions — you reconcile. V0a does not auto-classify or canonize.");
|
|
391
|
+
return lines;
|
|
392
|
+
}
|
|
393
|
+
export function renderSpecAfterReport(report) {
|
|
394
|
+
const lines = [];
|
|
395
|
+
lines.push(`foo spec — spec-after drift report (${report.tier})`);
|
|
396
|
+
lines.push(`target: ${report.target}`);
|
|
397
|
+
lines.push("");
|
|
398
|
+
// Lead with the drift report (packet Decisions: Positioning).
|
|
399
|
+
lines.push(...driftSection(report));
|
|
400
|
+
lines.push("");
|
|
401
|
+
// Capture + safety status.
|
|
402
|
+
if (report.consent.ingested) {
|
|
403
|
+
const q = report.redaction.quarantined ? " — QUARANTINED tokens need a human pass before canonization" : "";
|
|
404
|
+
lines.push(`Capture: ingested ${report.intent.length} intent item(s) from ${report.prompt_context.source ?? "prompts"} → ` +
|
|
405
|
+
`${report.redaction.redacted_count} redaction(s)${q}. Raw prompts are run-scoped and never committed.`);
|
|
406
|
+
if (report.screening.triggered) {
|
|
407
|
+
lines.push(` ${report.screening.stripped.length} injected directive(s) neutralized as inert data (untrusted prompt stream, R-006).`);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
else if (report.consent.available) {
|
|
411
|
+
lines.push(`Capture: prompts available from ${report.prompt_context.source ?? "prompts"} but NOT ingested (${report.consent.reason}). ` +
|
|
412
|
+
"Re-run with --consent to ingest; prompts are redacted and never committed.");
|
|
413
|
+
}
|
|
414
|
+
else {
|
|
415
|
+
}
|
|
416
|
+
lines.push(`Coverage: ${report.coverage.story_files} behavior stor${report.coverage.story_files === 1 ? "y" : "ies"} ` +
|
|
417
|
+
`from ${report.coverage.scanned_files} changed section(s); skipped ${report.coverage.skipped.length}` +
|
|
418
|
+
`${report.coverage.skipped.length ? ` (${report.coverage.skipped.map((s) => `${s.file}:${s.reason}`).join(", ")})` : ""}.`);
|
|
419
|
+
lines.push("");
|
|
420
|
+
if (report.intent.length) {
|
|
421
|
+
lines.push(`Intent (${report.intent.length}):`);
|
|
422
|
+
for (const item of report.intent)
|
|
423
|
+
lines.push(` ${item.id}: ${item.text}`);
|
|
424
|
+
lines.push("");
|
|
425
|
+
}
|
|
426
|
+
if (report.behavior.length) {
|
|
427
|
+
lines.push(`Behavior (${report.behavior.length}):`);
|
|
428
|
+
for (const story of report.behavior)
|
|
429
|
+
lines.push(` ${story.id}: ${story.expected_behavior}`);
|
|
430
|
+
lines.push("");
|
|
431
|
+
}
|
|
432
|
+
lines.push("Next — pull through into the loop:");
|
|
433
|
+
lines.push(` ${report.pull_through.next}`);
|
|
434
|
+
lines.push(` ${report.pull_through.note}`);
|
|
435
|
+
return lines.join("\n");
|
|
436
|
+
}
|
|
437
|
+
export function parseSpecFromArgs(args) {
|
|
438
|
+
const options = { consent: false, json: false };
|
|
439
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
440
|
+
const arg = args[index];
|
|
441
|
+
if (arg === "--out") {
|
|
442
|
+
const value = args[index + 1];
|
|
443
|
+
if (!value)
|
|
444
|
+
throw new Error("--out requires a path");
|
|
445
|
+
options.out = value;
|
|
446
|
+
index += 1;
|
|
447
|
+
continue;
|
|
448
|
+
}
|
|
449
|
+
if (arg === "--consent" || arg === "--yes") {
|
|
450
|
+
options.consent = true;
|
|
451
|
+
continue;
|
|
452
|
+
}
|
|
453
|
+
if (arg === "--json") {
|
|
454
|
+
options.json = true;
|
|
455
|
+
continue;
|
|
456
|
+
}
|
|
457
|
+
// Name a flag, never a value. An unrecognized value is very often a path
|
|
458
|
+
// the caller did not mean to publish — `--from-prompts <private-file>` used
|
|
459
|
+
// to be refused before parsing, and echoing the stray operand back into an
|
|
460
|
+
// error message (and from there into logs) would reintroduce exactly the
|
|
461
|
+
// leak that guard existed to prevent.
|
|
462
|
+
throw new Error(arg.startsWith("--")
|
|
463
|
+
? `unknown spec from argument: ${arg}`
|
|
464
|
+
: "unknown spec from argument: unexpected value (pass --help to see the supported flags)");
|
|
465
|
+
}
|
|
466
|
+
return options;
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
469
|
+
* These probes are *expected* to fail — that is how they answer their question. gitRun echoes the
|
|
470
|
+
* child's stderr to the parent by default, so without this a routine "is this a revision?" check
|
|
471
|
+
* spits a raw `git fatal:` at the user mid-report. Silence the channel; keep the answer.
|
|
472
|
+
*/
|
|
473
|
+
const QUIET = { stdio: ["ignore", "pipe", "ignore"], maxBuffer: 32 * 1024 * 1024 };
|
|
474
|
+
/** Does this argument name something git can actually diff? A revision, or an `a..b` range. */
|
|
475
|
+
function isGitRevision(root, target) {
|
|
476
|
+
const ends = target.includes("..") ? target.split(/\.{2,3}/).filter(Boolean) : [target];
|
|
477
|
+
if (ends.length === 0)
|
|
478
|
+
return false;
|
|
479
|
+
return ends.every((end) => gitRunOrNull(["-C", root, "rev-parse", "--verify", "--quiet", `${end}^{commit}`], QUIET) !== null);
|
|
480
|
+
}
|
|
481
|
+
/**
|
|
482
|
+
* `foo spec from <what-you-built>` is documented — in `foo --help` and in AGENTS.md — as taking a
|
|
483
|
+
* plain-English description of the work. It used to pass that description straight to `git diff`,
|
|
484
|
+
* which meant the documented usage died on a raw `git fatal: ambiguous argument`. Following your own
|
|
485
|
+
* tool's help text into a stack trace is the fastest way to lose someone on their first run.
|
|
486
|
+
*
|
|
487
|
+
* So: resolve the argument as a revision first. If git knows it, diff it. If it doesn't, the argument
|
|
488
|
+
* is what it says it is — a description — and the thing the user means by "what I built" is the work
|
|
489
|
+
* sitting in front of them: uncommitted changes, or failing that, the last commit.
|
|
490
|
+
*/
|
|
491
|
+
function gitDiff(root, target) {
|
|
492
|
+
const spec = target === "." || target === "" ? "HEAD" : target;
|
|
493
|
+
if (isGitRevision(root, spec)) {
|
|
494
|
+
const result = gitRun(["-C", root, "diff", spec], { maxBuffer: 32 * 1024 * 1024 });
|
|
495
|
+
if (!result.ok)
|
|
496
|
+
throw new Error(result.message);
|
|
497
|
+
return result.stdout;
|
|
498
|
+
}
|
|
499
|
+
const working = gitRunOrNull(["-C", root, "diff", "HEAD"], { maxBuffer: 32 * 1024 * 1024 }) ?? "";
|
|
500
|
+
const untracked = untrackedDiff(root);
|
|
501
|
+
const uncommitted = `${working}${untracked}`;
|
|
502
|
+
if (uncommitted.trim())
|
|
503
|
+
return uncommitted;
|
|
504
|
+
const lastCommit = gitRunOrNull(["-C", root, "diff", "HEAD~1..HEAD"], QUIET);
|
|
505
|
+
if (lastCommit && lastCommit.trim())
|
|
506
|
+
return lastCommit;
|
|
507
|
+
return "";
|
|
508
|
+
}
|
|
509
|
+
/**
|
|
510
|
+
* `git diff HEAD` does not show untracked files, so a brand-new file — an entire new module, the
|
|
511
|
+
* whole point of most "what did my agent just build" questions — was invisible to the drift report.
|
|
512
|
+
* It would cheerfully tell you nothing had changed while a new service sat on disk.
|
|
513
|
+
*
|
|
514
|
+
* Synthesize a diff for each untracked file instead. `--no-index` against /dev/null gives a real
|
|
515
|
+
* unified diff and mutates nothing; `git add -N` would also work but writes to the user's index,
|
|
516
|
+
* and a tool that silently stages your files to answer a question is not one you keep.
|
|
517
|
+
*/
|
|
518
|
+
function untrackedDiff(root) {
|
|
519
|
+
const listing = gitRunOrNull(["-C", root, "ls-files", "--others", "--exclude-standard"]);
|
|
520
|
+
if (!listing)
|
|
521
|
+
return "";
|
|
522
|
+
const files = listing.split("\n").map((line) => line.trim()).filter(Boolean);
|
|
523
|
+
let out = "";
|
|
524
|
+
for (const file of files) {
|
|
525
|
+
// --no-index exits 1 when the files differ, which is the normal case here, so read stdout
|
|
526
|
+
// regardless of exit status rather than treating "they differ" as a failure.
|
|
527
|
+
const result = gitRun(["-C", root, "diff", "--no-index", "--", "/dev/null", file], QUIET);
|
|
528
|
+
if (result.stdout)
|
|
529
|
+
out += result.stdout;
|
|
530
|
+
}
|
|
531
|
+
return out;
|
|
532
|
+
}
|
|
533
|
+
export function runSpecFrom(root, target, args) {
|
|
534
|
+
const options = parseSpecFromArgs(args);
|
|
535
|
+
const diff = gitDiff(root, target);
|
|
536
|
+
const report = runSpecAfter({ target, diff, consent: options.consent });
|
|
537
|
+
const rendered = options.json ? JSON.stringify(report, null, 2) : renderSpecAfterReport(report);
|
|
538
|
+
const body = rendered.endsWith("\n") ? rendered : `${rendered}\n`;
|
|
539
|
+
if (options.out) {
|
|
540
|
+
// B1: only the redacted report is persisted; raw prompts stay run-scoped.
|
|
541
|
+
writeFileSync(path.resolve(root, options.out), body);
|
|
542
|
+
return `${body}\nWrote redacted report to ${options.out} (raw prompts not persisted).\n`;
|
|
543
|
+
}
|
|
544
|
+
return body;
|
|
545
|
+
}
|
|
546
|
+
const CODE_EXT = /\.(ts|tsx|js|jsx|mjs|cjs|py|go|rs|java|rb|php|c|h|cc|cpp|cs|swift|kt|scala|sh)$/i;
|
|
547
|
+
const NON_CODE_PREFIX = /^(specs\/|docs\/|dist\/|node_modules\/|\.githooks\/)/;
|
|
548
|
+
const FOO_SPEC_TRAILER = /^Foo-Spec:/m;
|
|
549
|
+
const SKIP_SUBJECT = /^(merge|chore|docs)\b/i;
|
|
550
|
+
function isCodeFile(file) {
|
|
551
|
+
return CODE_EXT.test(file) && !NON_CODE_PREFIX.test(file);
|
|
552
|
+
}
|
|
553
|
+
export function shouldNudgeSpecAfter(input) {
|
|
554
|
+
const codeFiles = input.changedFiles.filter(isCodeFile);
|
|
555
|
+
if (input.parentCount >= 2)
|
|
556
|
+
return { nudge: false, reason: "merge commit", codeFiles };
|
|
557
|
+
if (FOO_SPEC_TRAILER.test(input.message))
|
|
558
|
+
return { nudge: false, reason: "already spec-linked (Foo-Spec trailer)", codeFiles };
|
|
559
|
+
const subject = input.message.split(/\r?\n/, 1)[0] ?? "";
|
|
560
|
+
if (SKIP_SUBJECT.test(subject))
|
|
561
|
+
return { nudge: false, reason: "chore/docs/merge subject", codeFiles };
|
|
562
|
+
if (codeFiles.length === 0)
|
|
563
|
+
return { nudge: false, reason: "no product code changed", codeFiles };
|
|
564
|
+
return { nudge: true, reason: "code shipped without a spec link", codeFiles };
|
|
565
|
+
}
|
|
566
|
+
export function renderSpecNudge(decision) {
|
|
567
|
+
const n = decision.codeFiles.length;
|
|
568
|
+
return [
|
|
569
|
+
"foo · spec-after nudge (advisory — your commit is unchanged)",
|
|
570
|
+
` You shipped ${n} code file${n === 1 ? "" : "s"} with no spec link. Review the local Git behavior now:`,
|
|
571
|
+
" foo spec from HEAD~1..HEAD",
|
|
572
|
+
" Leads with behavior-only drift: the report compares the diff against the packet, not against prompts.",
|
|
573
|
+
" Signal, not a gate — silence with FOO_SPEC_NUDGE=0.",
|
|
574
|
+
].join("\n");
|
|
575
|
+
}
|
|
576
|
+
export function runSpecNudge(root) {
|
|
577
|
+
// fail open: outside a repo or no HEAD -> no nudge
|
|
578
|
+
const shown = gitRunOrNull(["-C", root, "show", "--name-only", "--pretty=format:", "HEAD"]);
|
|
579
|
+
if (shown === null)
|
|
580
|
+
return "";
|
|
581
|
+
const changedFiles = shown
|
|
582
|
+
.split(/\r?\n/)
|
|
583
|
+
.map((line) => line.trim())
|
|
584
|
+
.filter(Boolean);
|
|
585
|
+
const message = gitRunOrNull(["-C", root, "log", "-1", "--pretty=%B"]);
|
|
586
|
+
if (message === null)
|
|
587
|
+
return "";
|
|
588
|
+
const parentsOut = gitRunOrNull(["-C", root, "show", "-s", "--pretty=%P", "HEAD"]);
|
|
589
|
+
if (parentsOut === null)
|
|
590
|
+
return "";
|
|
591
|
+
const parents = parentsOut.trim();
|
|
592
|
+
const parentCount = parents ? parents.split(/\s+/).length : 0;
|
|
593
|
+
const decision = shouldNudgeSpecAfter({ changedFiles, message, parentCount });
|
|
594
|
+
return decision.nudge ? `${renderSpecNudge(decision)}\n` : "";
|
|
595
|
+
}
|
|
596
|
+
export const PANEL_LENSES = [
|
|
597
|
+
"correctness",
|
|
598
|
+
"security-safety",
|
|
599
|
+
"intent-fidelity",
|
|
600
|
+
"subtraction-red-team",
|
|
601
|
+
];
|
|
602
|
+
export function runQaPanel(report) {
|
|
603
|
+
const findings = [];
|
|
604
|
+
let n = 0;
|
|
605
|
+
const add = (lens, severity, label, refs = {}) => {
|
|
606
|
+
n += 1;
|
|
607
|
+
findings.push({ id: `F-${String(n).padStart(3, "0")}`, lens, severity, label, refs });
|
|
608
|
+
};
|
|
609
|
+
// security-safety: quarantined secret-shaped tokens are a hard-block — the
|
|
610
|
+
// stream cannot be canonized until a human clears the quarantine.
|
|
611
|
+
if (report.redaction.quarantined) {
|
|
612
|
+
add("security-safety", "hard-block", "quarantined secret-shaped token(s) in the captured stream need a human pass before canonization");
|
|
613
|
+
}
|
|
614
|
+
// Neutralized injections are audited, not executed (R-006): reviewable, not a block.
|
|
615
|
+
if (report.screening.triggered) {
|
|
616
|
+
add("security-safety", "finding", `${report.screening.stripped.length} injected directive(s) were neutralized as inert data — review before trusting distilled intent`);
|
|
617
|
+
}
|
|
618
|
+
for (const item of report.drift) {
|
|
619
|
+
if (item.category === "CODE_ONLY") {
|
|
620
|
+
add("correctness", "finding", `behavior with no stated intent: ${item.label}`, { behavior_ref: item.behavior_ref ?? undefined });
|
|
621
|
+
add("subtraction-red-team", "finding", `unrequested change — candidate for subtraction or an intent backfill: ${item.label}`, { behavior_ref: item.behavior_ref ?? undefined });
|
|
622
|
+
}
|
|
623
|
+
if (item.category === "INTENT_ONLY") {
|
|
624
|
+
add("intent-fidelity", "finding", `stated intent with no observed implementation: ${item.label}`, { drift_ref: item.intent_ref ?? undefined });
|
|
625
|
+
}
|
|
626
|
+
if (item.category === "UNREVIEWED") {
|
|
627
|
+
add("correctness", "finding", `unreviewed pairing — verify before canonizing: ${item.label}`, { drift_ref: item.intent_ref ?? undefined, behavior_ref: item.behavior_ref ?? undefined });
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
return {
|
|
631
|
+
schema_version: "0.1",
|
|
632
|
+
lenses: PANEL_LENSES,
|
|
633
|
+
findings,
|
|
634
|
+
security_blocked: findings.some((f) => f.severity === "hard-block"),
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
// Accept-as-known: an attributed, append-only ledger record on the TARGET
|
|
638
|
+
// packet. Never called by the panel or canonize themselves — a human invokes
|
|
639
|
+
// it, and canonize only honors accepts that are already on the ledger.
|
|
640
|
+
export function acceptPanelFinding(packetDir, opts) {
|
|
641
|
+
if (!opts.by.trim() || !opts.reason.trim())
|
|
642
|
+
throw new Error("accept-as-known needs --by <who> and --reason <why>");
|
|
643
|
+
const record = {
|
|
644
|
+
specversion: "0.1",
|
|
645
|
+
id: `evt_specafter_accept_${opts.finding_id}_${Date.now()}`.replace(/[^a-zA-Z0-9_]/g, "_"),
|
|
646
|
+
source: "agent://foo/spec-after",
|
|
647
|
+
type: "com.adaptivespec.specafter.finding_accepted",
|
|
648
|
+
subject: `finding:${opts.finding_id}`,
|
|
649
|
+
time: opts.at,
|
|
650
|
+
actor: { type: "human", id: opts.by, mode: "human-in-loop" },
|
|
651
|
+
data: { finding_id: opts.finding_id, lens: opts.lens, label: opts.label, reason: opts.reason },
|
|
652
|
+
extensions: {},
|
|
653
|
+
};
|
|
654
|
+
appendFileSync(path.join(packetDir, "events.jsonl"), `${JSON.stringify(record)}\n`);
|
|
655
|
+
}
|
|
656
|
+
export function acceptedFindingIds(packetDir) {
|
|
657
|
+
const file = path.join(packetDir, "events.jsonl");
|
|
658
|
+
if (!existsSync(file))
|
|
659
|
+
return new Set();
|
|
660
|
+
const ids = new Set();
|
|
661
|
+
for (const line of readFileSync(file, "utf8").split("\n")) {
|
|
662
|
+
if (!line.trim())
|
|
663
|
+
continue;
|
|
664
|
+
try {
|
|
665
|
+
const evt = JSON.parse(line);
|
|
666
|
+
if (evt.type === "com.adaptivespec.specafter.finding_accepted" && evt.data?.finding_id)
|
|
667
|
+
ids.add(String(evt.data.finding_id));
|
|
668
|
+
}
|
|
669
|
+
catch {
|
|
670
|
+
// tolerate foreign ledger lines
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
return ids;
|
|
674
|
+
}
|
|
675
|
+
export function computeAgreeBatch(report) {
|
|
676
|
+
// The deterministic join marks suggested matches UNREVIEWED — those pairs ARE
|
|
677
|
+
// the AGREE candidates, and every one goes through human eyes at the confirm
|
|
678
|
+
// batch (a near-AGREE that merely overlaps keywords is caught there, C-008).
|
|
679
|
+
return report.drift
|
|
680
|
+
.filter((d) => d.category === "UNREVIEWED" && d.intent_ref && d.behavior_ref)
|
|
681
|
+
.map((d) => ({ intent_ref: d.intent_ref, behavior_ref: d.behavior_ref, label: d.label }));
|
|
682
|
+
}
|
|
683
|
+
export function canonizeSpecAfter(root, report, panel, opts) {
|
|
684
|
+
const agree = computeAgreeBatch(report);
|
|
685
|
+
const relDir = opts.packetName.includes("/") ? opts.packetName : path.posix.join("specs", opts.packetName);
|
|
686
|
+
const packetDir = path.resolve(root, relDir);
|
|
687
|
+
// Security hard-blocks refuse canonization unless each carries a prior
|
|
688
|
+
// attributed accept on the target packet's ledger (never silent, C-007).
|
|
689
|
+
const accepted = existsSync(packetDir) ? acceptedFindingIds(packetDir) : new Set();
|
|
690
|
+
const blocking = panel.findings.filter((f) => f.severity === "hard-block" && !accepted.has(f.id));
|
|
691
|
+
if (blocking.length > 0) {
|
|
692
|
+
return { canonized: false, reason: `security hard-block: ${blocking.map((f) => f.id).join(", ")} — accept-as-known (attributed) or resolve before canonizing`, agree_batch: agree, blocking };
|
|
693
|
+
}
|
|
694
|
+
// No silent AGREE: the batch canonizes only under an explicit, attributed
|
|
695
|
+
// human confirm. This is the moment a seeded near-AGREE is caught (C-008).
|
|
696
|
+
if (!opts.confirmBatch || !opts.by?.trim()) {
|
|
697
|
+
return { canonized: false, reason: "confirm-batch required: review the AGREE batch and re-run with --confirm-batch --by <who>", agree_batch: agree, blocking: [] };
|
|
698
|
+
}
|
|
699
|
+
if (existsSync(path.join(packetDir, "spec.md"))) {
|
|
700
|
+
return { canonized: false, reason: `refusing to overwrite existing packet: ${relDir}`, agree_batch: agree, blocking: [] };
|
|
701
|
+
}
|
|
702
|
+
mkdirSync(packetDir, { recursive: true });
|
|
703
|
+
const base = path.basename(packetDir);
|
|
704
|
+
const specId = base.replace(/-/g, "_");
|
|
705
|
+
const isoDate = opts.at.slice(0, 10);
|
|
706
|
+
const carried = report.drift.filter((d) => d.category !== "UNREVIEWED");
|
|
707
|
+
const agreeLines = agree.map((p, i) => `- R-${String(i + 1).padStart(3, "0")}: ${p.label} (confirmed AGREE, ${p.intent_ref} ↔ ${p.behavior_ref})`);
|
|
708
|
+
const driftLines = carried.map((d) => `- [${d.category}] ${d.label}`);
|
|
709
|
+
const intentSummary = report.prompt_context.captured
|
|
710
|
+
? `Canonized by \`foo spec canonize\` from a spec-after drift report for \`${report.target}\` — verified intent recovered from the captured prompt stream, confirmed by ${opts.by} on ${isoDate}. This spec stays live through \`foo asp reconcile\`; brand build commits with \`foo commit-trailer ${relDir}\`.`
|
|
711
|
+
: `Canonized by \`foo spec canonize\` from a behavior-only spec-after report for \`${report.target}\`, confirmed by ${opts.by} on ${isoDate}. No prompt-file intent was ingested; requested intent remains unknown. This spec stays live through \`foo asp reconcile\`; brand build commits with \`foo commit-trailer ${relDir}\`.`;
|
|
712
|
+
const understanding = report.prompt_context.captured
|
|
713
|
+
? "Intent was distilled from the redacted, screened prompt stream; behavior from the diff. The QA panel ran review-only (fixed lens set); no directive from the captured stream was executed."
|
|
714
|
+
: "Behavior came from the local Git diff. Requested intent was not inferred from absent prompt context; the QA panel ran review-only with its fixed lens set.";
|
|
715
|
+
const evidence = report.prompt_context.captured
|
|
716
|
+
? `Redacted spec-after report for ${report.target} (run-scoped; raw prompts never persisted).`
|
|
717
|
+
: `Behavior-only spec-after report for ${report.target} (prompt-file capture unavailable; no prompt path was read).`;
|
|
718
|
+
writeFileSync(path.join(packetDir, "spec.md"), `---\nspec_id: ${specId}\ntitle: ${base}\nmode: capability\nlifecycle: draft\nstage: shaping\nowner: product\nautonomy: human-in-loop\nupdated: ${isoDate}\n---\n\n# ${base}\n\n## Intent\n\n${intentSummary}\n\n## Current Understanding\n\n${understanding}\n\n## Requirements\n\n${agreeLines.length ? agreeLines.join("\n") : "- (no confirmed AGREE pairs — drift-only capture)"}\n\n## Decisions\n\n- Canonized via explicit human confirm-batch by ${opts.by} at ${opts.at}; no category auto-canonized.\n\n## Risks\n\n- Carried drift items below are unreconciled intent/behavior residue.\n\n## Validation\n\n- foo asp validate ${relDir}; drift tracked via foo asp reconcile ${relDir}.\n\n## Evidence\n\n- ${evidence}\n\n## Open Questions\n\n${driftLines.length ? driftLines.map((l) => `${l} — reconcile before promoting`).join("\n") : "- none carried"}\n`);
|
|
719
|
+
const taskYamlBlock = (t) => [
|
|
720
|
+
` - id: ${t.id}`,
|
|
721
|
+
` title: ${JSON.stringify(t.title)}`,
|
|
722
|
+
` status: ${t.status}`,
|
|
723
|
+
" agreement: agreed",
|
|
724
|
+
" stage: shaping",
|
|
725
|
+
" refs: []",
|
|
726
|
+
" depends_on: []",
|
|
727
|
+
" owner: product",
|
|
728
|
+
" autonomy: human-in-loop",
|
|
729
|
+
" do:",
|
|
730
|
+
` - ${JSON.stringify(t.title)}`,
|
|
731
|
+
" done_when:",
|
|
732
|
+
` - id: ${t.check.id}`,
|
|
733
|
+
` text: ${JSON.stringify(t.check.text)}`,
|
|
734
|
+
` done: ${t.check.done}`,
|
|
735
|
+
` proof: ${t.check.proof === null ? "null" : JSON.stringify(t.check.proof)}`,
|
|
736
|
+
...(t.status === "done"
|
|
737
|
+
? [
|
|
738
|
+
" completed:",
|
|
739
|
+
` at: ${JSON.stringify(opts.at)}`,
|
|
740
|
+
` by: ${JSON.stringify(opts.by ?? "human")}`,
|
|
741
|
+
` summary: ${JSON.stringify("confirmed AGREE at canonization")}`,
|
|
742
|
+
" event: null",
|
|
743
|
+
]
|
|
744
|
+
: [" completed: null"]),
|
|
745
|
+
" blocked: null",
|
|
746
|
+
" review:",
|
|
747
|
+
" owner: product",
|
|
748
|
+
" due: null",
|
|
749
|
+
" stale_after_days: 7",
|
|
750
|
+
" extensions: {}",
|
|
751
|
+
];
|
|
752
|
+
const taskLines = [
|
|
753
|
+
...agree.map((p, i) => taskYamlBlock({
|
|
754
|
+
id: `T-${String(i + 1).padStart(3, "0")}`,
|
|
755
|
+
title: `Keep confirmed behavior true: ${truncate(p.label, 80)}`,
|
|
756
|
+
status: "done",
|
|
757
|
+
check: {
|
|
758
|
+
id: `C-${String(i + 1).padStart(3, "0")}`,
|
|
759
|
+
text: p.label,
|
|
760
|
+
done: true,
|
|
761
|
+
proof: `confirmed AGREE by ${opts.by} at ${opts.at} (${p.intent_ref} ↔ ${p.behavior_ref})`,
|
|
762
|
+
},
|
|
763
|
+
})),
|
|
764
|
+
...carried.map((d, i) => taskYamlBlock({
|
|
765
|
+
id: `T-${String(agree.length + i + 1).padStart(3, "0")}`,
|
|
766
|
+
title: `Reconcile drift: ${truncate(d.label, 80)}`,
|
|
767
|
+
status: "todo",
|
|
768
|
+
check: {
|
|
769
|
+
id: `C-${String(agree.length + i + 1).padStart(3, "0")}`,
|
|
770
|
+
text: `${d.category} resolved: ${d.label}`,
|
|
771
|
+
done: false,
|
|
772
|
+
proof: null,
|
|
773
|
+
},
|
|
774
|
+
})),
|
|
775
|
+
].flat();
|
|
776
|
+
const tasksYaml = [
|
|
777
|
+
`schema_version: "0.1"`,
|
|
778
|
+
`spec_id: ${specId}`,
|
|
779
|
+
`spec_ref: ${relDir}/spec.md`,
|
|
780
|
+
`updated: ${JSON.stringify(opts.at)}`,
|
|
781
|
+
"defaults:",
|
|
782
|
+
" autonomy: human-in-loop",
|
|
783
|
+
" stale_after_days:",
|
|
784
|
+
" suggested: 7",
|
|
785
|
+
" discovered: 7",
|
|
786
|
+
"ledger:",
|
|
787
|
+
" segment_order:",
|
|
788
|
+
" - events.jsonl",
|
|
789
|
+
"tasks:",
|
|
790
|
+
...taskLines,
|
|
791
|
+
"",
|
|
792
|
+
].join("\n");
|
|
793
|
+
writeFileSync(path.join(packetDir, "tasks.yaml"), tasksYaml);
|
|
794
|
+
const canonEvent = {
|
|
795
|
+
specversion: "0.1",
|
|
796
|
+
id: `evt_specafter_canonized_${Date.now()}`,
|
|
797
|
+
source: "agent://foo/spec-after",
|
|
798
|
+
type: "com.adaptivespec.specafter.canonized",
|
|
799
|
+
subject: `packet:${relDir}`,
|
|
800
|
+
time: opts.at,
|
|
801
|
+
actor: { type: "human", id: opts.by, mode: "human-in-loop" },
|
|
802
|
+
data: {
|
|
803
|
+
target: report.target,
|
|
804
|
+
agree_confirmed: agree.length,
|
|
805
|
+
drift_carried: carried.length,
|
|
806
|
+
// T-009: registration with the reconcile loop — the packet's live-ness
|
|
807
|
+
// contract is `foo asp reconcile <dir>` plus spec-linked commit trailers.
|
|
808
|
+
reconcile: { registered: true, command: `foo asp reconcile ${relDir}`, trailer: `foo commit-trailer ${relDir}` },
|
|
809
|
+
},
|
|
810
|
+
extensions: {},
|
|
811
|
+
};
|
|
812
|
+
writeFileSync(path.join(packetDir, "events.jsonl"), `${JSON.stringify(canonEvent)}\n`);
|
|
813
|
+
return { canonized: true, packetDir, agree_batch: agree, blocking: [] };
|
|
814
|
+
}
|