@ikarmic/forge-core 5.0.0 → 5.1.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/README.md +1 -1
- package/dist/agent/agent.d.ts +8 -0
- package/dist/agent/agent.js +148 -24
- package/dist/agent/background.d.ts +58 -0
- package/dist/agent/background.js +139 -0
- package/dist/agent/enhance-prompt.d.ts +3 -1
- package/dist/agent/enhance-prompt.js +6 -2
- package/dist/agent/goal.d.ts +54 -0
- package/dist/agent/goal.js +65 -0
- package/dist/agent/prompts.d.ts +25 -1
- package/dist/agent/prompts.js +61 -17
- package/dist/agents/agent-files.d.ts +43 -0
- package/dist/agents/agent-files.js +109 -0
- package/dist/checkpoints/store.js +5 -1
- package/dist/commands/custom-commands.d.ts +9 -4
- package/dist/commands/custom-commands.js +13 -2
- package/dist/config-file.d.ts +43 -0
- package/dist/config-file.js +249 -5
- package/dist/config-merge.d.ts +13 -0
- package/dist/config-merge.js +67 -0
- package/dist/config-schema.d.ts +340 -0
- package/dist/config-schema.js +126 -0
- package/dist/config.js +2 -1
- package/dist/context/compaction.js +4 -0
- package/dist/context/mask.js +1 -1
- package/dist/custom-providers.d.ts +95 -0
- package/dist/custom-providers.js +120 -0
- package/dist/git/auto-commit.d.ts +2 -2
- package/dist/git/auto-commit.js +5 -4
- package/dist/github/github-tools.d.ts +2 -2
- package/dist/hooks/runner.js +3 -2
- package/dist/import/claude-code.d.ts +39 -0
- package/dist/import/claude-code.js +254 -0
- package/dist/index.d.ts +34 -5
- package/dist/index.js +27 -4
- package/dist/journey/answers-block.d.ts +17 -0
- package/dist/journey/answers-block.js +15 -0
- package/dist/journey/index.d.ts +9 -0
- package/dist/journey/index.js +8 -0
- package/dist/journey/journey.d.ts +13 -0
- package/dist/journey/journey.js +40 -0
- package/dist/journey/offer.d.ts +25 -0
- package/dist/journey/offer.js +48 -0
- package/dist/journey/questions.d.ts +23 -0
- package/dist/journey/questions.js +49 -0
- package/dist/journey/stages.d.ts +13 -0
- package/dist/journey/stages.js +107 -0
- package/dist/journey/survey.d.ts +10 -0
- package/dist/journey/survey.js +203 -0
- package/dist/journey/trigger.d.ts +19 -0
- package/dist/journey/trigger.js +79 -0
- package/dist/journey/types.d.ts +72 -0
- package/dist/journey/types.js +1 -0
- package/dist/lsp/client.d.ts +5 -0
- package/dist/lsp/client.js +39 -3
- package/dist/mcp/index.d.ts +5 -1
- package/dist/mcp/index.js +3 -1
- package/dist/mcp/manager.d.ts +17 -3
- package/dist/mcp/manager.js +27 -2
- package/dist/mcp/marketplace.d.ts +2 -0
- package/dist/mcp/marketplace.js +10 -4
- package/dist/mcp/mcp-client.d.ts +12 -2
- package/dist/mcp/mcp-client.js +74 -17
- package/dist/mcp/mcp-config.d.ts +2 -0
- package/dist/mcp/mcp-config.js +1 -0
- package/dist/mcp/oauth.d.ts +55 -0
- package/dist/mcp/oauth.js +197 -0
- package/dist/mcp/resource-tools.d.ts +8 -0
- package/dist/mcp/resource-tools.js +56 -0
- package/dist/memory/session-digest.d.ts +12 -0
- package/dist/memory/session-digest.js +47 -0
- package/dist/memory/stale.js +2 -1
- package/dist/model-alias.d.ts +18 -2
- package/dist/model-alias.js +27 -3
- package/dist/model-catalog.js +0 -1
- package/dist/model-list.js +2 -1
- package/dist/model-overrides.d.ts +30 -0
- package/dist/model-overrides.js +48 -0
- package/dist/orchestrator/specialists-registry.js +2 -3
- package/dist/orchestrator/specialists.js +4 -4
- package/dist/policy/ca-certs.d.ts +8 -0
- package/dist/policy/ca-certs.js +30 -0
- package/dist/policy/proxy.d.ts +10 -3
- package/dist/policy/proxy.js +33 -6
- package/dist/pricing.js +4 -0
- package/dist/providers.d.ts +7 -0
- package/dist/providers.js +39 -5
- package/dist/semantic/embedder.d.ts +15 -0
- package/dist/semantic/embedder.js +25 -0
- package/dist/semantic/semantic-index.d.ts +10 -2
- package/dist/semantic/semantic-index.js +16 -7
- package/dist/sessions/session-store.js +2 -1
- package/dist/skills/builtin.d.ts +2 -0
- package/dist/skills/builtin.js +68 -0
- package/dist/skills/index.d.ts +2 -0
- package/dist/skills/index.js +2 -0
- package/dist/skills/loader.d.ts +10 -3
- package/dist/skills/loader.js +20 -7
- package/dist/skills/marketplace.js +2 -1
- package/dist/skills/remote.d.ts +14 -0
- package/dist/skills/remote.js +131 -0
- package/dist/skills/types.d.ts +10 -0
- package/dist/specs/generate.d.ts +18 -9
- package/dist/specs/generate.js +174 -14
- package/dist/specs/index.d.ts +2 -2
- package/dist/specs/index.js +1 -1
- package/dist/specs/store.d.ts +14 -2
- package/dist/specs/store.js +26 -1
- package/dist/specs/types.d.ts +11 -0
- package/dist/steering/extra-instructions.d.ts +8 -0
- package/dist/steering/extra-instructions.js +104 -0
- package/dist/steering/folder-instructions.d.ts +8 -0
- package/dist/steering/folder-instructions.js +28 -0
- package/dist/steering/select.js +2 -1
- package/dist/timed-fetch.d.ts +12 -0
- package/dist/timed-fetch.js +83 -0
- package/dist/tools/apply-patch.d.ts +52 -0
- package/dist/tools/apply-patch.js +197 -0
- package/dist/tools/ask-user.d.ts +27 -0
- package/dist/tools/ask-user.js +60 -0
- package/dist/tools/browser-tools.d.ts +6 -6
- package/dist/tools/browser-tools.js +3 -13
- package/dist/tools/codebase-search.d.ts +4 -2
- package/dist/tools/codebase-search.js +2 -2
- package/dist/tools/edit-file.js +5 -3
- package/dist/tools/fetch-url.d.ts +7 -0
- package/dist/tools/fetch-url.js +52 -19
- package/dist/tools/find-symbol.d.ts +2 -2
- package/dist/tools/ignore-rules.js +2 -1
- package/dist/tools/index.d.ts +5 -1
- package/dist/tools/index.js +10 -1
- package/dist/tools/memory.d.ts +6 -6
- package/dist/tools/notebook.d.ts +2 -2
- package/dist/tools/permission-rules.d.ts +32 -0
- package/dist/tools/permission-rules.js +182 -0
- package/dist/tools/read-file.d.ts +5 -0
- package/dist/tools/read-file.js +12 -2
- package/dist/tools/read-only.d.ts +1 -1
- package/dist/tools/read-only.js +2 -0
- package/dist/tools/run-command.d.ts +4 -2
- package/dist/tools/run-command.js +12 -8
- package/dist/tools/sandbox-loader.js +3 -13
- package/dist/tools/search-files.d.ts +8 -2
- package/dist/tools/search-files.js +43 -0
- package/dist/tools/shared.d.ts +5 -0
- package/dist/tools/shared.js +30 -2
- package/dist/tools/shell-env.d.ts +7 -0
- package/dist/tools/shell-env.js +9 -0
- package/dist/tools/sub-agent-wait.d.ts +29 -0
- package/dist/tools/sub-agent-wait.js +60 -0
- package/dist/tools/sub-agent.d.ts +50 -4
- package/dist/tools/sub-agent.js +213 -125
- package/dist/tools/text-encoding.d.ts +10 -0
- package/dist/tools/text-encoding.js +40 -0
- package/dist/tools/todo.d.ts +36 -0
- package/dist/tools/todo.js +42 -0
- package/dist/tools/write-file.js +15 -9
- package/dist/tools/write-tools.d.ts +12 -0
- package/dist/tools/write-tools.js +18 -0
- package/dist/types.d.ts +98 -0
- package/dist/worktree/worktree.d.ts +3 -0
- package/dist/worktree/worktree.js +22 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -22,13 +22,13 @@ export { checkStale, staleKey } from './memory/stale.js';
|
|
|
22
22
|
export { MAX_BODY_CHARS } from './tools/memory.js';
|
|
23
23
|
export { buildLlmSummarizer, compactMessages, estimateMessageTokens, estimateTokens, getModelWindowTokens, shouldCompact, } from './context/compaction.js';
|
|
24
24
|
export { defaultModelFor, detectProvider, envVarForProvider, fastDefaultFor, hasProviderKey, readModelConfigFromEnv, resolveApiKey, SUPPORTED_PROVIDERS, } from './config.js';
|
|
25
|
-
export { loadMcpConfig, parseMcpConfig, connectAll, connectOne, closeAll, mcpResultToValue, jsonSchemaToZod, } from './mcp/index.js';
|
|
25
|
+
export { loadMcpConfig, parseMcpConfig, connectAll, connectOne, closeAll, mcpResultToValue, mcpInstructionsBlock, createMcpResourceTools, jsonSchemaToZod, } from './mcp/index.js';
|
|
26
26
|
export { createLanguageModel, formatModelId, pickAvailableProvider, resolveModelFromEnv, resolveModelFromEnvAsync } from './providers.js';
|
|
27
27
|
export { defaultSessionDir, generateSessionId, saveSession, loadSession, deleteSession, listSessions, findLatestSession, formatSessionMarkdown, formatSessionJson, appendedSince, summarizeMessage, watchSession, searchSessions, sessionText, pruneSessions, pruneByRetention, } from './sessions/index.js';
|
|
28
28
|
export { commandDirs, expandCustomCommand, formatCustomCommands, loadCustomCommands, parseCommandFile, } from './commands/custom-commands.js';
|
|
29
29
|
export { BUILTIN_TOOLS, editFileTool, fetchUrlTool, findSymbolTool, listFilesTool, memoryReadTool, memoryForgetTool, memoryWriteTool, readFileTool, runCommandTool, searchFilesTool, subAgentTool, writeFileTool, checkPolicyViolation, globToRegex, mergeToolSandbox, normalizePathPattern, pathMatchesAny, redact, countRedactions, buildRules, ToolResultCache, buildCacheKey, cachedExecute, } from './tools/index.js';
|
|
30
30
|
// Skills system
|
|
31
|
-
export { buildSkillsBlock, composeSkillsPrompt, discoverSkills, fetchSkillsCatalog, installSkillFromCatalog, loadSkillsRegistry, loadSkillsRegistryAsync, parseSkill, parseSkillsDir, resolveSkillRoots, scoreSkill, searchSkills, selectSkills, skillsRegistryFromList, DEFAULT_SKILL_CATALOG_URL, GLOBAL_SKILLS_DIR, LOCAL_SKILLS_DIR, PROJECT_SKILLS_DIR, SKILL_CATALOG_VERSION, } from './skills/index.js';
|
|
31
|
+
export { buildSkillsBlock, builtinSkills, composeSkillsPrompt, discoverSkills, fetchSkillsCatalog, installSkillFromCatalog, loadSkillsRegistry, loadSkillsRegistryAsync, parseSkill, parseSkillsDir, resolveSkillRoots, scoreSkill, searchSkills, selectSkills, skillsRegistryFromList, DEFAULT_SKILL_CATALOG_URL, GLOBAL_SKILLS_DIR, LOCAL_SKILLS_DIR, PROJECT_SKILLS_DIR, SKILL_CATALOG_VERSION, } from './skills/index.js';
|
|
32
32
|
export { cosine, embedderFromEnv, hashingEmbedder, openAiCompatibleEmbedder, } from './semantic/embedder.js';
|
|
33
33
|
export { ensureSemanticFresh, searchSemantic } from './semantic/semantic-index.js';
|
|
34
34
|
export { createCodebaseSearchTool } from './tools/codebase-search.js';
|
|
@@ -40,7 +40,10 @@ export { applyCacheHints, supportsCacheHints } from './agent/cache-hints.js';
|
|
|
40
40
|
// Organization policy, audit log, proxy
|
|
41
41
|
export { PolicyError, applyPolicy, assertProviderAllowed, filterMcpServers, getOrgPolicy, loadOrgPolicy, managedPolicyPath, parsePolicy, resetOrgPolicyCache, } from './policy/policy.js';
|
|
42
42
|
export { createFileAuditSink, defaultAuditFile, scrubArgs } from './policy/audit.js';
|
|
43
|
-
export { hasProxy, installProxy, proxyFromEnv } from './policy/proxy.js';
|
|
43
|
+
export { hasProxy, installProxy, installedFetch, proxyFromEnv } from './policy/proxy.js';
|
|
44
|
+
export { readCaCertificates } from './policy/ca-certs.js';
|
|
45
|
+
export { BackgroundAgents, MAX_RUNNING_BACKGROUND, backgroundResultsBlock } from './agent/background.js';
|
|
46
|
+
export { applyClaudeImport, planClaudeImport } from './import/claude-code.js';
|
|
44
47
|
// Hooks system
|
|
45
48
|
export { AGENT_HOOK_TOOLS, createAgentHookRunner, DEFAULT_AGENT_HOOK_TIMEOUT_MS, DEFAULT_HOOK_TIMEOUT_MS, HooksBus, eventFilePath, eventToEnv, eventToolName, globalSettingsPath, hooksBusFromConfig, loadHooksConfig, matcherMatches, projectSettingsPath, runHook, validateHooksConfig, HOOK_EVENT_NAMES, shouldIgnoreWatchPath, FileChangeCoalescer, watchFilesystem, } from './hooks/index.js';
|
|
46
49
|
// MCP marketplace
|
|
@@ -50,7 +53,7 @@ export { MARKETPLACE_CATALOG_VERSION } from './mcp/marketplace-types.js';
|
|
|
50
53
|
// Checkpoint system
|
|
51
54
|
export { CheckpointStore } from './checkpoints/index.js';
|
|
52
55
|
// Specs system (spec-driven planning mode)
|
|
53
|
-
export { SpecStore, isSafeSpecId, runKey, slugifyTaskForSpecId, SPEC_READONLY_TOOLS, splitSpecSections, convertToCheckboxes, parseTasks, setTaskDone, changedSince, changeSize, keepDoneTasks, referencedPaths, specDrift, syncSpecTask } from './specs/index.js';
|
|
56
|
+
export { SpecStore, isSafeSpecId, runKey, slugifyTaskForSpecId, SPEC_READONLY_TOOLS, splitRequirementsDesign, splitSpecSections, splitTasksSection, convertToCheckboxes, parseTasks, setTaskDone, changedSince, changeSize, keepDoneTasks, referencedPaths, specDrift, syncSpecTask } from './specs/index.js';
|
|
54
57
|
// Orchestrator system
|
|
55
58
|
export { BUILTIN_SPECIALISTS, defaultConfigPath, effectiveToolsFor, BOARD_TOOL_NAMES, createBoardTools, DEFAULT_BOARD_CAP, getRegisteredSpecialist, getSpecialist, MessageBoard, listSpecialists, loadSpecialistsFromConfig, parseRouterOutput, registerSpecialist, resolveEffectiveModel, ROUTER_SYSTEM_PROMPT, runOrchestrator, runRouter, saveSpecialistsToConfig, unregisterSpecialist, validateSpecialistConfig, _resetSpecialistsForTests, _customSpecialistCount, } from './orchestrator/index.js';
|
|
56
59
|
// Diff utilities (line-level unified diff)
|
|
@@ -70,11 +73,22 @@ export { defaultRetryPrompt, formatZodError, isStructuredOutputError, loadSchema
|
|
|
70
73
|
export { branchChangedFiles, branchDiff, commitAllInWorktree, compareWorktreeContexts, deleteBranch, mergeBranch, snapshotWorktreeContext, WorktreeError, WorktreeManager, } from './worktree/index.js';
|
|
71
74
|
// Steering files (SP3a)
|
|
72
75
|
export { formatSteeringLine, isSafeSteeringName, loadSteering, overrideFilePath, parseSteering, pathMatchesGlob, readOverride, selectAtStart, selectOnToolTouch, setSteeringDisabled, stripSteering, wrapSteering, } from './steering/index.js';
|
|
76
|
+
export { CONFIG_KEY_COVERAGE, FORGE_CONFIG_SCHEMA } from './config-schema.js';
|
|
77
|
+
export { FILE_WRITE_TOOL_NAMES, filesTouchedBy } from './tools/write-tools.js';
|
|
78
|
+
export { shellEnv } from './tools/shell-env.js';
|
|
79
|
+
export { previewPatch } from './tools/apply-patch.js';
|
|
80
|
+
export { applyPatchTool, parsePatch, patchedPaths } from './tools/apply-patch.js';
|
|
81
|
+
export { buildEmbedder } from './semantic/embedder.js';
|
|
82
|
+
export { parseExtensions } from './semantic/semantic-index.js';
|
|
83
|
+
export { cleanDigest, generateSessionDigest, shouldDigest } from './memory/session-digest.js';
|
|
84
|
+
export { buildExtraInstructionsBlock } from './steering/extra-instructions.js';
|
|
73
85
|
export { buildProjectInstructionsBlock, PROJECT_INSTRUCTION_FILES } from './steering/project-instructions.js';
|
|
74
86
|
// AI comments (Aider parity E).
|
|
75
87
|
export { aiCommentTask, findAiComments, triggerOf } from './ai-comments/index.js';
|
|
76
88
|
// Git workflow: dirty-work commit before a run, drafted commit after, /undo.
|
|
77
89
|
export * from './git/index.js';
|
|
90
|
+
// Guided journey: the two-tier trigger, the LLM-free survey, stages and gates.
|
|
91
|
+
export * from './journey/index.js';
|
|
78
92
|
// Context: ranked repo map in the system prompt; auto-lint of edited files.
|
|
79
93
|
export * from './repomap/index.js';
|
|
80
94
|
export * from './lint/index.js';
|
|
@@ -85,8 +99,17 @@ export * from './lsp/index.js';
|
|
|
85
99
|
export * from './plugins/index.js';
|
|
86
100
|
// Tools that are approved without a prompt by default (read-only).
|
|
87
101
|
export { READ_ONLY_TOOLS, resolveAutoApprove } from './tools/read-only.js';
|
|
102
|
+
export { compilePermissionRules, evaluatePermission, isSensitiveFile, permissionFor, splitShellCommands, touchesOutsideCwd } from './tools/permission-rules.js';
|
|
88
103
|
// Project check detection (typecheck / lint / test commands) for verify-by-default.
|
|
89
104
|
export { autoRunCommands, autoRunFingerprint, detectProjectChecks, formatProjectCommandsBlock, projectScriptsFingerprint } from './verify/index.js';
|
|
90
105
|
export { isAlwaysAllowable, isListedCommand, needsScriptFingerprint, normalizeCommand } from './tools/command-allow.js';
|
|
91
106
|
// Git-style ignore rules (.gitignore, .forgeignore) used by the file tools and indexes.
|
|
92
107
|
export { IgnoreRules, IGNORE_FILE_NAMES, findGitRoot } from './tools/ignore-rules.js';
|
|
108
|
+
export { MAX_OBJECTIVE_CHARS, applyTurnOutcome, createGoalReportTool, goalPrompt, pauseGoal, resumeGoal, startGoal } from './agent/goal.js';
|
|
109
|
+
export { loadMergedForgeConfig, projectConfigPath } from './config-merge.js';
|
|
110
|
+
export { PROVIDER_PRESETS, apiModelId, customProviderProblems, filterModels, knownProviderIds, modelFilterFor, registerCustomProviders, registerModelFilters, registeredCustomProviders, resolveProviderEndpoint, } from './custom-providers.js';
|
|
111
|
+
export { withTimeouts } from './timed-fetch.js';
|
|
112
|
+
export { createEndpointModel } from './providers.js';
|
|
113
|
+
export { agentToSpecialist, loadAgentFiles, parseAgentFile, registerAgentDefs, registeredAgent, registeredAgents } from './agents/agent-files.js';
|
|
114
|
+
export { applyResolved } from './model-alias.js';
|
|
115
|
+
export { getModelOverride, overridesFromProviders, registerModelOverrides } from './model-overrides.js';
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The interview answers, rendered for the system prompt.
|
|
3
|
+
*
|
|
4
|
+
* Why the system prompt and not a turn: `compactMessages` keeps the system
|
|
5
|
+
* message and the most recent turns verbatim and summarises everything in
|
|
6
|
+
* between, so an answer sitting in a middle turn is forgotten on a long
|
|
7
|
+
* journey — the person says "Postgres, no invoicing" and forty turns later the
|
|
8
|
+
* model scaffolds MySQL with invoicing. In the system message it cannot be
|
|
9
|
+
* forgotten.
|
|
10
|
+
*
|
|
11
|
+
* Why the interview must finish before the first agent run:
|
|
12
|
+
* `buildInitialMessages` assembles the blocks only when no history is passed
|
|
13
|
+
* (`agent.ts:231`), so this block enters the prompt once, on a session's first
|
|
14
|
+
* turn, and rides inside `history[0]` from then on.
|
|
15
|
+
*/
|
|
16
|
+
import type { Answer } from './types.js';
|
|
17
|
+
export declare function renderAnswersBlock(answers: readonly Answer[]): string;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export function renderAnswersBlock(answers) {
|
|
2
|
+
// A dismissed question comes back with an empty answer. Rendering it under
|
|
3
|
+
// "these are decisions" would present an open question as settled, which
|
|
4
|
+
// invites the model to invent the very thing the person declined to choose.
|
|
5
|
+
const answered = answers.filter((a) => a.answer.trim() !== '');
|
|
6
|
+
if (answered.length === 0)
|
|
7
|
+
return '';
|
|
8
|
+
const lines = answered.map((a) => `- ${a.question}: ${a.answer.replace(/<\/journey-answers>/gi, '')}`);
|
|
9
|
+
return [
|
|
10
|
+
'<journey-answers>',
|
|
11
|
+
'The person answered these before work started. They are decisions, not suggestions:',
|
|
12
|
+
...lines,
|
|
13
|
+
'</journey-answers>',
|
|
14
|
+
].join('\n');
|
|
15
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** The guided journey: trigger, survey, interview questions, offer, stages and state. */
|
|
2
|
+
export { renderAnswersBlock } from './answers-block.js';
|
|
3
|
+
export { abandon, advance, startJourney, stepDown } from './journey.js';
|
|
4
|
+
export { decideOffer } from './offer.js';
|
|
5
|
+
export { answeredBySurvey, gapQuestions } from './questions.js';
|
|
6
|
+
export { NO_TOOLS_SENTINEL, stageById, STAGES } from './stages.js';
|
|
7
|
+
export { MAX_SURVEY_FILES, surveyWorkspace } from './survey.js';
|
|
8
|
+
export { shouldSurvey } from './trigger.js';
|
|
9
|
+
export type { Answer, InterviewQuestion, JourneyOffer, JourneyState, Stage, StageId, SurveyFacts, TriggerVerdict, } from './types.js';
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** The guided journey: trigger, survey, interview questions, offer, stages and state. */
|
|
2
|
+
export { renderAnswersBlock } from './answers-block.js';
|
|
3
|
+
export { abandon, advance, startJourney, stepDown } from './journey.js';
|
|
4
|
+
export { decideOffer } from './offer.js';
|
|
5
|
+
export { answeredBySurvey, gapQuestions } from './questions.js';
|
|
6
|
+
export { NO_TOOLS_SENTINEL, stageById, STAGES } from './stages.js';
|
|
7
|
+
export { MAX_SURVEY_FILES, surveyWorkspace } from './survey.js';
|
|
8
|
+
export { shouldSurvey } from './trigger.js';
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Answer, JourneyState } from './types.js';
|
|
2
|
+
export declare function startJourney(answers: readonly Answer[]): JourneyState;
|
|
3
|
+
/**
|
|
4
|
+
* The next stage, or `'done'` after the last one. An unrecognised stage — a
|
|
5
|
+
* session saved by a different version of Forge — ends the journey rather than
|
|
6
|
+
* throwing, so an unknown id cannot trap the person in a stage they cannot
|
|
7
|
+
* leave (spec anti-goal 5).
|
|
8
|
+
*/
|
|
9
|
+
export declare function advance(state: JourneyState): JourneyState;
|
|
10
|
+
/** The previous stage. Already at the first stage, it stays there. */
|
|
11
|
+
export declare function stepDown(state: JourneyState): JourneyState;
|
|
12
|
+
/** Leave the journey. Always available, from any stage. */
|
|
13
|
+
export declare function abandon(state: JourneyState): JourneyState;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The journey state machine.
|
|
3
|
+
*
|
|
4
|
+
* Every transition is an explicit call. Nothing advances because a stage
|
|
5
|
+
* finished or because the model said it was done — crossing a gate is a
|
|
6
|
+
* person's click, and a machine that can advance itself is a gate that can be
|
|
7
|
+
* crossed without one.
|
|
8
|
+
*
|
|
9
|
+
* Transitions return a new state and never mutate their input, so a surface can
|
|
10
|
+
* hold the previous state while a gate is open.
|
|
11
|
+
*/
|
|
12
|
+
import { STAGES } from './stages.js';
|
|
13
|
+
const ORDER = STAGES.map((s) => s.id);
|
|
14
|
+
export function startJourney(answers) {
|
|
15
|
+
return { current: ORDER[0], answers: [...answers], taskCursor: 0 };
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* The next stage, or `'done'` after the last one. An unrecognised stage — a
|
|
19
|
+
* session saved by a different version of Forge — ends the journey rather than
|
|
20
|
+
* throwing, so an unknown id cannot trap the person in a stage they cannot
|
|
21
|
+
* leave (spec anti-goal 5).
|
|
22
|
+
*/
|
|
23
|
+
export function advance(state) {
|
|
24
|
+
const at = ORDER.indexOf(state.current);
|
|
25
|
+
if (at === -1)
|
|
26
|
+
return { ...state, current: 'done' };
|
|
27
|
+
const next = ORDER[at + 1];
|
|
28
|
+
return { ...state, current: next ?? 'done' };
|
|
29
|
+
}
|
|
30
|
+
/** The previous stage. Already at the first stage, it stays there. */
|
|
31
|
+
export function stepDown(state) {
|
|
32
|
+
const at = ORDER.indexOf(state.current);
|
|
33
|
+
if (at === -1)
|
|
34
|
+
return { ...state, current: 'done' };
|
|
35
|
+
return { ...state, current: ORDER[Math.max(0, at - 1)] };
|
|
36
|
+
}
|
|
37
|
+
/** Leave the journey. Always available, from any stage. */
|
|
38
|
+
export function abandon(state) {
|
|
39
|
+
return { ...state, current: 'done' };
|
|
40
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tier 2: the real trigger.
|
|
3
|
+
*
|
|
4
|
+
* Tier 1 only decided whether looking was worth it. This decides whether to
|
|
5
|
+
* put a card in front of the person, and it does so from what the Survey
|
|
6
|
+
* actually read. That ordering is the whole defence against spec anti-goal 1:
|
|
7
|
+
* a card that arrives with evidence ("touches auth and billing") reads as
|
|
8
|
+
* earned, where a card triggered by the word "build" reads as nagging.
|
|
9
|
+
*
|
|
10
|
+
* Every string in `evidence` is a count or a name the Survey produced. Nothing
|
|
11
|
+
* here hedges or infers, because the card is the one place a wrong guess is
|
|
12
|
+
* visible to the person.
|
|
13
|
+
*/
|
|
14
|
+
import type { JourneyOffer, SurveyFacts, TriggerVerdict } from './types.js';
|
|
15
|
+
/**
|
|
16
|
+
* Decide whether to put the card in front of the person.
|
|
17
|
+
*
|
|
18
|
+
* The key case, and the one this got wrong first time: **nothing matching the
|
|
19
|
+
* request is the strongest evidence for a journey, not against it.** A request
|
|
20
|
+
* that names a subsystem the repo does not have ("add a reporting module", or
|
|
21
|
+
* anything at all in an empty folder) is new work, and new work is what the
|
|
22
|
+
* journey is for. Only a request that lands inside a *single* existing area is
|
|
23
|
+
* the repo telling us the change is scoped.
|
|
24
|
+
*/
|
|
25
|
+
export declare function decideOffer(facts: SurveyFacts, verdict: TriggerVerdict): JourneyOffer;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/** Below this many distinct areas, an existing codebase is telling us the change is scoped. */
|
|
2
|
+
const MIN_AREAS = 2;
|
|
3
|
+
/** Everything a request matched sits at the repo root, so there are no areas to count. */
|
|
4
|
+
const ROOT_AREA = '(root)';
|
|
5
|
+
/** The area of a repo-relative path: the coarsest grouping that means "part of the system". */
|
|
6
|
+
function areaOf(rel) {
|
|
7
|
+
const parts = rel.split(/[\\/]/).filter((p) => p.length > 0 && p !== '.');
|
|
8
|
+
// `src/auth/session.ts` is the auth area, not the src area.
|
|
9
|
+
if (parts.length > 2 && (parts[0] === 'src' || parts[0] === 'packages' || parts[0] === 'lib'))
|
|
10
|
+
return parts[1];
|
|
11
|
+
// A file directly in the root is not an area; calling it one would both over-offer
|
|
12
|
+
// and show the person a filename labelled as a subsystem.
|
|
13
|
+
if (parts.length < 2)
|
|
14
|
+
return ROOT_AREA;
|
|
15
|
+
return parts[0];
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Decide whether to put the card in front of the person.
|
|
19
|
+
*
|
|
20
|
+
* The key case, and the one this got wrong first time: **nothing matching the
|
|
21
|
+
* request is the strongest evidence for a journey, not against it.** A request
|
|
22
|
+
* that names a subsystem the repo does not have ("add a reporting module", or
|
|
23
|
+
* anything at all in an empty folder) is new work, and new work is what the
|
|
24
|
+
* journey is for. Only a request that lands inside a *single* existing area is
|
|
25
|
+
* the repo telling us the change is scoped.
|
|
26
|
+
*/
|
|
27
|
+
export function decideOffer(facts, verdict) {
|
|
28
|
+
if (!verdict.survey)
|
|
29
|
+
return { offer: false, evidence: [] };
|
|
30
|
+
const areas = [...new Set(facts.relatedSymbols.map(areaOf))];
|
|
31
|
+
if (facts.relatedSymbols.length === 0) {
|
|
32
|
+
const where = facts.sourceFileCount === 0
|
|
33
|
+
? 'no source files here yet'
|
|
34
|
+
: `nothing among ${facts.sourceFileCount} surveyed files is named for this request`;
|
|
35
|
+
return { offer: true, evidence: [`${where}: this is new work`, ...verdict.reasons] };
|
|
36
|
+
}
|
|
37
|
+
if (areas.length < MIN_AREAS)
|
|
38
|
+
return { offer: false, evidence: [] };
|
|
39
|
+
const evidence = [
|
|
40
|
+
`touches ${areas.length} areas: ${areas.join(', ')}`,
|
|
41
|
+
`${facts.relatedSymbols.length} related files of ${facts.sourceFileCount} surveyed`,
|
|
42
|
+
];
|
|
43
|
+
if (facts.checks.length > 0)
|
|
44
|
+
evidence.push(`checks available: ${facts.checks.map((c) => c.kind).join(', ')}`);
|
|
45
|
+
if (facts.conventionFiles.length > 0)
|
|
46
|
+
evidence.push(`conventions in ${facts.conventionFiles.join(', ')}`);
|
|
47
|
+
return { offer: true, evidence };
|
|
48
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The interview question catalogue, and the gap analysis that decides which of
|
|
3
|
+
* them survive the Survey.
|
|
4
|
+
*
|
|
5
|
+
* This file is where spec decision 13 lives: nothing branches on "greenfield"
|
|
6
|
+
* or "brownfield". Each question carries a precondition against the facts, and
|
|
7
|
+
* the number of questions a request earns is a *consequence* of what the
|
|
8
|
+
* workspace could answer, not a tier anyone has to tune. An empty folder is not
|
|
9
|
+
* a special case — it is a survey that found nothing, so every precondition
|
|
10
|
+
* comes back unanswered and all four are asked.
|
|
11
|
+
*/
|
|
12
|
+
import type { InterviewQuestion, SurveyFacts } from './types.js';
|
|
13
|
+
/**
|
|
14
|
+
* The questions this request still needs, capped at `max` (spec decision 8's
|
|
15
|
+
* budget, which `ask_user` also enforces mechanically so prompt text cannot
|
|
16
|
+
* talk past it).
|
|
17
|
+
*/
|
|
18
|
+
export declare function gapQuestions(facts: SurveyFacts, max?: number): InterviewQuestion[];
|
|
19
|
+
/** What the Survey already answered, for the record shown beside the interview. */
|
|
20
|
+
export declare function answeredBySurvey(facts: SurveyFacts): Array<{
|
|
21
|
+
id: string;
|
|
22
|
+
fact: string;
|
|
23
|
+
}>;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/** Paths that mean this codebase already has an auth story. */
|
|
2
|
+
const AUTH_PATH = /auth|session|login|oauth|permission/i;
|
|
3
|
+
/**
|
|
4
|
+
* In priority order: the first `max` unanswered questions are the ones asked.
|
|
5
|
+
* `deploy` is last because it is the one a repo can least often answer, so it
|
|
6
|
+
* survives into mature-repo journeys where the others do not.
|
|
7
|
+
*/
|
|
8
|
+
const CATALOGUE = [
|
|
9
|
+
{
|
|
10
|
+
id: 'stack',
|
|
11
|
+
question: 'Which stack should this use?',
|
|
12
|
+
options: ['Next.js + Postgres', 'Django + Postgres', 'Rails + Postgres', 'Express + SQLite'],
|
|
13
|
+
answeredBy: (f) => f.manifest ? `${f.manifest.path}${f.manifest.packageManager ? ` (${f.manifest.packageManager})` : ''}` : undefined,
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
id: 'modules',
|
|
17
|
+
question: 'Which parts should it cover?',
|
|
18
|
+
options: ['Tasks', 'Boards', 'Time tracking', 'Invoicing', 'Reporting', 'Notifications'],
|
|
19
|
+
multiSelect: true,
|
|
20
|
+
answeredBy: (f) => (f.sourceFileCount > 0 ? `${f.sourceFileCount} existing source files` : undefined),
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
id: 'authScale',
|
|
24
|
+
question: 'Who signs in, and roughly how many of them?',
|
|
25
|
+
options: ['One team, internal', 'Many organisations, each isolated', 'Public sign-up'],
|
|
26
|
+
answeredBy: (f) => f.relatedSymbols.find((p) => AUTH_PATH.test(p)),
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
id: 'deploy',
|
|
30
|
+
question: 'Where will this run?',
|
|
31
|
+
options: ['Vercel or similar', 'A container somewhere', 'A VM we manage', 'Not decided yet'],
|
|
32
|
+
answeredBy: () => undefined,
|
|
33
|
+
},
|
|
34
|
+
];
|
|
35
|
+
/**
|
|
36
|
+
* The questions this request still needs, capped at `max` (spec decision 8's
|
|
37
|
+
* budget, which `ask_user` also enforces mechanically so prompt text cannot
|
|
38
|
+
* talk past it).
|
|
39
|
+
*/
|
|
40
|
+
export function gapQuestions(facts, max = 4) {
|
|
41
|
+
return CATALOGUE.filter((q) => q.answeredBy(facts) === undefined).slice(0, max);
|
|
42
|
+
}
|
|
43
|
+
/** What the Survey already answered, for the record shown beside the interview. */
|
|
44
|
+
export function answeredBySurvey(facts) {
|
|
45
|
+
return CATALOGUE.flatMap((q) => {
|
|
46
|
+
const fact = q.answeredBy(facts);
|
|
47
|
+
return fact === undefined ? [] : [{ id: q.id, fact }];
|
|
48
|
+
});
|
|
49
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Stage, StageId } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* "This stage runs no tools."
|
|
4
|
+
*
|
|
5
|
+
* It cannot be `[]`: `applySandbox` treats an empty `allowTools` as *no
|
|
6
|
+
* whitelist at all*, so an empty list would hand a host-driven stage every
|
|
7
|
+
* tool, writes included. `vscode/src/chat/modes.ts` already learned this and
|
|
8
|
+
* carries its own copy of the same trick; this one lives in core so a surface
|
|
9
|
+
* wiring stages into a sandbox gets the safe value without rediscovering it.
|
|
10
|
+
*/
|
|
11
|
+
export declare const NO_TOOLS_SENTINEL = "__forge_no_tools__";
|
|
12
|
+
export declare const STAGES: readonly Stage[];
|
|
13
|
+
export declare function stageById(id: StageId): Stage | undefined;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The six stages of a guided journey.
|
|
3
|
+
*
|
|
4
|
+
* A stage is a prompt suffix plus a tool set, which is the point: gating reuses
|
|
5
|
+
* the sandbox the agent already applies, so "implementation cannot start yet"
|
|
6
|
+
* is the write tools being absent from the list rather than an instruction the
|
|
7
|
+
* model may decline. That is the one enforcement mechanism in this design that
|
|
8
|
+
* a model cannot talk past.
|
|
9
|
+
*
|
|
10
|
+
* `allowTools` takes its write-tool names from `FILE_WRITE_TOOL_NAMES` rather
|
|
11
|
+
* than retyping them: a tool added to that list and missed here would be a
|
|
12
|
+
* write the Build gate does not cover (RB-13).
|
|
13
|
+
*/
|
|
14
|
+
import { FILE_WRITE_TOOL_NAMES } from '../tools/write-tools.js';
|
|
15
|
+
const READ_TOOLS = ['read_file', 'list_files', 'search_files', 'find_symbol', 'list_symbols', 'find_references'];
|
|
16
|
+
/**
|
|
17
|
+
* "This stage runs no tools."
|
|
18
|
+
*
|
|
19
|
+
* It cannot be `[]`: `applySandbox` treats an empty `allowTools` as *no
|
|
20
|
+
* whitelist at all*, so an empty list would hand a host-driven stage every
|
|
21
|
+
* tool, writes included. `vscode/src/chat/modes.ts` already learned this and
|
|
22
|
+
* carries its own copy of the same trick; this one lives in core so a surface
|
|
23
|
+
* wiring stages into a sandbox gets the safe value without rediscovering it.
|
|
24
|
+
*/
|
|
25
|
+
export const NO_TOOLS_SENTINEL = '__forge_no_tools__';
|
|
26
|
+
export const STAGES = [
|
|
27
|
+
{
|
|
28
|
+
id: 'survey',
|
|
29
|
+
usesModel: false,
|
|
30
|
+
promptSuffix: '',
|
|
31
|
+
allowTools: [NO_TOOLS_SENTINEL],
|
|
32
|
+
artefact: 'survey facts',
|
|
33
|
+
gateOptions: [],
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
id: 'interview',
|
|
37
|
+
usesModel: false,
|
|
38
|
+
promptSuffix: '',
|
|
39
|
+
allowTools: [NO_TOOLS_SENTINEL],
|
|
40
|
+
artefact: 'interview answers',
|
|
41
|
+
gateOptions: [],
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
id: 'plan',
|
|
45
|
+
usesModel: true,
|
|
46
|
+
promptSuffix: [
|
|
47
|
+
'',
|
|
48
|
+
'PLAN STAGE — state the plan and stop.',
|
|
49
|
+
'- Produce a short numbered plan of the work, grounded in what you can read.',
|
|
50
|
+
'- You cannot ask questions at this stage: the interview is over. Where something',
|
|
51
|
+
' is genuinely undecided, state it as an assumption in the plan.',
|
|
52
|
+
'- Do not write or change any file.',
|
|
53
|
+
].join('\n'),
|
|
54
|
+
allowTools: [...READ_TOOLS],
|
|
55
|
+
artefact: 'a plan, in chat',
|
|
56
|
+
gateOptions: ['Approve the plan', 'Revise it', 'Skip to the spec'],
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
id: 'spec',
|
|
60
|
+
usesModel: true,
|
|
61
|
+
promptSuffix: [
|
|
62
|
+
'',
|
|
63
|
+
'SPEC STAGE — produce exactly two sections and stop.',
|
|
64
|
+
'- `## requirements.md` then `## design.md`, each starting with its own heading line.',
|
|
65
|
+
'- Inside a section use `###` or deeper for headings: any `##` line ends the section.',
|
|
66
|
+
'- Requirements: user stories with EARS-syntax acceptance criteria, e.g. "WHEN <trigger> THE SYSTEM SHALL <response>".',
|
|
67
|
+
' Number each requirement (1, 2, 3, ...) and its criteria (1.1, 1.2, ...) so the implementation plan can cite them.',
|
|
68
|
+
'- Design: ground it in what you read: data flow, schemas, interfaces, API endpoints, and the existing patterns to follow.',
|
|
69
|
+
'- The implementation plan is a separate stage: do not write tasks here.',
|
|
70
|
+
'- You cannot write files at this stage. Reply with the sections as text, with no preamble and no closing summary.',
|
|
71
|
+
].join('\n'),
|
|
72
|
+
allowTools: [...READ_TOOLS],
|
|
73
|
+
artefact: 'requirements.md, design.md',
|
|
74
|
+
gateOptions: ['Approve the spec', 'Revise it'],
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
id: 'implPlan',
|
|
78
|
+
usesModel: true,
|
|
79
|
+
promptSuffix: [
|
|
80
|
+
'',
|
|
81
|
+
'IMPLEMENTATION PLAN STAGE — produce `## tasks.md` and stop.',
|
|
82
|
+
'- A markdown checklist: "- [ ] 1. Title", subtasks indented as " - [ ] 1.1 Title".',
|
|
83
|
+
'- Under each task add a line like "_Requirements: 2.1, 3.2_", citing the requirement numbers you were given.',
|
|
84
|
+
'- One checkbox per line, never two on a line.',
|
|
85
|
+
'- You cannot write files at this stage. Reply with the document as text, with no preamble and no closing summary.',
|
|
86
|
+
].join('\n'),
|
|
87
|
+
allowTools: [...READ_TOOLS],
|
|
88
|
+
artefact: 'tasks.md',
|
|
89
|
+
gateOptions: ['Approve the tasks', 'Revise them'],
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
id: 'build',
|
|
93
|
+
usesModel: true,
|
|
94
|
+
promptSuffix: [
|
|
95
|
+
'',
|
|
96
|
+
'BUILD STAGE — do the task you were given and stop at it.',
|
|
97
|
+
'- Work only on the current task; the next one is gated on the person.',
|
|
98
|
+
'- Run the project\'s own check when the change could break something.',
|
|
99
|
+
].join('\n'),
|
|
100
|
+
allowTools: [...READ_TOOLS, ...FILE_WRITE_TOOL_NAMES, 'run_command'],
|
|
101
|
+
artefact: 'ticked tasks',
|
|
102
|
+
gateOptions: ['Continue to the next task', 'Retry this one', 'Skip it', 'Stop', 'Run the rest without asking'],
|
|
103
|
+
},
|
|
104
|
+
];
|
|
105
|
+
export function stageById(id) {
|
|
106
|
+
return STAGES.find((s) => s.id === id);
|
|
107
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { SurveyFacts } from './types.js';
|
|
2
|
+
/** Ceiling on the tree walk, so a monorepo costs the same as a small repo. */
|
|
3
|
+
export declare const MAX_SURVEY_FILES = 400;
|
|
4
|
+
/** Second ceiling: a tree of mostly empty or non-source directories is bounded too. */
|
|
5
|
+
export declare const MAX_SURVEY_DIRS = 2000;
|
|
6
|
+
/**
|
|
7
|
+
* Read what this workspace already says about itself. Never throws: a fact that
|
|
8
|
+
* cannot be read is absent.
|
|
9
|
+
*/
|
|
10
|
+
export declare function surveyWorkspace(cwd: string, request: string): Promise<SurveyFacts>;
|