From 45d32114ac537bb368c9d568b4dfd77940644321 Mon Sep 17 00:00:00 2001 From: Tom Softreck Date: Tue, 4 Aug 2026 18:35:27 +0200 Subject: [PATCH 1/6] refactor: split python SDK basic example flow --- sdk/python/examples/basic.py | 176 +++++++++++++++++++++++++++++------ 1 file changed, 146 insertions(+), 30 deletions(-) diff --git a/sdk/python/examples/basic.py b/sdk/python/examples/basic.py index 6bf8dce..01cc200 100644 --- a/sdk/python/examples/basic.py +++ b/sdk/python/examples/basic.py @@ -12,37 +12,101 @@ import os import sys +from dataclasses import dataclass from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parents[1])) from todo2code import T2CClient, T2CError # noqa: E402 +DEFAULT_A2A_URL = "http://localhost:8787" +DEFAULT_EXAMPLE_ROOT = "examples/backend" +DEFAULT_COMPARE_BASE = "origin/main" +TODO_PATCH_PATH = ".intent-sdk/python/TODO.patch" +TODO_AUDIT_PATH = ".intent-sdk/python/TODO.patch.json" +TODO_RECEIPT_PATH = ".intent-sdk/python/TODO.patch.receipt.json" -def main() -> int: - base_url = os.environ.get("T2C_A2A_URL", "http://localhost:8787") - token = os.environ.get("T2C_A2A_TOKEN") - root = os.environ.get("T2C_EXAMPLE_ROOT", "examples/backend") - client = T2CClient(base_url, token=token) +@dataclass(frozen=True) +class ExampleContext: + base_url: str + token: str | None + root: str + compare_base: str + compare_workspace: bool + + +@dataclass(frozen=True) +class ExtractionArtifacts: + graph: object + report: object + record_count: int + + +@dataclass(frozen=True) +class ProposalArtifacts: + new_proposal_ids: tuple[str, ...] + duplicate_proposal_ids: tuple[str, ...] + patch_hash: str + + +def main() -> int: + context = read_example_context() + client = T2CClient(context.base_url, token=context.token) print("health:", client.health()) + run_flow(client, context) + print("OK") + return 0 + + +def read_example_context() -> ExampleContext: + return ExampleContext( + base_url=os.environ.get("T2C_A2A_URL", DEFAULT_A2A_URL), + token=os.environ.get("T2C_A2A_TOKEN"), + root=os.environ.get("T2C_EXAMPLE_ROOT", DEFAULT_EXAMPLE_ROOT), + compare_base=os.environ.get("T2C_COMPARE_BASE", DEFAULT_COMPARE_BASE), + compare_workspace=os.environ.get("T2C_COMPARE_WORKSPACE", "0") == "1", + ) + + +def run_flow(client: T2CClient, context: ExampleContext) -> None: card = client.agent_card() print("agent skills:", ", ".join(skill["id"] for skill in card.get("skills", ()))) - # 1. Deterministic extraction -> graph -> diagnostics. + extraction = run_extraction_flow(client, context.root) + print(f"extracted {extraction.record_count} records from {context.root}") + + proposal = run_proposal_flow( + client, + context.root, + extraction.graph, + extraction.report, + ) + print("proposal ids:", ",".join(proposal.new_proposal_ids) or "-") + print("duplicate ids:", ",".join(proposal.duplicate_proposal_ids) or "-") + print("patch fingerprint:", proposal.patch_hash[:16]) + + run_reality_and_diff(client, extraction.graph, extraction.report, context.root) + run_optional_workspace_comparison( + client, + context.root, + context.compare_base, + context.compare_workspace, + ) + + +def run_extraction_flow(client: T2CClient, root: str) -> ExtractionArtifacts: nl = client.extract_nl_result("task.md", root, nl_mode="deterministic") - if nl.audit is None or nl.audit.get("status") != "succeeded" or nl.audit.get("effectiveMode") != "deterministic": - raise RuntimeError(f"unexpected NL audit: {nl.audit}") + assert_audit_success(nl.audit, "NL", check_mode=True) print("NL audit:", nl.audit.get("status"), nl.audit.get("effectiveMode")) + ast_records = client.extract_ast(root) markdown = client.extract_markdown_result(root, markdown_mode="deterministic") - if markdown.audit is None or markdown.audit.get("status") != "succeeded": - raise RuntimeError(f"unexpected Markdown audit: {markdown.audit}") + assert_audit_success(markdown.audit, "Markdown") print("markdown audit:", markdown.audit.get("status"), markdown.audit.get("effectiveMode")) - records = [*nl.records, *ast_records, *markdown.records] - print(f"extracted {len(records)} records from {root}") + records = [*nl.records, *ast_records, *markdown.records] graph = client.link(records) print("graph fingerprint:", graph.fingerprint[:16]) print("records by source:", graph.stats.get("bySource")) @@ -52,39 +116,91 @@ def main() -> int: for diagnostic in report.diagnostics[:3]: print(f" - [{diagnostic.severity}] {diagnostic.code}: {diagnostic.title}") - # 2. Audited propose -> review -> approved no-op apply without secrets. - synthesis = client.propose_todo({"root": root, "graph": graph.raw, "diagnostics": report.raw, "mode": "prefer-llm"}) + return ExtractionArtifacts( + graph=graph, + report=report, + record_count=len(records), + ) + + +def assert_audit_success( + audit: dict[str, object] | None, + label: str, + check_mode: bool = False, +) -> None: + if audit is None or audit.get("status") != "succeeded": + raise RuntimeError(f"unexpected {label} audit: {audit}") + if check_mode and audit.get("effectiveMode") != "deterministic": + raise RuntimeError(f"unexpected {label} mode: {audit.get('effectiveMode')} for {label}") + + +def run_proposal_flow( + client: T2CClient, + root: str, + graph: object, + report: object, +) -> ProposalArtifacts: + synthesis = client.propose_todo({ + "root": root, + "graph": graph.raw, + "diagnostics": report.raw, + "mode": "prefer-llm", + }) validation = synthesis.get("validation", {}) + rendered = client.render_todo({ - "root": root, "graph": graph.raw, "diagnostics": report.raw, "synthesis": synthesis, "todo": "TODO.md", - "patch": ".intent-sdk/python/TODO.patch", "audit": ".intent-sdk/python/TODO.patch.json", + "root": root, + "graph": graph.raw, + "diagnostics": report.raw, + "synthesis": synthesis, + "todo": "TODO.md", + "patch": TODO_PATCH_PATH, + "audit": TODO_AUDIT_PATH, }) + patch_hash = rendered["artifact"]["renderedPatchHash"] client.apply_todo({ - "root": root, "todo": "TODO.md", "patch": ".intent-sdk/python/TODO.patch", - "audit": ".intent-sdk/python/TODO.patch.json", "receipt": ".intent-sdk/python/TODO.patch.receipt.json", - "actor": "sdk-python", "approvalHash": patch_hash, + "root": root, + "todo": "TODO.md", + "patch": TODO_PATCH_PATH, + "audit": TODO_AUDIT_PATH, + "receipt": TODO_RECEIPT_PATH, + "actor": "sdk-python", + "approvalHash": patch_hash, }) - print("proposal ids:", ",".join(validation.get("newProposalIds", ())) or "-") - print("duplicate ids:", ",".join(validation.get("duplicateProposalIds", ())) or "-") - print("patch fingerprint:", patch_hash[:16]) - # 3. Intent-vs-reality view. + return ProposalArtifacts( + new_proposal_ids=tuple(validation.get("newProposalIds", ())), + duplicate_proposal_ids=tuple(validation.get("duplicateProposalIds", ())), + patch_hash=patch_hash, + ) + + +def run_reality_and_diff( + client: T2CClient, + graph: object, + report: object, + root: str, +) -> None: reality = client.reality(graph, report, gapsOnly=True, includeSvg=True) print("reality svg bytes:", len(reality.get("svg", ""))) print(reality["markdown"].split("\n")[4]) - # 4. Git diff rendered as SVG. git_diff = client.diff_git(root=root, revision="HEAD", includeSvg=True) print("git diff files:", len(git_diff.get("diffs", ()))) - # 5. Optional origin/main -> local filesystem Intent comparison. - if os.environ.get("T2C_COMPARE_WORKSPACE") == "1": - comparison = client.compare_workspace(root=root, base=os.environ.get("T2C_COMPARE_BASE", "origin/main")) - print("workspace trend:", comparison.get("trend", {}).get("direction")) - print("OK") - return 0 +def run_optional_workspace_comparison( + client: T2CClient, + root: str, + compare_base: str, + compare_workspace: bool, +) -> None: + if not compare_workspace: + return + + comparison = client.compare_workspace(root=root, base=compare_base) + print("workspace trend:", comparison.get("trend", {}).get("direction")) if __name__ == "__main__": From ea3ae0ad3ac8aa0f0bd0f51ab47e70b846775cbc Mon Sep 17 00:00:00 2001 From: Tom Softreck Date: Tue, 4 Aug 2026 18:55:01 +0200 Subject: [PATCH 2/6] refactor: split generation metadata synthesis helpers --- src/summary/generation-metadata.ts | 84 ++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 src/summary/generation-metadata.ts diff --git a/src/summary/generation-metadata.ts b/src/summary/generation-metadata.ts new file mode 100644 index 0000000..0b86dbc --- /dev/null +++ b/src/summary/generation-metadata.ts @@ -0,0 +1,84 @@ +import { sha256, stableStringify } from '../core/id.js'; +import type { T2CConfig } from '../config/env.js'; +import { openRouterAuditConfiguration } from '../llm/audit.js'; +import { T2C_VERSION } from '../version.js'; +import type { + GroundedGenerationMetadata, + LlmResponseMetadata, +} from '../core/types.js'; + +export function generationMetadata( + config: T2CConfig, + mode: GroundedGenerationMetadata['requestedMode'], + response?: LlmResponseMetadata, + reason?: string, +): GroundedGenerationMetadata { + return { + generator: 't2c/grounded-summary', + generatorVersion: '2', + runtimeVersion: T2C_VERSION, + generatedAt: new Date().toISOString(), + requestedMode: mode, + ...resolveGenerationSummary(response, mode), + model: resolveGenerationModel(config, response), + provider: resolveGenerationProvider(response), + responseId: resolveResponseId(response), + configurationFingerprint: sha256(stableStringify(resolveGenerationConfiguration(config, mode))), + reason: resolveGenerationReason(response, mode, reason), + }; +} + +function resolveGenerationSummary( + response: LlmResponseMetadata | undefined, + mode: GroundedGenerationMetadata['requestedMode'], +): Pick { + const effectiveMode = resolveGenerationMode(response); + return { + effectiveMode, + degraded: shouldDegradeGeneration(mode, effectiveMode), + }; +} + +function resolveGenerationMode(response?: LlmResponseMetadata): 'llm' | 'deterministic' { + return response ? 'llm' : 'deterministic'; +} + +function shouldDegradeGeneration( + mode: GroundedGenerationMetadata['requestedMode'], + effectiveMode: 'llm' | 'deterministic', +): boolean { + return mode === 'prefer-llm' && effectiveMode === 'deterministic'; +} + +function resolveGenerationModel(config: T2CConfig, response?: LlmResponseMetadata): string | null { + return response ? response.model ?? config.openRouter.summaryModel : null; +} + +function resolveGenerationProvider(response?: LlmResponseMetadata): string | null { + return response ? response.provider ?? 'openrouter' : null; +} + +function resolveResponseId(response?: LlmResponseMetadata): string | null { + return response?.responseId ?? null; +} + +function resolveGenerationReason( + response: LlmResponseMetadata | undefined, + mode: GroundedGenerationMetadata['requestedMode'], + reason?: string, +): string | null { + if (!shouldDegradeGeneration(mode, resolveGenerationMode(response))) { + return null; + } + return reason ?? 'LLM_UNAVAILABLE'; +} + +function resolveGenerationConfiguration( + config: T2CConfig, + mode: GroundedGenerationMetadata['requestedMode'], +) { + return openRouterAuditConfiguration( + config, + mode === 'deterministic' ? null : config.openRouter.summaryModel, + ); +} From d0a4825d01503e7151f55badf91a0357f84557f5 Mon Sep 17 00:00:00 2001 From: Tom Softreck Date: Tue, 4 Aug 2026 18:54:54 +0200 Subject: [PATCH 3/6] refactor: split and wire-based intake CLI decoding --- src/interfaces/intake_cli.py | 125 ++++++++++++++++++++++++----------- 1 file changed, 86 insertions(+), 39 deletions(-) diff --git a/src/interfaces/intake_cli.py b/src/interfaces/intake_cli.py index d430580..457b3ad 100644 --- a/src/interfaces/intake_cli.py +++ b/src/interfaces/intake_cli.py @@ -23,6 +23,7 @@ 5: "idempotencyKey", 6: "authenticatedPrincipal", 7: "expectedVersion", 8: "timestamp", 9: "payloadHash", 10: "payload", } +SKIP_UNKNOWN_FIELD = object() def _varint(value: int) -> bytes: @@ -81,32 +82,55 @@ def decode_envelope(data: bytes) -> dict[str, Any]: offset = 0 while offset < len(data): start = offset - tag, offset = _read_varint(data, offset) - number, wire = tag >> 3, tag & 7 - if wire == 0: - value, offset = _read_varint(data, offset) - if number == 7: - output["expectedVersion"] = value - else: - unknown.append(base64.b64encode(data[start:offset]).decode()) - elif wire == 2: - length, offset = _read_varint(data, offset) - end = offset + length - if end > len(data): - raise ValueError("truncated length-delimited field") - raw = data[offset:end] - offset = end - if number in FIELDS and number != 7: - output[FIELDS[number]] = json.loads(raw) if number == 10 else raw.decode() - else: - unknown.append(base64.b64encode(data[start:offset]).decode()) - else: - raise ValueError(f"unsupported wire type {wire}") + number, wire, value_start, offset = read_field_metadata(data, offset) + value = read_field_value(data, number, wire, value_start, offset) + if value is SKIP_UNKNOWN_FIELD: + unknown.append(base64.b64encode(data[start:offset]).decode()) + continue + if value is not None: + output[value[0]] = value[1] if unknown: output["unknownFields"] = unknown return output +def read_field_metadata(data: bytes, offset: int) -> tuple[int, int, int, int]: + tag, offset = _read_varint(data, offset) + number, wire = tag >> 3, tag & 7 + if wire == 0: + value_start = offset + _, offset = _read_varint(data, offset) + return number, wire, value_start, offset + if wire == 2: + length, offset = _read_varint(data, offset) + end = offset + length + if end > len(data): + raise ValueError("truncated length-delimited field") + return number, wire, offset, end + raise ValueError(f"unsupported wire type {wire}") + + +def read_field_value( + data: bytes, + number: int, + wire: int, + value_start: int, + value_end: int, +) -> tuple[str, Any] | object | None: + if value_start > len(data) or value_end > len(data) or value_end < value_start: + raise ValueError("invalid field payload") + if wire == 0 and number == 7: + value, _ = _read_varint(data, value_start) + return ("expectedVersion", value) + if wire == 0: + return SKIP_UNKNOWN_FIELD + if wire == 2 and number in FIELDS and number != 7: + payload = data[value_start:value_end] + field_value = json.loads(payload) if number == 10 else payload.decode() + return (FIELDS[number], field_value) + return SKIP_UNKNOWN_FIELD + + def execute(args: argparse.Namespace) -> int: repository = pathlib.Path(args.repository).resolve() cli = repository / "dist" / "src" / "cli.js" @@ -119,35 +143,58 @@ def execute(args: argparse.Namespace) -> int: return subprocess.run(command, cwd=repository, check=False).returncode -def main() -> int: +def run_command(args: argparse.Namespace) -> int: + return execute(args) + + +def run_encode(args: argparse.Namespace) -> int: + envelope = json.loads(pathlib.Path(args.input).read_text(encoding="utf-8")) + pathlib.Path(args.output).write_bytes(encode_envelope(envelope)) + return 0 + + +def run_decode(args: argparse.Namespace) -> int: + envelope = decode_envelope(pathlib.Path(args.input).read_bytes()) + pathlib.Path(args.output).write_text(json.dumps(envelope, indent=2, ensure_ascii=False) + "\n", encoding="utf-8") + return 0 + + +def build_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser(description="Dependency-free todo2code governed-intake CLI") sub = parser.add_subparsers(dest="mode", required=True) for operation in ("command", "query"): - run = sub.add_parser(operation) - run.add_argument("input") - run.add_argument("--repository", default=".") - run.add_argument("--root", default=".") - run.add_argument("--project-dir", default="project") - run.add_argument("--protobuf", action="store_true") + add_command_parser(sub, operation) + encode = sub.add_parser("encode") encode.add_argument("input") encode.add_argument("output") + encode.set_defaults(handler=run_encode) + decode = sub.add_parser("decode") decode.add_argument("input") decode.add_argument("output") + decode.set_defaults(handler=run_decode) + + return parser + + +def add_command_parser(sub: argparse._SubParsersAction, operation: str) -> None: + run = sub.add_parser(operation) + run.add_argument("input") + run.add_argument("--repository", default=".") + run.add_argument("--root", default=".") + run.add_argument("--project-dir", default="project") + run.add_argument("--protobuf", action="store_true") + run.set_defaults(handler=run_command, operation=operation) + + +def main() -> int: + parser = build_parser() args = parser.parse_args() try: - if args.mode in ("command", "query"): - args.operation = args.mode - return execute(args) - if args.mode == "encode": - envelope = json.loads(pathlib.Path(args.input).read_text(encoding="utf-8")) - pathlib.Path(args.output).write_bytes(encode_envelope(envelope)) - else: - envelope = decode_envelope(pathlib.Path(args.input).read_bytes()) - pathlib.Path(args.output).write_text(json.dumps(envelope, indent=2, ensure_ascii=False) + "\n", encoding="utf-8") - return 0 - except (OSError, ValueError, json.JSONDecodeError) as error: + result = args.handler(args) + return int(result) + except (OSError, ValueError, json.JSONDecodeError, UnicodeError) as error: print(f"T2C-INTAKE-INVALID-WIRE: {error}", file=sys.stderr) return 2 From eba140fb23d62bf1322cbd9386885966ab3f17a9 Mon Sep 17 00:00:00 2001 From: Tom Softreck Date: Tue, 4 Aug 2026 18:59:54 +0200 Subject: [PATCH 4/6] fix: repair syntax errors in generation validation and run helpers --- src/operations/generation-validation.ts | 98 +++++++++++++ src/pipeline/run-helpers.ts | 178 ++++++++++++++++++++++++ 2 files changed, 276 insertions(+) create mode 100644 src/operations/generation-validation.ts create mode 100644 src/pipeline/run-helpers.ts diff --git a/src/operations/generation-validation.ts b/src/operations/generation-validation.ts new file mode 100644 index 0000000..fea72a2 --- /dev/null +++ b/src/operations/generation-validation.ts @@ -0,0 +1,98 @@ +import type { GroundedGenerationMetadata } from '../core/types.js'; + +const SHA256 = /^[a-f0-9]{64}$/; +const GENERATION_REQUIRED_FIELDS = [ + 'generator', + 'generatorVersion', + 'runtimeVersion', + 'generatedAt', + 'requestedMode', + 'effectiveMode', + 'degraded', + 'model', + 'provider', + 'responseId', + 'configurationFingerprint', + 'reason', +] as const; + +const REQUESTED_MODES = ['deterministic', 'prefer-llm', 'require-llm'] as const; +const EFFECTIVE_MODES = ['deterministic', 'llm'] as const; + +function asObject(value: unknown, name: string): Record { + if (!value || typeof value !== 'object' || Array.isArray(value)) { + throw new Error(`${name} must be an object`); + } + return value as Record; +} + +function assertExactKeys(value: Record, expected: string[], name: string): void { + const actual = Object.keys(value).sort(); + const wanted = [...expected].sort(); + if (JSON.stringify(actual) !== JSON.stringify(wanted)) { + throw new Error(`${name} keys must be exactly: ${expected.join(', ')}`); + } +} + +function assertNonBlank(value: unknown, name: string): asserts value is string { + if (typeof value !== 'string' || !value.trim()) throw new Error(`${name} must be a non-blank string`); +} + +function assertDateString(value: unknown, name: string): void { + assertNonBlank(value, name); + if (!Number.isFinite(Date.parse(value))) throw new Error(`${name} must be an ISO date-time`); +} + +export function assertGeneration(value: unknown): asserts value is GroundedGenerationMetadata { + const generation = asObject(value, 'Operation plan generation'); + assertExactKeys(generation, [...GENERATION_REQUIRED_FIELDS], 'Operation plan generation'); + assertGenerationRequiredTextFields(generation); + assertDateString(generation.generatedAt, 'generation.generatedAt'); + assertGenerationModes(generation); + assertGenerationOptionalTextFields(generation); + assertGenerationProvenanceRules(generation); +} + +function assertGenerationRequiredTextFields(generation: Record): void { + for (const field of ['generator', 'generatorVersion', 'runtimeVersion'] as const) assertNonBlank(generation[field], `generation.${field}`); +} + +function assertGenerationModes(generation: Record): void { + if (!isAllowedGenerationMode(generation.requestedMode, REQUESTED_MODES)) { + throw new Error('generation.requestedMode is invalid'); + } + if (!isAllowedGenerationMode(generation.effectiveMode, EFFECTIVE_MODES)) { + throw new Error('generation.effectiveMode is invalid'); + } +} + +function assertGenerationOptionalTextFields(generation: Record): void { + for (const field of ['model', 'provider', 'responseId', 'reason'] as const) { + if (generation[field] !== null) assertNonBlank(generation[field], `generation.${field}`); + } +} + +function assertGenerationProvenanceRules(generation: Record): void { + if (typeof generation.degraded !== 'boolean') throw new Error('generation.degraded must be a boolean'); + if (typeof generation.configurationFingerprint !== 'string' || !SHA256.test(generation.configurationFingerprint)) { + throw new Error('generation.configurationFingerprint must be SHA-256'); + } + + if (generation.effectiveMode === 'llm' && (generation.model === null || generation.provider === null)) { + throw new Error('LLM operation plans require model and provider provenance'); + } + if (isDeterministicModeProvenance(generation)) { + throw new Error('Deterministic operation plans cannot claim LLM provenance'); + } +} + +function isAllowedGenerationMode(value: unknown, allowed: readonly string[]): boolean { + return allowed.includes(String(value)); +} + +function isDeterministicModeProvenance(generation: Record): boolean { + return ( + generation.effectiveMode === 'deterministic' + && (generation.model !== null || generation.provider !== null || generation.responseId !== null) + ); +} diff --git a/src/pipeline/run-helpers.ts b/src/pipeline/run-helpers.ts new file mode 100644 index 0000000..9a9b794 --- /dev/null +++ b/src/pipeline/run-helpers.ts @@ -0,0 +1,178 @@ +import path from 'node:path'; + +import { createCodeChangeReviewPatch, createCodeChangeSourcePatchSet, createRepositoryPathProbe, proposeCodeChangePlans } from '../synthesis/code-change-plan.js'; +import { extractCommunicationIntentAudited, type ParticipantCommunicationSynthesis } from '../communication/llm.js'; +import { type AuditedTaskSynthesisResult, synthesizeTodoProposals } from '../synthesis/tasks-llm.js'; +import { createTodoPatch, type CreatedTodoPatch } from '../synthesis/todo-patch.js'; +import { createIntentId } from '../core/id.js'; +import type { Diagnostic, DiagnosticReport, IntentRecord, PipelineOptions, PipelineStageAudit } from '../core/types.js'; +import { readText } from '../core/io.js'; +import { T2C_VERSION } from '../version.js'; +import { openRouterAuditConfiguration } from '../llm/audit.js'; +import type { T2CConfig } from '../config/env.js'; +import type { PipelineContext } from './run-types.js'; +import { skippedAudit } from './run-failed.js'; + +export async function collectCommunicationAnalysis( + context: PipelineContext, + options: Pick, + config: T2CConfig, +): Promise<{ + audit: PipelineStageAudit; + syntheses: ParticipantCommunicationSynthesis[]; + missingDirectory: boolean; +}> { + const { root, warnings, bySource } = context; + const includeCommunication = options.includeCommunication !== false; + const communicationStartedAt = Date.now(); + let communicationAudit: PipelineStageAudit = skippedAudit('disabled', 'Communication analysis was disabled'); + let missingDirectory = false; + let communicationSyntheses: ParticipantCommunicationSynthesis[] = []; + + if (!includeCommunication) { + return { audit: communicationAudit, syntheses: communicationSyntheses, missingDirectory: true }; + } + + context.activeStage = 'communicationAnalysis'; + const communication = await extractCommunicationIntentAudited({ + root, + projectDir: options.projectDirectory ?? 'project', + ticket: options.communicationTicket ?? null, + }, config, options.communicationMode ?? config.communicationMode); + const foundMissingDirectory = communication.records.length === 0 + && communication.warnings.length === 1 + && communication.warnings[0]?.startsWith('Communication directory not found:'); + if (!foundMissingDirectory) warnings.push(...communication.warnings); + bySource.communication = communication.records; + communicationSyntheses = communication.participants; + missingDirectory = foundMissingDirectory; + if (!foundMissingDirectory) { + communicationAudit = { + ...communication.audit, + durationMs: Date.now() - communicationStartedAt, + effectiveMode: communication.audit.effectiveMode, + }; + } else { + communicationAudit = skippedAudit('deterministic', communication.warnings[0] ?? 'Communication directory not found'); + } + + return { + audit: communicationAudit, + syntheses: communicationSyntheses, + missingDirectory, + }; +} + +export async function collectTaskSynthesis( + context: PipelineContext, + options: Pick, + config: T2CConfig, + root: string, + graph: Parameters[0], + diagnostics: DiagnosticReport, +): Promise<{ result: AuditedTaskSynthesisResult | null; patch: CreatedTodoPatch | null; audit: PipelineStageAudit }> { + const { warnings } = context; + const taskSynthesisMode = options.taskSynthesisMode ?? 'disabled'; + let taskSynthesis: AuditedTaskSynthesisResult | null = null; + let todoPatch: CreatedTodoPatch | null = null; + let taskSynthesisAudit = skippedAudit('disabled', 'Task synthesis was disabled'); + + if (taskSynthesisMode === 'disabled') { + return { result: taskSynthesis, patch: todoPatch, audit: taskSynthesisAudit }; + } + + context.activeStage = 'taskSynthesis'; + taskSynthesis = await synthesizeTodoProposals(graph, diagnostics, config, taskSynthesisMode); + warnings.push(...taskSynthesis.warnings); + taskSynthesisAudit = taskSynthesis.audit; + + if (!options.todoFile) throw new Error('Task synthesis rendering requires a TODO source file'); + context.activeStage = 'todoRendering'; + const todoContent = await readText(path.resolve(root, options.todoFile), config.maxFileBytes); + todoPatch = createTodoPatch({ + todoPath: path.relative(root, path.resolve(root, options.todoFile)).replace(/\\/g, '/'), + todoContent, + graph, + diagnostics, + conclusions: taskSynthesis.conclusions, + proposals: taskSynthesis.proposals, + validation: taskSynthesis.validation, + synthesisAudit: taskSynthesis.audit, + }); + return { result: taskSynthesis, patch: todoPatch, audit: taskSynthesisAudit }; +} + +export function createCodeChangeArtifacts( + graph: Parameters[0]['graph'], + diagnostics: DiagnosticReport, + generatedAt: string, + root: string, + taskSynthesis: AuditedTaskSynthesisResult | null, + config: T2CConfig, +) { + const codeChangePlans = proposeCodeChangePlans({ + graph, + diagnostics, + ...(taskSynthesis + ? { conclusions: taskSynthesis.conclusions, proposals: taskSynthesis.proposals } + : {}), + generatedAt, + pathExists: createRepositoryPathProbe(root), + }); + const codeChangeReview = createCodeChangeReviewPatch({ + plans: codeChangePlans.plans, + graphFingerprint: graph.fingerprint, + createdAt: generatedAt, + }); + const codeChangeSourcePatches = createCodeChangeSourcePatchSet({ + plans: codeChangePlans.plans, + graphFingerprint: graph.fingerprint, + generatedAt, + }); + const codeChangePlanningAudit: PipelineStageAudit = { + runtimeVersion: T2C_VERSION, + configuration: openRouterAuditConfiguration(config, null), + status: 'succeeded', + requestedMode: 'deterministic', + effectiveMode: 'deterministic', + degraded: false, + recordCount: codeChangePlans.plans.length, + warningCount: 0, + model: null, + durationMs: 0, + reason: null, + responses: [], + }; + return { + codeChangePlans, + codeChangeReview, + codeChangeSourcePatches, + codeChangePlanningAudit, + }; +} + +export function collectTargetHints(records: IntentRecord[]): { paths: string[]; symbols: string[]; tickets: string[]; versions: string[] } { + const values = (key: K): string[] => [ + ...new Set(records.flatMap((record) => record.statement.target[key])), + ].slice(0, 200); + return { + paths: values('paths'), + symbols: values('symbols'), + tickets: values('tickets'), + versions: values('versions'), + }; +} + +export function appendLlmNotConfigured(report: DiagnosticReport): void { + const diagnostic: Diagnostic = { + id: createIntentId({ code: 'LLM_NOT_CONFIGURED', graph: report.graphFingerprint }, 'DIAG'), + code: 'LLM_NOT_CONFIGURED', + severity: 'warning', + title: 'OpenRouter nie jest skonfigurowany', + detail: 'Etap dokumentacja -> Intent DSL został pominięty, ponieważ brakuje OPENROUTER_API_KEY.', + recordIds: [], + suggestedAction: 'Ustawić OPENROUTER_API_KEY w .env i ponownie uruchomić pipeline.', + }; + report.diagnostics.unshift(diagnostic); + report.counts.warning += 1; +} From 512104ba4c0757f5dc56ae10eca929b0be7208e3 Mon Sep 17 00:00:00 2001 From: Tom Softreck Date: Tue, 4 Aug 2026 19:21:58 +0200 Subject: [PATCH 5/6] fix: normalize missing-directory boolean in communication analysis --- src/pipeline/run-helpers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pipeline/run-helpers.ts b/src/pipeline/run-helpers.ts index 9a9b794..623e48d 100644 --- a/src/pipeline/run-helpers.ts +++ b/src/pipeline/run-helpers.ts @@ -41,7 +41,7 @@ export async function collectCommunicationAnalysis( }, config, options.communicationMode ?? config.communicationMode); const foundMissingDirectory = communication.records.length === 0 && communication.warnings.length === 1 - && communication.warnings[0]?.startsWith('Communication directory not found:'); + && !!communication.warnings[0]?.startsWith('Communication directory not found:'); if (!foundMissingDirectory) warnings.push(...communication.warnings); bySource.communication = communication.records; communicationSyntheses = communication.participants; From 5109338e69b36e4a2b84f813209df61a5923695b Mon Sep 17 00:00:00 2001 From: Tom Softreck Date: Tue, 4 Aug 2026 19:25:24 +0200 Subject: [PATCH 6/6] fix: make run helpers self-contained for current pipeline --- src/pipeline/run-helpers.ts | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/src/pipeline/run-helpers.ts b/src/pipeline/run-helpers.ts index 623e48d..c74534e 100644 --- a/src/pipeline/run-helpers.ts +++ b/src/pipeline/run-helpers.ts @@ -10,8 +10,31 @@ import { readText } from '../core/io.js'; import { T2C_VERSION } from '../version.js'; import { openRouterAuditConfiguration } from '../llm/audit.js'; import type { T2CConfig } from '../config/env.js'; -import type { PipelineContext } from './run-types.js'; -import { skippedAudit } from './run-failed.js'; +import type { PipelineFailureStage } from '../core/types.js'; + +type PipelineContext = { + root: string; + warnings: string[]; + bySource: Record; + activeStage: PipelineFailureStage; +}; + +function skippedAudit(requestedMode: PipelineStageAudit['requestedMode'], message: string): PipelineStageAudit { + return { + runtimeVersion: T2C_VERSION, + configuration: {}, + status: 'skipped', + requestedMode, + effectiveMode: 'none', + degraded: false, + recordCount: 0, + warningCount: 0, + model: null, + durationMs: 0, + reason: { code: 'STAGE_SKIPPED', message }, + responses: [], + }; +} export async function collectCommunicationAnalysis( context: PipelineContext,