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,89 @@
|
|
|
1
|
+
const MAX_SECRET_BYTES = 8192;
|
|
2
|
+
export async function readSecretFromStream(input, maxBytes = MAX_SECRET_BYTES) {
|
|
3
|
+
const chunks = [];
|
|
4
|
+
let total = 0;
|
|
5
|
+
for await (const chunkValue of input) {
|
|
6
|
+
const chunk = Buffer.isBuffer(chunkValue) ? chunkValue : Buffer.from(String(chunkValue));
|
|
7
|
+
total += chunk.length;
|
|
8
|
+
if (total > maxBytes)
|
|
9
|
+
throw new Error("secret input is too large");
|
|
10
|
+
chunks.push(chunk);
|
|
11
|
+
}
|
|
12
|
+
const bytes = Buffer.concat(chunks);
|
|
13
|
+
const text = bytes.toString("utf8");
|
|
14
|
+
if (!Buffer.from(text, "utf8").equals(bytes))
|
|
15
|
+
throw new Error("secret input is not valid UTF-8");
|
|
16
|
+
bytes.fill(0);
|
|
17
|
+
const trimmed = text.trim();
|
|
18
|
+
if (!trimmed)
|
|
19
|
+
throw new Error("secret input is empty");
|
|
20
|
+
return trimmed;
|
|
21
|
+
}
|
|
22
|
+
export function readMaskedSecret(io) {
|
|
23
|
+
const input = io.input;
|
|
24
|
+
const output = io.output;
|
|
25
|
+
if (!input.isTTY || typeof input.setRawMode !== "function") {
|
|
26
|
+
return Promise.reject(new Error("masked secret input requires an interactive terminal"));
|
|
27
|
+
}
|
|
28
|
+
output.write(io.prompt);
|
|
29
|
+
return new Promise((resolve, reject) => {
|
|
30
|
+
let secret = "";
|
|
31
|
+
let settled = false;
|
|
32
|
+
const finish = (error) => {
|
|
33
|
+
if (settled)
|
|
34
|
+
return;
|
|
35
|
+
settled = true;
|
|
36
|
+
input.off("data", onData);
|
|
37
|
+
try {
|
|
38
|
+
input.setRawMode(false);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
// Best effort terminal restoration after the descriptor disappears.
|
|
42
|
+
}
|
|
43
|
+
input.pause();
|
|
44
|
+
output.write("\n");
|
|
45
|
+
if (error)
|
|
46
|
+
reject(error);
|
|
47
|
+
else if (!secret.trim())
|
|
48
|
+
reject(new Error("secret input is empty"));
|
|
49
|
+
else
|
|
50
|
+
resolve(secret.trim());
|
|
51
|
+
};
|
|
52
|
+
const onData = (chunkValue) => {
|
|
53
|
+
const chunk = Buffer.isBuffer(chunkValue) ? chunkValue.toString("utf8") : chunkValue;
|
|
54
|
+
for (const character of chunk) {
|
|
55
|
+
if (character === "\u0003" || character === "\u0004") {
|
|
56
|
+
finish(new Error("secret input cancelled"));
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
if (character === "\r" || character === "\n") {
|
|
60
|
+
finish();
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (character === "\u007f" || character === "\b") {
|
|
64
|
+
if (secret.length > 0) {
|
|
65
|
+
secret = secret.slice(0, -1);
|
|
66
|
+
output.write("\b \b");
|
|
67
|
+
}
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (character >= " " && character <= "~") {
|
|
71
|
+
if (Buffer.byteLength(secret) >= MAX_SECRET_BYTES) {
|
|
72
|
+
finish(new Error("secret input is too large"));
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
secret += character;
|
|
76
|
+
output.write("*");
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
try {
|
|
81
|
+
input.setRawMode(true);
|
|
82
|
+
input.resume();
|
|
83
|
+
input.on("data", onData);
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
finish(error instanceof Error ? error : new Error("could not read masked secret"));
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export type SelfCheckResult = {
|
|
2
|
+
output: string;
|
|
3
|
+
exitCode: number;
|
|
4
|
+
};
|
|
5
|
+
/** Supported checks for this build. */
|
|
6
|
+
export declare function selfUsage(): string;
|
|
7
|
+
export declare function runSelfCheck(root: string, command: string | undefined, args: string[]): SelfCheckResult;
|
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { UsageError } from "./errorTaxonomy.js";
|
|
4
|
+
import { FOO_COMMAND_CONTRACT, validateCommandDocs, validateCommittedLaunchers } from "./commandContract.js";
|
|
5
|
+
import { gitRunOrNull } from "./gitRun.js";
|
|
6
|
+
import { commitTrailerSignal } from "./specLinkedCommits.js";
|
|
7
|
+
import { PII_PROFILE, scanText } from "./redactEngine.js";
|
|
8
|
+
import { loadRepoAllowlist, REDACT_ALLOW_FILE } from "./redactConfig.js";
|
|
9
|
+
import { ARMED_LOOP_STATES, loadLoopContracts } from "./loopContracts.js";
|
|
10
|
+
import { estimateLoopBudget, handshakeFloorUsd, hasExternalSources, loadPricing } from "./loopBudget.js";
|
|
11
|
+
import { allowlistDigest, loadScanCache, saveScanCache, SCAN_DETECTOR_VERSION, } from "./selfCheckCache.js";
|
|
12
|
+
const SECRET_NAME = /(^|[._-])(secret|secrets|credential|credentials|transcript|transcripts)([._-]|$)/i;
|
|
13
|
+
const SECRET_EXT = /\.(env|key|pem|pfx|p12)$/i;
|
|
14
|
+
// B1 for feedback content (feedback_triage_v0 RISK-001). A feedback document is
|
|
15
|
+
// raw private material about a named person, so it may not be tracked until it
|
|
16
|
+
// has been through the `pii` profile.
|
|
17
|
+
//
|
|
18
|
+
// Recognising feedback content is deliberately NARROW, and it is the whole
|
|
19
|
+
// design decision here. Matching on the word "feedback" anywhere would fire on
|
|
20
|
+
// src/feedback.ts and every skill that mentions the word — a gate that fires on
|
|
21
|
+
// the mention of a risk rather than the risk, which is the failure this repo has
|
|
22
|
+
// now learned four times. So a tracked file is feedback content only when it
|
|
23
|
+
// says so, or when it sits in a directory that exists to hold it:
|
|
24
|
+
// 1. it carries an explicit `foo-feedback: true` marker line (what
|
|
25
|
+
// `foo triage-feedback` will write), or
|
|
26
|
+
// 2. it is a .md/.txt document inside a directory named `feedback`.
|
|
27
|
+
// Everything else — including source files about feedback — is untouched.
|
|
28
|
+
const FEEDBACK_DOC_EXT = /\.(md|txt)$/i;
|
|
29
|
+
const FEEDBACK_MARKER = /^\s*(?:#\s*)?foo-feedback:\s*true\s*$/im;
|
|
30
|
+
function isFeedbackContent(file, text) {
|
|
31
|
+
if (FEEDBACK_MARKER.test(text))
|
|
32
|
+
return true;
|
|
33
|
+
return FEEDBACK_DOC_EXT.test(file) && file.split("/").slice(0, -1).includes("feedback");
|
|
34
|
+
}
|
|
35
|
+
function trackedFiles(root) {
|
|
36
|
+
const out = gitRunOrNull(["ls-files"], { cwd: root });
|
|
37
|
+
if (out === null)
|
|
38
|
+
return null;
|
|
39
|
+
return out
|
|
40
|
+
.split(/\r?\n/)
|
|
41
|
+
.filter((line) => line.trim().length > 0);
|
|
42
|
+
}
|
|
43
|
+
// Crude binary guard: a NUL byte in the first chunk means skip (not text).
|
|
44
|
+
function isProbablyText(buf) {
|
|
45
|
+
const n = Math.min(buf.length, 4096);
|
|
46
|
+
for (let i = 0; i < n; i++)
|
|
47
|
+
if (buf[i] === 0)
|
|
48
|
+
return false;
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
// The two B1 messages, built in exactly one place so a cached finding and a
|
|
52
|
+
// live scan produce byte-identical output.
|
|
53
|
+
function secretFindingMessage(file, f) {
|
|
54
|
+
return `high-confidence ${f.detector} in ${file}:${f.line} — redact (\`foo redact\`) or mark the line with a foo-redact:allow pragma`;
|
|
55
|
+
}
|
|
56
|
+
function piiFindingMessage(file, f) {
|
|
57
|
+
return `unredacted personal detail (${f.detector}) in tracked feedback content ${file}:${f.line} — run \`foo redact ${file} --profile secrets,pii --write\` before tracking it`;
|
|
58
|
+
}
|
|
59
|
+
function pushFileFindings(findings, file, scan) {
|
|
60
|
+
for (const f of scan.findings) {
|
|
61
|
+
findings.push({ id: "B1", message: secretFindingMessage(file, f) });
|
|
62
|
+
}
|
|
63
|
+
if (scan.pii === null)
|
|
64
|
+
return;
|
|
65
|
+
for (const f of scan.pii) {
|
|
66
|
+
findings.push({ id: "B1", message: piiFindingMessage(file, f) });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function scanTrackedFile(absPath, relPath, allowlist) {
|
|
70
|
+
let buf;
|
|
71
|
+
try {
|
|
72
|
+
buf = readFileSync(absPath);
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return null; // tracked but unreadable (deleted, permissions) — same skip as before
|
|
76
|
+
}
|
|
77
|
+
if (buf.length > 512 * 1024)
|
|
78
|
+
return { skipped: "large", findings: [], pii: null };
|
|
79
|
+
if (!isProbablyText(buf))
|
|
80
|
+
return { skipped: "binary", findings: [], pii: null };
|
|
81
|
+
const text = buf.toString("utf8");
|
|
82
|
+
const findings = [];
|
|
83
|
+
for (const f of scanText(text, { allowlist }).findings) {
|
|
84
|
+
if (f.confidence === "high")
|
|
85
|
+
findings.push({ detector: f.detector, line: f.line });
|
|
86
|
+
}
|
|
87
|
+
// Feedback content gets the pii profile on top of the secrets scan (only
|
|
88
|
+
// high-confidence personal identifiers block — see the isFeedbackContent
|
|
89
|
+
// note above). `pii: null` records "not feedback content" so the warm path
|
|
90
|
+
// reproduces the exact decision without re-reading.
|
|
91
|
+
let pii = null;
|
|
92
|
+
if (isFeedbackContent(relPath, text)) {
|
|
93
|
+
pii = [];
|
|
94
|
+
for (const f of scanText(text, { allowlist, profiles: [PII_PROFILE] }).findings) {
|
|
95
|
+
if (f.confidence === "high")
|
|
96
|
+
pii.push({ detector: f.detector, line: f.line });
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return { findings, pii };
|
|
100
|
+
}
|
|
101
|
+
function statSafe(absPath) {
|
|
102
|
+
try {
|
|
103
|
+
// statSync (not lstat): readFileSync follows symlinks, so the content key
|
|
104
|
+
// must describe the target, not the link.
|
|
105
|
+
const st = statSync(absPath);
|
|
106
|
+
return { mtimeMs: st.mtimeMs, size: st.size };
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
// A cache entry is a hit only when the file it describes is unchanged on disk:
|
|
113
|
+
// same mtime, same size. Anything else reads as cold — a rescan.
|
|
114
|
+
function cacheHit(absPath, entry) {
|
|
115
|
+
const st = statSafe(absPath);
|
|
116
|
+
return st !== null && st.mtimeMs === entry.mtimeMs && st.size === entry.size;
|
|
117
|
+
}
|
|
118
|
+
function sameEntries(a, b) {
|
|
119
|
+
const keysA = Object.keys(a);
|
|
120
|
+
if (keysA.length !== Object.keys(b).length)
|
|
121
|
+
return false;
|
|
122
|
+
for (const key of keysA) {
|
|
123
|
+
if (JSON.stringify(a[key]) !== JSON.stringify(b[key]))
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
function checkSafety(root, tracked, full) {
|
|
129
|
+
if (!tracked)
|
|
130
|
+
return { findings: [], verified: false };
|
|
131
|
+
const findings = [];
|
|
132
|
+
for (const file of tracked) {
|
|
133
|
+
const base = path.basename(file);
|
|
134
|
+
if (file === ".foo" || file.startsWith(".foo/")) {
|
|
135
|
+
findings.push({ id: "B1", message: `tracked file under local productfoo state: ${file}` });
|
|
136
|
+
}
|
|
137
|
+
else if (["docs/pitch", "docs/playbooks", "docs/research"].some((prefix) => file === prefix || file.startsWith(`${prefix}/`))) {
|
|
138
|
+
findings.push({ id: "B1", message: `tracked file under legacy private scratch: ${file}` });
|
|
139
|
+
}
|
|
140
|
+
else if (SECRET_NAME.test(base) || SECRET_EXT.test(base)) {
|
|
141
|
+
findings.push({ id: "B1", message: `tracked file looks like raw private material: ${file}` });
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
const allowlist = loadRepoAllowlist(root);
|
|
145
|
+
const head = gitRunOrNull(["rev-parse", "HEAD"], { cwd: root })?.trim() ?? null;
|
|
146
|
+
const allowDigest = allowlistDigest(root, REDACT_ALLOW_FILE);
|
|
147
|
+
// --full rescans every tracked file (the first-run/CI path, on demand) — but
|
|
148
|
+
// it still records results, so it doubles as a cache refresh.
|
|
149
|
+
const cache = full ? null : loadScanCache(root, head, SCAN_DETECTOR_VERSION, allowDigest);
|
|
150
|
+
// Caching is on whenever the run is keyable (HEAD known) — a cold run records
|
|
151
|
+
// entries too, that is how the cache warms. `cache` being null only means no
|
|
152
|
+
// prior results to reuse.
|
|
153
|
+
const caching = head !== null;
|
|
154
|
+
const entries = {};
|
|
155
|
+
for (const file of tracked) {
|
|
156
|
+
if (file === REDACT_ALLOW_FILE)
|
|
157
|
+
continue; // the allowlist itself lists secret-shaped values
|
|
158
|
+
const abs = path.join(root, file);
|
|
159
|
+
if (cache) {
|
|
160
|
+
const cached = cache.entries[file];
|
|
161
|
+
if (cached && cacheHit(abs, cached)) {
|
|
162
|
+
entries[file] = cached;
|
|
163
|
+
pushFileFindings(findings, file, cached);
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
const scan = scanTrackedFile(abs, file, allowlist);
|
|
168
|
+
if (scan === null)
|
|
169
|
+
continue; // unreadable — same skip as before, nothing to cache
|
|
170
|
+
if (caching) {
|
|
171
|
+
const st = statSafe(abs);
|
|
172
|
+
if (st)
|
|
173
|
+
entries[file] = { ...scan, mtimeMs: st.mtimeMs, size: st.size };
|
|
174
|
+
}
|
|
175
|
+
pushFileFindings(findings, file, scan);
|
|
176
|
+
}
|
|
177
|
+
// Skip the rewrite when nothing changed: a warm run over an unchanged tree
|
|
178
|
+
// stays read-only. `repo` is resolved so the same repo reached as "." and as
|
|
179
|
+
// an absolute path shares one cache file and one validity check.
|
|
180
|
+
if (caching && (cache === null || !sameEntries(entries, cache.entries))) {
|
|
181
|
+
saveScanCache({
|
|
182
|
+
schema_version: 1,
|
|
183
|
+
repo: path.resolve(root),
|
|
184
|
+
head,
|
|
185
|
+
detector_version: SCAN_DETECTOR_VERSION,
|
|
186
|
+
allowlist_digest: allowDigest,
|
|
187
|
+
entries,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
return { findings, verified: true };
|
|
191
|
+
}
|
|
192
|
+
function specHomes(root) {
|
|
193
|
+
const homes = [];
|
|
194
|
+
for (const dir of ["specs", "examples"]) {
|
|
195
|
+
const base = path.join(root, dir);
|
|
196
|
+
if (!existsSync(base))
|
|
197
|
+
continue;
|
|
198
|
+
for (const entry of readdirSync(base, { withFileTypes: true })) {
|
|
199
|
+
if (!entry.isDirectory())
|
|
200
|
+
continue;
|
|
201
|
+
const specPath = path.join(base, entry.name, "spec.md");
|
|
202
|
+
if (existsSync(specPath))
|
|
203
|
+
homes.push(specPath);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return homes;
|
|
207
|
+
}
|
|
208
|
+
// B2 — every committed spec.md has a present, unique Spec ID, read from YAML
|
|
209
|
+
// frontmatter (`spec_id`, canonical per T-013) or the transitional `Spec ID:`
|
|
210
|
+
// line.
|
|
211
|
+
function specIdFor(specText) {
|
|
212
|
+
if (specText.startsWith("---")) {
|
|
213
|
+
const end = specText.indexOf("\n---", 3);
|
|
214
|
+
if (end !== -1) {
|
|
215
|
+
const fm = specText.slice(3, end).match(/^spec_id:\s*(\S+)\s*$/m);
|
|
216
|
+
if (fm)
|
|
217
|
+
return fm[1];
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
const line = specText.match(/^Spec ID:\s*(\S+)\s*$/m);
|
|
221
|
+
return line ? line[1] : null;
|
|
222
|
+
}
|
|
223
|
+
function checkSpecIdentity(root) {
|
|
224
|
+
const findings = [];
|
|
225
|
+
const seen = new Map();
|
|
226
|
+
for (const specPath of specHomes(root)) {
|
|
227
|
+
const rel = path.relative(root, specPath);
|
|
228
|
+
const id = specIdFor(readFileSync(specPath, "utf8"));
|
|
229
|
+
if (!id) {
|
|
230
|
+
findings.push({ id: "B2", message: `${rel} is missing a Spec ID` });
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
const prior = seen.get(id);
|
|
234
|
+
if (prior)
|
|
235
|
+
findings.push({ id: "B2", message: `duplicate Spec ID ${id}: ${rel} and ${prior}` });
|
|
236
|
+
else
|
|
237
|
+
seen.set(id, rel);
|
|
238
|
+
}
|
|
239
|
+
return findings;
|
|
240
|
+
}
|
|
241
|
+
// B4 — autonomy. An autonomous loop needs a well-formed contract AND an agreed
|
|
242
|
+
// budget. This boundary used to be honor-system: `foo loop check` enforced it,
|
|
243
|
+
// but nothing invoked `foo loop check` — no hook, no skill, and `self check`
|
|
244
|
+
// itself only covered B1/B2. So a packet could carry a scheduled, acting,
|
|
245
|
+
// unbudgeted loop and every automated gate in the repo would pass it. B1 gets a
|
|
246
|
+
// fail-closed pre-commit hook; B4 got a command you had to remember to type.
|
|
247
|
+
//
|
|
248
|
+
// Now B4 is checked wherever B1 and B2 are. Strictly READ-ONLY: `foo loop check`
|
|
249
|
+
// may WRITE an auto-approved budget, but a checker must never mutate the thing it
|
|
250
|
+
// is checking — so this re-derives the same decision purely and reports it.
|
|
251
|
+
//
|
|
252
|
+
// Two things keep this from being merely strict:
|
|
253
|
+
// 1. Only an ARMED loop blocks. B4 governs "any loop or autonomous action" — a
|
|
254
|
+
// `draft` contract takes no action, so drafting one must not make the repo
|
|
255
|
+
// un-committable. A draft is a signal; the moment it is armed (ready/active)
|
|
256
|
+
// the budget handshake becomes a hard boundary. That is exactly when the
|
|
257
|
+
// money can start being spent.
|
|
258
|
+
// 2. An unbudgeted loop that provably prices BELOW the handshake floor is a
|
|
259
|
+
// signal, not a block — cheap work keeps its momentum (B4's size-aware rule).
|
|
260
|
+
// Otherwise it blocks: malformed contract, or armed + unbudgeted + unable to prove
|
|
261
|
+
// it is cheap (unpriced, unmeasurable sources, or genuinely at/above the floor).
|
|
262
|
+
function checkAutonomy(root) {
|
|
263
|
+
const boundaries = [];
|
|
264
|
+
const signals = [];
|
|
265
|
+
const specsDir = path.join(root, "specs");
|
|
266
|
+
if (!existsSync(specsDir))
|
|
267
|
+
return { boundaries, signals };
|
|
268
|
+
const floor = handshakeFloorUsd();
|
|
269
|
+
const pricing = loadPricing(root);
|
|
270
|
+
for (const entry of readdirSync(specsDir, { withFileTypes: true })) {
|
|
271
|
+
if (!entry.isDirectory())
|
|
272
|
+
continue;
|
|
273
|
+
const packetDir = path.join(specsDir, entry.name);
|
|
274
|
+
if (!existsSync(path.join(packetDir, "loops.yaml")))
|
|
275
|
+
continue;
|
|
276
|
+
let loops;
|
|
277
|
+
try {
|
|
278
|
+
loops = loadLoopContracts(packetDir).loops;
|
|
279
|
+
}
|
|
280
|
+
catch (error) {
|
|
281
|
+
boundaries.push({
|
|
282
|
+
id: "B4",
|
|
283
|
+
message: `specs/${entry.name}/loops.yaml is a malformed loop contract: ${error instanceof Error ? error.message : String(error)}`,
|
|
284
|
+
});
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
for (const loop of loops) {
|
|
288
|
+
if (loop.budget?.agreed)
|
|
289
|
+
continue; // handshake already done
|
|
290
|
+
const estimate = estimateLoopBudget({
|
|
291
|
+
packetDir,
|
|
292
|
+
sources: loop.observe.sources,
|
|
293
|
+
pricing,
|
|
294
|
+
unit: "usd",
|
|
295
|
+
budget: loop.budget,
|
|
296
|
+
});
|
|
297
|
+
const unmeasurable = hasExternalSources(estimate.measured);
|
|
298
|
+
const belowFloor = estimate.priced && !unmeasurable && estimate.usd.expected < floor;
|
|
299
|
+
if (belowFloor) {
|
|
300
|
+
signals.push({
|
|
301
|
+
id: "signal",
|
|
302
|
+
message: `specs/${entry.name} loop ${loop.id} has no agreed budget but prices below the $${floor.toFixed(2)} floor ($${estimate.usd.expected.toFixed(2)} expected) — foo loop check will auto-approve it${ARMED_LOOP_STATES.has(loop.status) ? "" : " once it is armed"}`,
|
|
303
|
+
});
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
// Never quote a dollar figure for an unmeasurable loop — its cost is unknown,
|
|
307
|
+
// and a number here would be invented.
|
|
308
|
+
const why = unmeasurable
|
|
309
|
+
? "its cost is unknown (it reads sources productfoo cannot size), so it can never prove it is cheap"
|
|
310
|
+
: !estimate.priced
|
|
311
|
+
? "its cost could not be priced"
|
|
312
|
+
: `it prices at $${estimate.usd.expected.toFixed(2)} expected, at or above the $${floor.toFixed(2)} floor`;
|
|
313
|
+
const fix = unmeasurable
|
|
314
|
+
? `run foo loop estimate specs/${entry.name} --agree --limit <usd> (you set the cap; productfoo will not invent one)`
|
|
315
|
+
: `run foo loop estimate specs/${entry.name} --agree`;
|
|
316
|
+
// A draft loop cannot act, so it cannot spend — surface it, don't block the
|
|
317
|
+
// repo. Arming it (status: ready|active) is what makes the handshake due.
|
|
318
|
+
if (!ARMED_LOOP_STATES.has(loop.status)) {
|
|
319
|
+
signals.push({
|
|
320
|
+
id: "signal",
|
|
321
|
+
message: `specs/${entry.name} loop ${loop.id} (${loop.status}) has no agreed budget — ${why}; budget it before arming it (${fix})`,
|
|
322
|
+
});
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
boundaries.push({
|
|
326
|
+
id: "B4",
|
|
327
|
+
message: `specs/${entry.name} loop ${loop.id} is ${loop.status} (armed) with no agreed budget and ${why} — ${fix}`,
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
return { boundaries, signals };
|
|
332
|
+
}
|
|
333
|
+
// Signal — skill structure and runtime-neutral wording. Surfaced, never blocking.
|
|
334
|
+
// The command surface is what a user reads before they run anything, so a
|
|
335
|
+
// description that no longer matches the contract is a live lie in every
|
|
336
|
+
// harness's picker. Both halves are checked: `commands/*.md` (the docs) and the
|
|
337
|
+
// committed launchers (`.claude/commands/foo/*`, plugin skills).
|
|
338
|
+
//
|
|
339
|
+
// A signal, not a boundary — AGENTS.md keeps boundaries narrow (B1/B2/B4), and
|
|
340
|
+
// stale copy does not cross one. `foo commands validate` is the gating check;
|
|
341
|
+
// this makes the drift visible in the run people actually do.
|
|
342
|
+
function commandSurfaceSignals(root, tracked) {
|
|
343
|
+
const findings = [];
|
|
344
|
+
const docs = validateCommandDocs(root);
|
|
345
|
+
for (const d of docs.drift) {
|
|
346
|
+
findings.push({
|
|
347
|
+
id: "signal",
|
|
348
|
+
message: `command contract drift: ${d.id}.${d.field} in ${FOO_COMMAND_CONTRACT.find((c) => c.id === d.id)?.docs_path ?? "docs"} — run \`foo commands validate\` for the detail.`,
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
if (tracked === null)
|
|
352
|
+
return findings; // no git: the same posture B1 takes.
|
|
353
|
+
const launchers = validateCommittedLaunchers(root, tracked);
|
|
354
|
+
for (const d of launchers.drift) {
|
|
355
|
+
findings.push({
|
|
356
|
+
id: "signal",
|
|
357
|
+
message: `committed launcher drift: ${d.file} advertises ${d.field} "${d.launcher ?? "(missing)"}" but the contract says "${d.contract}" — re-run \`foo agent setup\` and commit the result.`,
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
return findings;
|
|
361
|
+
}
|
|
362
|
+
function skillSignals(root) {
|
|
363
|
+
const skillsDir = path.join(root, "skills");
|
|
364
|
+
if (!existsSync(skillsDir))
|
|
365
|
+
return [];
|
|
366
|
+
const registry = existsSync(path.join(skillsDir, "skill-registry.yaml"))
|
|
367
|
+
? readFileSync(path.join(skillsDir, "skill-registry.yaml"), "utf8")
|
|
368
|
+
: "";
|
|
369
|
+
const forbidden = [
|
|
370
|
+
"AskUserQuestion",
|
|
371
|
+
"TodoWrite",
|
|
372
|
+
"Task tool",
|
|
373
|
+
"Skill tool",
|
|
374
|
+
"WebFetch",
|
|
375
|
+
"ToolSearch",
|
|
376
|
+
"mcp__",
|
|
377
|
+
"Claude Code",
|
|
378
|
+
];
|
|
379
|
+
const findings = [];
|
|
380
|
+
for (const entry of readdirSync(skillsDir, { withFileTypes: true })) {
|
|
381
|
+
if (!entry.isDirectory())
|
|
382
|
+
continue;
|
|
383
|
+
const name = entry.name;
|
|
384
|
+
const skillPath = path.join(skillsDir, name, "SKILL.md");
|
|
385
|
+
if (!existsSync(skillPath))
|
|
386
|
+
continue;
|
|
387
|
+
const text = readFileSync(skillPath, "utf8");
|
|
388
|
+
if (!/^[a-z][a-z0-9]*(-[a-z0-9]+)*$/.test(name))
|
|
389
|
+
findings.push({ id: "signal", message: `skill folder name not kebab-case: ${name}` });
|
|
390
|
+
if (!text.startsWith("---\n"))
|
|
391
|
+
findings.push({ id: "signal", message: `${name}/SKILL.md missing frontmatter` });
|
|
392
|
+
if (!text.includes(`name: ${name}`))
|
|
393
|
+
findings.push({ id: "signal", message: `${name}/SKILL.md frontmatter name mismatch` });
|
|
394
|
+
if (!text.includes("## Usage"))
|
|
395
|
+
findings.push({ id: "signal", message: `${name}/SKILL.md missing ## Usage` });
|
|
396
|
+
if (!text.includes("## Error Handling"))
|
|
397
|
+
findings.push({ id: "signal", message: `${name}/SKILL.md missing ## Error Handling` });
|
|
398
|
+
if (registry && !registry.includes(`id: "${name}"`))
|
|
399
|
+
findings.push({ id: "signal", message: `${name} missing from skill-registry.yaml` });
|
|
400
|
+
text.split(/\r?\n/).forEach((line, index) => {
|
|
401
|
+
for (const token of forbidden) {
|
|
402
|
+
if (line.includes(token))
|
|
403
|
+
findings.push({ id: "signal", message: `${name}/SKILL.md:${index + 1} runtime-specific wording: ${token}` });
|
|
404
|
+
}
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
return findings;
|
|
408
|
+
}
|
|
409
|
+
/** Supported checks for this build. */
|
|
410
|
+
export function selfUsage() {
|
|
411
|
+
return [
|
|
412
|
+
"foo self — check foo against its own boundaries.",
|
|
413
|
+
"",
|
|
414
|
+
" foo self check [--json] [--full]",
|
|
415
|
+
" run the boundary checks (B1 safety, B2 spec identity, B4 autonomy).",
|
|
416
|
+
" Results are cached per file (mtime, size) under the machine state dir;",
|
|
417
|
+
" --full rescans every tracked file and refreshes the cache.",
|
|
418
|
+
" foo self gate fail-closed secret gate (used as the pre-commit hook)",
|
|
419
|
+
" foo self which which build is running, and from where",
|
|
420
|
+
" foo self help this",
|
|
421
|
+
"",
|
|
422
|
+
"",
|
|
423
|
+
].join("\n");
|
|
424
|
+
}
|
|
425
|
+
export function runSelfCheck(root, command, args) {
|
|
426
|
+
// A typo is not a crash. Throwing a bare Error here classified as command_error, which
|
|
427
|
+
// `pages()` treats as report-worthy — so `foo self help` manufactured a bug report about
|
|
428
|
+
// itself. UsageError classifies as usage_error: told to the human, never filed.
|
|
429
|
+
if (command && command !== "check") {
|
|
430
|
+
throw new UsageError(`unknown self command: ${command}\n\n${selfUsage()}`, "E_BAD_FLAG");
|
|
431
|
+
}
|
|
432
|
+
const asJson = args.includes("--json");
|
|
433
|
+
const full = args.includes("--full");
|
|
434
|
+
// One `git ls-files` per run, shared by the B1 scan and the launcher-surface
|
|
435
|
+
// signals — the scan used to spawn it twice (cli_performance_v0 R-003).
|
|
436
|
+
const tracked = trackedFiles(root);
|
|
437
|
+
const safety = checkSafety(root, tracked, full);
|
|
438
|
+
const autonomy = checkAutonomy(root);
|
|
439
|
+
const boundaries = [...safety.findings, ...checkSpecIdentity(root), ...autonomy.boundaries];
|
|
440
|
+
const signals = [
|
|
441
|
+
...autonomy.signals,
|
|
442
|
+
...skillSignals(root),
|
|
443
|
+
...commandSurfaceSignals(root, tracked),
|
|
444
|
+
...commitTrailerSignal(root).map((message) => ({ id: "signal", message })),
|
|
445
|
+
];
|
|
446
|
+
const exitCode = boundaries.length > 0 ? 1 : 0;
|
|
447
|
+
if (asJson) {
|
|
448
|
+
const payload = {
|
|
449
|
+
schema_version: "0.1",
|
|
450
|
+
blocked: exitCode !== 0,
|
|
451
|
+
safety_verified: safety.verified,
|
|
452
|
+
boundaries,
|
|
453
|
+
signals,
|
|
454
|
+
};
|
|
455
|
+
return { output: `${JSON.stringify(payload, null, 2)}\n`, exitCode };
|
|
456
|
+
}
|
|
457
|
+
const lines = [
|
|
458
|
+
"foo self check",
|
|
459
|
+
"",
|
|
460
|
+
"Boundaries (block):",
|
|
461
|
+
...(boundaries.length === 0
|
|
462
|
+
? [" none — B1 safety, B2 spec identity, and B4 autonomy hold."]
|
|
463
|
+
: boundaries.map((f) => ` [${f.id}] ${f.message}`)),
|
|
464
|
+
...(safety.verified ? [] : [" note: B1 could not be verified (git unavailable); treating as advisory."]),
|
|
465
|
+
"",
|
|
466
|
+
"Signals (surface, do not block):",
|
|
467
|
+
...(signals.length === 0 ? [" none."] : signals.map((f) => ` - ${f.message}`)),
|
|
468
|
+
"",
|
|
469
|
+
exitCode === 0 ? "Result: boundaries hold." : "Result: boundary violation — resolve before committing.",
|
|
470
|
+
"",
|
|
471
|
+
];
|
|
472
|
+
return { output: lines.join("\n"), exitCode };
|
|
473
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export declare const SCAN_DETECTOR_VERSION = "self-check-scan-cache-v1";
|
|
2
|
+
export type CachedFinding = {
|
|
3
|
+
detector: string;
|
|
4
|
+
line: number;
|
|
5
|
+
};
|
|
6
|
+
export type ScanCacheEntry = {
|
|
7
|
+
mtimeMs: number;
|
|
8
|
+
size: number;
|
|
9
|
+
skipped?: "large" | "binary";
|
|
10
|
+
findings: CachedFinding[];
|
|
11
|
+
pii: CachedFinding[] | null;
|
|
12
|
+
};
|
|
13
|
+
export type ScanCache = {
|
|
14
|
+
schema_version: 1;
|
|
15
|
+
repo: string;
|
|
16
|
+
head: string;
|
|
17
|
+
detector_version: string;
|
|
18
|
+
allowlist_digest: string | null;
|
|
19
|
+
entries: Record<string, ScanCacheEntry>;
|
|
20
|
+
};
|
|
21
|
+
export declare const MAX_CACHE_ENTRIES = 50000;
|
|
22
|
+
export declare const MAX_CACHE_BYTES: number;
|
|
23
|
+
export declare const MAX_CACHE_FILES = 128;
|
|
24
|
+
export declare function scanCachePath(root: string): string;
|
|
25
|
+
export declare function allowlistDigest(root: string, allowFile: string): string | null;
|
|
26
|
+
export declare function loadScanCache(root: string, head: string | null, detectorVersion: string, allowlistDigestValue: string | null): ScanCache | null;
|
|
27
|
+
export declare function capScanCacheEntries(entries: Record<string, ScanCacheEntry>, maxEntries: number): Record<string, ScanCacheEntry>;
|
|
28
|
+
export declare function pruneScanCacheDir(dir: string, maxFiles: number): void;
|
|
29
|
+
export declare function saveScanCache(cache: ScanCache): boolean;
|