1089 lines
37 KiB
TypeScript
1089 lines
37 KiB
TypeScript
import { createHash, randomUUID } from 'node:crypto';
|
|
import type { ExtensionAPI, ExtensionContext } from '@earendil-works/pi-coding-agent';
|
|
|
|
const STATE_ENTRY_TYPE = 'mosaic-goal-state';
|
|
const CONTEXT_MESSAGE_TYPE = 'mosaic-goal-context';
|
|
const CONTINUATION_MESSAGE_TYPE = 'mosaic-goal-continuation';
|
|
const GOAL_REPORT_TOOL = 'mosaic_goal_report';
|
|
const STATUS_KEY = 'mosaic-goal';
|
|
const STATE_VERSION = 1 as const;
|
|
const MAX_STATEMENT_LENGTH = 8_000;
|
|
const MAX_SUMMARY_LENGTH = 2_000;
|
|
const MAX_EVIDENCE_ITEMS = 20;
|
|
const MAX_EVIDENCE_LENGTH = 1_000;
|
|
const MAX_NEXT_STEP_LENGTH = 2_000;
|
|
const DEFAULT_MAX_TURNS = 40;
|
|
const DEFAULT_MAX_NO_PROGRESS = 6;
|
|
const REQUIRED_VERIFICATION_PASSES = 2;
|
|
const DEFERRED_CONTINUATION_MS = 10;
|
|
const REDACTED_SECRET = '[REDACTED-SECRET]';
|
|
const PRIVATE_KEY_PATTERN =
|
|
/-----BEGIN(?: [A-Z0-9]+)* PRIVATE KEY(?: BLOCK)?-----[\s\S]*?(?:-----END(?: [A-Z0-9]+)* PRIVATE KEY(?: BLOCK)?-----|$)/g;
|
|
const CREDENTIAL_URL_PATTERN = /\b([A-Za-z][A-Za-z0-9+.-]*:\/\/)[^\s/:]+:[^\s/@]+@/g;
|
|
const AUTHORIZATION_PATTERN =
|
|
/(\b(?:authorization|proxy-authorization)\s*[:=]\s*)(bearer|basic)\s+[^\s,;]+/gi;
|
|
const STANDALONE_AUTH_PATTERN = /\b(bearer|basic)\s+[A-Za-z0-9._~+/=-]{16,}/gi;
|
|
const JWT_PATTERN = /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/g;
|
|
const KNOWN_SECRET_PATTERN =
|
|
/\b(?:AKIA[0-9A-Z]{16}|ASIA[0-9A-Z]{16}|AIza[0-9A-Za-z_-]{35}|gh[pousr]_[A-Za-z0-9]{20,}|github_pat_[A-Za-z0-9_]{20,}|glpat-[A-Za-z0-9_-]{20,}|npm_[A-Za-z0-9]{20,}|sk-(?:ant-(?:api\d{2}-)?|proj-)?[A-Za-z0-9_-]{20,}|(?:sk|rk)_(?:live|test)_[A-Za-z0-9]{16,}|xox[baprs]-[A-Za-z0-9-]{10,}|hf_[A-Za-z0-9]{20,})\b/g;
|
|
const SENSITIVE_ASSIGNMENT_PATTERN =
|
|
/((?:["']?(?:[a-z0-9]+[_-])*(?:api[_-]?key|access[_-]?token|auth(?:orization)?[_-]?token|client[_-]?secret|password|passwd|secret(?:[_-]?access[_-]?key)?|private[_-]?key|database[_-]?url|token|cookie|set[_-]?cookie)["']?)\s*)((?:=|:)\s*)("[^"\r\n]*"|'[^'\r\n]*'|[^\s,;]+)/gi;
|
|
|
|
const GOAL_PHASES = [
|
|
'active',
|
|
'verifying',
|
|
'paused',
|
|
'blocked',
|
|
'achieved',
|
|
'cancelled',
|
|
'exhausted',
|
|
] as const;
|
|
const REPORT_STATUSES = ['continue', 'achieved', 'blocked'] as const;
|
|
const CHECK_SOURCES = ['command', 'turn', 'compact', 'restore', 'report'] as const;
|
|
|
|
type GoalPhase = (typeof GOAL_PHASES)[number];
|
|
type GoalReportStatus = (typeof REPORT_STATUSES)[number];
|
|
type GoalCheckSource = (typeof CHECK_SOURCES)[number];
|
|
|
|
interface GoalReport {
|
|
status: GoalReportStatus;
|
|
summary: string;
|
|
evidence: string[];
|
|
nextStep?: string;
|
|
fingerprint: string;
|
|
reportedAt: string;
|
|
}
|
|
|
|
interface GoalState {
|
|
version: typeof STATE_VERSION;
|
|
goalId: string;
|
|
statement: string;
|
|
phase: GoalPhase;
|
|
startedAt: string;
|
|
updatedAt: string;
|
|
turnCount: number;
|
|
reportCount: number;
|
|
verificationPasses: number;
|
|
requiredVerificationPasses: number;
|
|
noProgressReports: number;
|
|
maxTurns: number;
|
|
maxNoProgressReports: number;
|
|
compactionCount: number;
|
|
lastCheckSource: GoalCheckSource;
|
|
lastCheckAt: string;
|
|
lastCheckOutcome: string;
|
|
lastProgressFingerprint?: string;
|
|
lastReport?: GoalReport;
|
|
stopReason?: string;
|
|
}
|
|
|
|
interface GoalReportInput {
|
|
status: GoalReportStatus;
|
|
summary: string;
|
|
evidence: string[];
|
|
nextStep?: string;
|
|
}
|
|
|
|
interface GoalLimits {
|
|
maxTurns: number;
|
|
maxNoProgressReports: number;
|
|
}
|
|
|
|
interface GoalCommand {
|
|
action: 'set' | 'status' | 'pause' | 'resume' | 'cancel' | 'help';
|
|
value: string;
|
|
}
|
|
|
|
const GoalReportParameters = {
|
|
type: 'object',
|
|
properties: {
|
|
status: {
|
|
type: 'string',
|
|
enum: REPORT_STATUSES,
|
|
description:
|
|
'continue while work remains, achieved only with completion evidence, or blocked',
|
|
},
|
|
summary: {
|
|
type: 'string',
|
|
minLength: 1,
|
|
maxLength: MAX_SUMMARY_LENGTH,
|
|
description: 'Concise progress or completion assessment',
|
|
},
|
|
evidence: {
|
|
type: 'array',
|
|
items: { type: 'string', minLength: 1, maxLength: MAX_EVIDENCE_LENGTH },
|
|
maxItems: MAX_EVIDENCE_ITEMS,
|
|
description: 'Concrete observations, commands, tests, or artifacts supporting the status',
|
|
},
|
|
nextStep: {
|
|
type: 'string',
|
|
minLength: 1,
|
|
maxLength: MAX_NEXT_STEP_LENGTH,
|
|
description: 'The next concrete action when work remains',
|
|
},
|
|
},
|
|
required: ['status', 'summary', 'evidence'],
|
|
additionalProperties: false,
|
|
} as const;
|
|
|
|
function nowIso(): string {
|
|
return new Date().toISOString();
|
|
}
|
|
|
|
function shouldRedactSensitiveAssignment(separator: string, rawValue: string): boolean {
|
|
if (separator.trim() === '=') return true;
|
|
const quoted =
|
|
(rawValue.startsWith('"') && rawValue.endsWith('"')) ||
|
|
(rawValue.startsWith("'") && rawValue.endsWith("'"));
|
|
if (quoted || rawValue.includes('://')) return true;
|
|
if (/^[a-f0-9]{20,}$/i.test(rawValue)) return true;
|
|
return (
|
|
rawValue.length >= 20 &&
|
|
/[A-Za-z]/.test(rawValue) &&
|
|
/\d/.test(rawValue) &&
|
|
/[-_./+=]/.test(rawValue)
|
|
);
|
|
}
|
|
|
|
function redactSensitiveText(value: string): string {
|
|
let redacted = value.replace(PRIVATE_KEY_PATTERN, REDACTED_SECRET);
|
|
redacted = redacted.replace(
|
|
CREDENTIAL_URL_PATTERN,
|
|
(_match: string, prefix: string): string => `${prefix}${REDACTED_SECRET}@`,
|
|
);
|
|
redacted = redacted.replace(
|
|
AUTHORIZATION_PATTERN,
|
|
(_match: string, prefix: string, scheme: string): string =>
|
|
`${prefix}${scheme} ${REDACTED_SECRET}`,
|
|
);
|
|
redacted = redacted.replace(
|
|
STANDALONE_AUTH_PATTERN,
|
|
(_match: string, scheme: string): string => `${scheme} ${REDACTED_SECRET}`,
|
|
);
|
|
redacted = redacted.replace(JWT_PATTERN, REDACTED_SECRET);
|
|
redacted = redacted.replace(KNOWN_SECRET_PATTERN, REDACTED_SECRET);
|
|
return redacted.replace(
|
|
SENSITIVE_ASSIGNMENT_PATTERN,
|
|
(_match: string, prefix: string, separator: string, rawValue: string): string =>
|
|
shouldRedactSensitiveAssignment(separator, rawValue)
|
|
? `${prefix}${separator}${REDACTED_SECRET}`
|
|
: _match,
|
|
);
|
|
}
|
|
|
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
}
|
|
|
|
function isOneOf<const TValue extends string>(
|
|
value: unknown,
|
|
allowed: readonly TValue[],
|
|
): value is TValue {
|
|
return (
|
|
typeof value === 'string' && allowed.some((candidate: TValue): boolean => candidate === value)
|
|
);
|
|
}
|
|
|
|
function parseBoundedInteger(
|
|
value: string | undefined,
|
|
fallback: number,
|
|
minimum: number,
|
|
maximum: number,
|
|
): number {
|
|
if (value === undefined || !/^\d+$/.test(value.trim())) return fallback;
|
|
const parsed = Number.parseInt(value, 10);
|
|
if (!Number.isSafeInteger(parsed) || parsed < minimum || parsed > maximum) return fallback;
|
|
return parsed;
|
|
}
|
|
|
|
function readGoalLimits(env: NodeJS.ProcessEnv): GoalLimits {
|
|
return {
|
|
maxTurns: parseBoundedInteger(env['MOSAIC_GOAL_MAX_TURNS'], DEFAULT_MAX_TURNS, 1, 500),
|
|
maxNoProgressReports: parseBoundedInteger(
|
|
env['MOSAIC_GOAL_MAX_NO_PROGRESS'],
|
|
DEFAULT_MAX_NO_PROGRESS,
|
|
1,
|
|
100,
|
|
),
|
|
};
|
|
}
|
|
|
|
function parseStringArray(value: unknown): string[] | undefined {
|
|
if (!Array.isArray(value) || value.length > MAX_EVIDENCE_ITEMS) return undefined;
|
|
const result: string[] = [];
|
|
for (const item of value) {
|
|
if (typeof item !== 'string') return undefined;
|
|
const normalized = item.trim();
|
|
if (normalized.length === 0 || normalized.length > MAX_EVIDENCE_LENGTH) return undefined;
|
|
result.push(normalized);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function parseGoalReport(value: unknown): GoalReport | undefined {
|
|
if (!isRecord(value)) return undefined;
|
|
if (!isOneOf(value['status'], REPORT_STATUSES)) return undefined;
|
|
if (
|
|
typeof value['summary'] !== 'string' ||
|
|
value['summary'].length === 0 ||
|
|
value['summary'].length > MAX_SUMMARY_LENGTH
|
|
) {
|
|
return undefined;
|
|
}
|
|
const evidence = parseStringArray(value['evidence']);
|
|
if (evidence === undefined) return undefined;
|
|
if (typeof value['fingerprint'] !== 'string' || !/^[a-f0-9]{64}$/.test(value['fingerprint'])) {
|
|
return undefined;
|
|
}
|
|
if (
|
|
typeof value['reportedAt'] !== 'string' ||
|
|
value['reportedAt'].length === 0 ||
|
|
value['reportedAt'].length > 64
|
|
) {
|
|
return undefined;
|
|
}
|
|
const nextStepValue = value['nextStep'];
|
|
let nextStep: string | undefined;
|
|
if (nextStepValue !== undefined) {
|
|
if (
|
|
typeof nextStepValue !== 'string' ||
|
|
nextStepValue.length === 0 ||
|
|
nextStepValue.length > MAX_NEXT_STEP_LENGTH
|
|
) {
|
|
return undefined;
|
|
}
|
|
nextStep = nextStepValue;
|
|
}
|
|
return {
|
|
status: value['status'],
|
|
summary: value['summary'],
|
|
evidence,
|
|
...(nextStep === undefined ? {} : { nextStep }),
|
|
fingerprint: value['fingerprint'],
|
|
reportedAt: value['reportedAt'],
|
|
};
|
|
}
|
|
|
|
function readIntegerField(
|
|
value: Record<string, unknown>,
|
|
field: string,
|
|
minimum: number,
|
|
maximum: number = Number.MAX_SAFE_INTEGER,
|
|
): number | undefined {
|
|
const candidate = value[field];
|
|
if (
|
|
!Number.isSafeInteger(candidate) ||
|
|
typeof candidate !== 'number' ||
|
|
candidate < minimum ||
|
|
candidate > maximum
|
|
) {
|
|
return undefined;
|
|
}
|
|
return candidate;
|
|
}
|
|
|
|
function parseGoalState(value: unknown): GoalState | undefined {
|
|
if (!isRecord(value) || value['version'] !== STATE_VERSION) return undefined;
|
|
if (
|
|
typeof value['goalId'] !== 'string' ||
|
|
value['goalId'].length === 0 ||
|
|
value['goalId'].length > 128
|
|
) {
|
|
return undefined;
|
|
}
|
|
if (
|
|
typeof value['statement'] !== 'string' ||
|
|
value['statement'].length === 0 ||
|
|
value['statement'].length > MAX_STATEMENT_LENGTH
|
|
) {
|
|
return undefined;
|
|
}
|
|
if (!isOneOf(value['phase'], GOAL_PHASES)) return undefined;
|
|
if (!isOneOf(value['lastCheckSource'], CHECK_SOURCES)) return undefined;
|
|
if (
|
|
typeof value['startedAt'] !== 'string' ||
|
|
value['startedAt'].length > 64 ||
|
|
typeof value['updatedAt'] !== 'string' ||
|
|
value['updatedAt'].length > 64
|
|
) {
|
|
return undefined;
|
|
}
|
|
if (
|
|
typeof value['lastCheckAt'] !== 'string' ||
|
|
value['lastCheckAt'].length > 64 ||
|
|
typeof value['lastCheckOutcome'] !== 'string' ||
|
|
value['lastCheckOutcome'].length > 256
|
|
) {
|
|
return undefined;
|
|
}
|
|
|
|
const turnCount = readIntegerField(value, 'turnCount', 0, 500);
|
|
const reportCount = readIntegerField(value, 'reportCount', 0, 100_000);
|
|
const verificationPasses = readIntegerField(
|
|
value,
|
|
'verificationPasses',
|
|
0,
|
|
REQUIRED_VERIFICATION_PASSES,
|
|
);
|
|
const requiredVerificationPasses = readIntegerField(
|
|
value,
|
|
'requiredVerificationPasses',
|
|
REQUIRED_VERIFICATION_PASSES,
|
|
REQUIRED_VERIFICATION_PASSES,
|
|
);
|
|
const noProgressReports = readIntegerField(value, 'noProgressReports', 0, 100);
|
|
const maxTurns = readIntegerField(value, 'maxTurns', 1, 500);
|
|
const maxNoProgressReports = readIntegerField(value, 'maxNoProgressReports', 1, 100);
|
|
const compactionCount = readIntegerField(value, 'compactionCount', 0, 100_000);
|
|
if (
|
|
turnCount === undefined ||
|
|
reportCount === undefined ||
|
|
verificationPasses === undefined ||
|
|
requiredVerificationPasses === undefined ||
|
|
noProgressReports === undefined ||
|
|
maxTurns === undefined ||
|
|
maxNoProgressReports === undefined ||
|
|
compactionCount === undefined
|
|
) {
|
|
return undefined;
|
|
}
|
|
|
|
const lastReportValue = value['lastReport'];
|
|
const lastReport = lastReportValue === undefined ? undefined : parseGoalReport(lastReportValue);
|
|
if (lastReportValue !== undefined && lastReport === undefined) return undefined;
|
|
const lastProgressFingerprintValue = value['lastProgressFingerprint'];
|
|
let lastProgressFingerprint: string | undefined;
|
|
if (lastProgressFingerprintValue !== undefined) {
|
|
if (
|
|
typeof lastProgressFingerprintValue !== 'string' ||
|
|
!/^[a-f0-9]{64}$/.test(lastProgressFingerprintValue)
|
|
) {
|
|
return undefined;
|
|
}
|
|
lastProgressFingerprint = lastProgressFingerprintValue;
|
|
}
|
|
const stopReasonValue = value['stopReason'];
|
|
let stopReason: string | undefined;
|
|
if (stopReasonValue !== undefined) {
|
|
if (typeof stopReasonValue !== 'string' || stopReasonValue.length > MAX_SUMMARY_LENGTH) {
|
|
return undefined;
|
|
}
|
|
stopReason = stopReasonValue;
|
|
}
|
|
|
|
return {
|
|
version: STATE_VERSION,
|
|
goalId: value['goalId'],
|
|
statement: value['statement'],
|
|
phase: value['phase'],
|
|
startedAt: value['startedAt'],
|
|
updatedAt: value['updatedAt'],
|
|
turnCount,
|
|
reportCount,
|
|
verificationPasses,
|
|
requiredVerificationPasses,
|
|
noProgressReports,
|
|
maxTurns,
|
|
maxNoProgressReports,
|
|
compactionCount,
|
|
lastCheckSource: value['lastCheckSource'],
|
|
lastCheckAt: value['lastCheckAt'],
|
|
lastCheckOutcome: value['lastCheckOutcome'],
|
|
...(lastProgressFingerprint === undefined ? {} : { lastProgressFingerprint }),
|
|
...(lastReport === undefined ? {} : { lastReport }),
|
|
...(stopReason === undefined ? {} : { stopReason }),
|
|
};
|
|
}
|
|
|
|
function copyReport(report: GoalReport): GoalReport {
|
|
return {
|
|
...report,
|
|
evidence: [...report.evidence],
|
|
};
|
|
}
|
|
|
|
function copyState(state: GoalState): GoalState {
|
|
return {
|
|
...state,
|
|
...(state.lastReport === undefined ? {} : { lastReport: copyReport(state.lastReport) }),
|
|
};
|
|
}
|
|
|
|
function parseGoalReportInput(value: unknown): GoalReportInput {
|
|
if (!isRecord(value) || !isOneOf(value['status'], REPORT_STATUSES)) {
|
|
throw new Error('Goal report status must be continue, achieved, or blocked.');
|
|
}
|
|
if (typeof value['summary'] !== 'string') throw new Error('Goal report summary is required.');
|
|
const summary = value['summary'].trim();
|
|
if (summary.length === 0 || summary.length > MAX_SUMMARY_LENGTH) {
|
|
throw new Error(`Goal report summary must be 1-${MAX_SUMMARY_LENGTH} characters.`);
|
|
}
|
|
const evidence = parseStringArray(value['evidence']);
|
|
if (evidence === undefined) throw new Error('Goal report evidence is invalid.');
|
|
if (value['status'] === 'achieved' && evidence.length === 0) {
|
|
throw new Error('An achieved goal report requires concrete evidence.');
|
|
}
|
|
const rawNextStep = value['nextStep'];
|
|
if (rawNextStep !== undefined && typeof rawNextStep !== 'string') {
|
|
throw new Error('Goal report nextStep must be text.');
|
|
}
|
|
const nextStep = typeof rawNextStep === 'string' ? rawNextStep.trim() : undefined;
|
|
if (nextStep !== undefined && (nextStep.length === 0 || nextStep.length > MAX_NEXT_STEP_LENGTH)) {
|
|
throw new Error(`Goal report nextStep must be 1-${MAX_NEXT_STEP_LENGTH} characters.`);
|
|
}
|
|
return redactGoalReportInput({
|
|
status: value['status'],
|
|
summary,
|
|
evidence,
|
|
...(nextStep === undefined ? {} : { nextStep }),
|
|
});
|
|
}
|
|
|
|
function reportFingerprint(report: GoalReportInput): string {
|
|
const normalized = JSON.stringify({
|
|
summary: report.summary.trim().toLowerCase(),
|
|
evidence: report.evidence.map((item: string): string => item.trim().toLowerCase()),
|
|
nextStep: report.nextStep?.trim().toLowerCase() ?? '',
|
|
});
|
|
return createHash('sha256').update(normalized).digest('hex');
|
|
}
|
|
|
|
function redactGoalReportInput(report: GoalReportInput): GoalReportInput {
|
|
return {
|
|
status: report.status,
|
|
summary: redactSensitiveText(report.summary),
|
|
evidence: report.evidence.map(redactSensitiveText),
|
|
...(report.nextStep === undefined ? {} : { nextStep: redactSensitiveText(report.nextStep) }),
|
|
};
|
|
}
|
|
|
|
function redactGoalReport(report: GoalReport): GoalReport {
|
|
const redactedInput = redactGoalReportInput(report);
|
|
return {
|
|
...redactedInput,
|
|
fingerprint: reportFingerprint(redactedInput),
|
|
reportedAt: report.reportedAt,
|
|
};
|
|
}
|
|
|
|
function redactGoalState(state: GoalState): GoalState {
|
|
const lastReport =
|
|
state.lastReport === undefined ? undefined : redactGoalReport(state.lastReport);
|
|
const lastProgressFingerprint =
|
|
lastReport !== undefined && state.lastProgressFingerprint === state.lastReport?.fingerprint
|
|
? lastReport.fingerprint
|
|
: state.lastProgressFingerprint;
|
|
return {
|
|
...state,
|
|
statement: redactSensitiveText(state.statement),
|
|
...(lastReport === undefined ? {} : { lastReport }),
|
|
...(lastProgressFingerprint === undefined ? {} : { lastProgressFingerprint }),
|
|
...(state.stopReason === undefined
|
|
? {}
|
|
: { stopReason: redactSensitiveText(state.stopReason) }),
|
|
};
|
|
}
|
|
|
|
function goalStateContainsSensitiveText(state: GoalState): boolean {
|
|
const textValues = [state.statement];
|
|
if (state.stopReason !== undefined) textValues.push(state.stopReason);
|
|
if (state.lastReport !== undefined) {
|
|
textValues.push(state.lastReport.summary, ...state.lastReport.evidence);
|
|
if (state.lastReport.nextStep !== undefined) textValues.push(state.lastReport.nextStep);
|
|
}
|
|
return textValues.some((value: string): boolean => redactSensitiveText(value) !== value);
|
|
}
|
|
|
|
function isContinuingPhase(phase: GoalPhase): boolean {
|
|
return phase === 'active' || phase === 'verifying';
|
|
}
|
|
|
|
function canReplaceGoal(state: GoalState | undefined): boolean {
|
|
return state === undefined || state.phase === 'achieved' || state.phase === 'cancelled';
|
|
}
|
|
|
|
function parseGoalCommand(args: string): GoalCommand {
|
|
const trimmed = args.trim();
|
|
if (trimmed.length === 0) return { action: 'help', value: '' };
|
|
const separator = trimmed.indexOf(' ');
|
|
const first = (separator === -1 ? trimmed : trimmed.slice(0, separator)).toLowerCase();
|
|
const value = separator === -1 ? '' : trimmed.slice(separator + 1).trim();
|
|
if (first === 'set') return { action: 'set', value };
|
|
if (first === 'status') return { action: 'status', value };
|
|
if (first === 'pause') return { action: 'pause', value };
|
|
if (first === 'resume') return { action: 'resume', value };
|
|
if (first === 'cancel' || first === 'clear') return { action: 'cancel', value };
|
|
if (first === 'help') return { action: 'help', value };
|
|
return { action: 'set', value: trimmed };
|
|
}
|
|
|
|
function formatStatus(state: GoalState | undefined): string {
|
|
if (state === undefined) return 'No Mosaic goal is set. Use /goal set <statement>.';
|
|
const lines = [
|
|
`Goal ${state.goalId}`,
|
|
`Phase: ${state.phase}`,
|
|
`Turns: ${state.turnCount}/${state.maxTurns}`,
|
|
`Verification: ${state.verificationPasses}/${state.requiredVerificationPasses}`,
|
|
`No-progress reports: ${state.noProgressReports}/${state.maxNoProgressReports}`,
|
|
`Compactions checked: ${state.compactionCount}`,
|
|
`Goal: ${state.statement}`,
|
|
];
|
|
if (state.lastReport !== undefined) {
|
|
lines.push(`Latest report: ${state.lastReport.status} — ${state.lastReport.summary}`);
|
|
if (state.lastReport.evidence.length > 0) {
|
|
lines.push(
|
|
'Evidence:',
|
|
...state.lastReport.evidence.map((item: string): string => `- ${item}`),
|
|
);
|
|
}
|
|
if (state.lastReport.nextStep !== undefined) {
|
|
lines.push(`Next step: ${state.lastReport.nextStep}`);
|
|
}
|
|
}
|
|
if (state.stopReason !== undefined) lines.push(`Stopped: ${state.stopReason}`);
|
|
return lines.join('\n');
|
|
}
|
|
|
|
function buildGoalContract(state: GoalState): string {
|
|
const latest = state.lastReport;
|
|
const verificationInstruction =
|
|
state.phase === 'verifying'
|
|
? 'This is a verification pass. Re-inspect the actual result and rerun relevant checks; do not rely only on the prior claim.'
|
|
: 'Continue making concrete progress toward the goal.';
|
|
const lines = [
|
|
'[MOSAIC GOAL LOOP v1]',
|
|
`Goal ID: ${state.goalId}`,
|
|
`Goal: ${state.statement}`,
|
|
`Phase: ${state.phase}`,
|
|
`Budget: turn ${state.turnCount}/${state.maxTurns}; repeated no-progress reports ${state.noProgressReports}/${state.maxNoProgressReports}.`,
|
|
verificationInstruction,
|
|
'',
|
|
'Completion protocol:',
|
|
`- Before ending the work cycle, call ${GOAL_REPORT_TOOL} as the only tool call in the final assistant response.`,
|
|
'- Use status=continue whenever any requirement remains and provide the next concrete step.',
|
|
'- Use status=achieved only when concrete evidence covers the entire stated goal.',
|
|
'- Use status=blocked only for a genuine blocker that prevents meaningful progress.',
|
|
`- Achievement requires ${state.requiredVerificationPasses} consecutive evidence-bearing reports; the first claim starts a separate verification pass.`,
|
|
'- Never include secrets, tokens, credentials, private keys, or raw sensitive output in a report.',
|
|
'- Do not ask routine permission to continue. The operator can pause or cancel with /goal.',
|
|
];
|
|
if (latest !== undefined) {
|
|
lines.push('', `Previous report: ${latest.status} — ${latest.summary}`);
|
|
if (latest.nextStep !== undefined) lines.push(`Previous next step: ${latest.nextStep}`);
|
|
}
|
|
return lines.join('\n');
|
|
}
|
|
|
|
function continuationText(state: GoalState, reason: string): string {
|
|
if (state.phase === 'verifying') {
|
|
return `Goal ${state.goalId} requires a verification pass after ${reason}. Recheck the complete goal and report fresh evidence with ${GOAL_REPORT_TOOL}.`;
|
|
}
|
|
return `Goal remains active after ${reason}. Continue from the latest evidence and finish by calling ${GOAL_REPORT_TOOL}.`;
|
|
}
|
|
|
|
function toolResultIncludesGoalReport(toolResults: unknown): boolean {
|
|
if (!Array.isArray(toolResults)) return false;
|
|
return toolResults.some(
|
|
(result: unknown): boolean => isRecord(result) && result['toolName'] === GOAL_REPORT_TOOL,
|
|
);
|
|
}
|
|
|
|
function toolResultCount(toolResults: unknown): number {
|
|
return Array.isArray(toolResults) ? toolResults.length : 0;
|
|
}
|
|
|
|
export default function registerGoalExtension(pi: ExtensionAPI): void {
|
|
const limits = readGoalLimits(process.env);
|
|
let state: GoalState | undefined;
|
|
let continuationQueued = false;
|
|
let deferredTimer: ReturnType<typeof setTimeout> | undefined;
|
|
let lifecycleGeneration = 0;
|
|
let reportRollbackState: GoalState | undefined;
|
|
|
|
function updateStatus(ctx: ExtensionContext): void {
|
|
if (state === undefined || state.phase === 'cancelled') {
|
|
ctx.ui.setStatus(STATUS_KEY, undefined);
|
|
return;
|
|
}
|
|
ctx.ui.setStatus(STATUS_KEY, `🎯 ${state.phase} ${state.turnCount}/${state.maxTurns}`);
|
|
}
|
|
|
|
function persist(nextState: GoalState, ctx: ExtensionContext): void {
|
|
const redactedState = redactGoalState(nextState);
|
|
state = copyState(redactedState);
|
|
pi.appendEntry(STATE_ENTRY_TYPE, copyState(redactedState));
|
|
updateStatus(ctx);
|
|
}
|
|
|
|
function clearDeferredTimer(): void {
|
|
if (deferredTimer !== undefined) clearTimeout(deferredTimer);
|
|
deferredTimer = undefined;
|
|
}
|
|
|
|
function queueContinuation(
|
|
ctx: ExtensionContext,
|
|
reason: string,
|
|
trackDuplicate: boolean = true,
|
|
): void {
|
|
if (state === undefined || !isContinuingPhase(state.phase)) return;
|
|
if (ctx.hasPendingMessages()) return;
|
|
if (trackDuplicate && continuationQueued) return;
|
|
if (trackDuplicate) continuationQueued = true;
|
|
const options = ctx.isIdle()
|
|
? { triggerTurn: true as const }
|
|
: { triggerTurn: true as const, deliverAs: 'followUp' as const };
|
|
pi.sendMessage(
|
|
{
|
|
customType: CONTINUATION_MESSAGE_TYPE,
|
|
content: continuationText(state, reason),
|
|
display: true,
|
|
},
|
|
options,
|
|
);
|
|
}
|
|
|
|
function scheduleIdleContinuation(ctx: ExtensionContext, reason: string): void {
|
|
clearDeferredTimer();
|
|
const scheduledGeneration = lifecycleGeneration;
|
|
deferredTimer = setTimeout((): void => {
|
|
deferredTimer = undefined;
|
|
if (scheduledGeneration !== lifecycleGeneration || !ctx.isIdle()) return;
|
|
queueContinuation(ctx, reason);
|
|
}, DEFERRED_CONTINUATION_MS);
|
|
}
|
|
|
|
function restoreState(ctx: ExtensionContext): void {
|
|
state = undefined;
|
|
let rejectedSensitiveState = false;
|
|
for (const entry of ctx.sessionManager.getBranch()) {
|
|
if (entry.type !== 'custom' || entry.customType !== STATE_ENTRY_TYPE) continue;
|
|
const restoredState = parseGoalState(entry.data);
|
|
if (restoredState === undefined) {
|
|
state = undefined;
|
|
} else if (goalStateContainsSensitiveText(restoredState)) {
|
|
state = undefined;
|
|
rejectedSensitiveState = true;
|
|
} else if (rejectedSensitiveState) {
|
|
state = undefined;
|
|
} else {
|
|
state = redactGoalState(restoredState);
|
|
}
|
|
}
|
|
continuationQueued = false;
|
|
updateStatus(ctx);
|
|
if (rejectedSensitiveState) {
|
|
ctx.ui.notify(
|
|
'Mosaic goal state was not restored because persisted text matched a credential pattern. Remove the affected Pi session if it may contain a real secret, then set a new goal.',
|
|
'warning',
|
|
);
|
|
}
|
|
}
|
|
|
|
function createGoal(statement: string, ctx: ExtensionContext): void {
|
|
if (!canReplaceGoal(state)) {
|
|
ctx.ui.notify(
|
|
'A Mosaic goal already exists. Use /goal cancel before replacing it.',
|
|
'warning',
|
|
);
|
|
return;
|
|
}
|
|
if (statement.length === 0 || statement.length > MAX_STATEMENT_LENGTH) {
|
|
ctx.ui.notify(
|
|
`Usage: /goal set <statement> (${MAX_STATEMENT_LENGTH.toLocaleString()} characters maximum).`,
|
|
'warning',
|
|
);
|
|
return;
|
|
}
|
|
const timestamp = nowIso();
|
|
const redactedStatement = redactSensitiveText(statement);
|
|
persist(
|
|
{
|
|
version: STATE_VERSION,
|
|
goalId: randomUUID(),
|
|
statement: redactedStatement,
|
|
phase: 'active',
|
|
startedAt: timestamp,
|
|
updatedAt: timestamp,
|
|
turnCount: 0,
|
|
reportCount: 0,
|
|
verificationPasses: 0,
|
|
requiredVerificationPasses: REQUIRED_VERIFICATION_PASSES,
|
|
noProgressReports: 0,
|
|
maxTurns: limits.maxTurns,
|
|
maxNoProgressReports: limits.maxNoProgressReports,
|
|
compactionCount: 0,
|
|
lastCheckSource: 'command',
|
|
lastCheckAt: timestamp,
|
|
lastCheckOutcome: 'set',
|
|
},
|
|
ctx,
|
|
);
|
|
ctx.ui.notify(`Mosaic goal started: ${redactedStatement}`, 'info');
|
|
if (ctx.isIdle()) queueContinuation(ctx, 'goal start', false);
|
|
}
|
|
|
|
pi.registerCommand('goal', {
|
|
description: 'Set or control a persistent Mosaic goal loop',
|
|
handler: async (args, ctx): Promise<void> => {
|
|
const command = parseGoalCommand(args);
|
|
if (command.action === 'help') {
|
|
ctx.ui.notify(
|
|
[
|
|
'Mosaic goal commands:',
|
|
'/goal set <statement> (or /goal <statement>)',
|
|
'/goal status',
|
|
'/goal pause [reason]',
|
|
'/goal resume',
|
|
'/goal cancel',
|
|
].join('\n'),
|
|
'info',
|
|
);
|
|
return;
|
|
}
|
|
if (command.action === 'status') {
|
|
ctx.ui.notify(formatStatus(state), 'info');
|
|
return;
|
|
}
|
|
if (command.action === 'set') {
|
|
createGoal(command.value, ctx);
|
|
return;
|
|
}
|
|
if (state === undefined) {
|
|
ctx.ui.notify('No Mosaic goal is set.', 'warning');
|
|
return;
|
|
}
|
|
const timestamp = nowIso();
|
|
if (command.action === 'pause') {
|
|
if (!isContinuingPhase(state.phase)) {
|
|
ctx.ui.notify(`Goal cannot be paused from phase ${state.phase}.`, 'warning');
|
|
return;
|
|
}
|
|
persist(
|
|
{
|
|
...state,
|
|
phase: 'paused',
|
|
updatedAt: timestamp,
|
|
lastCheckSource: 'command',
|
|
lastCheckAt: timestamp,
|
|
lastCheckOutcome: 'paused',
|
|
stopReason: command.value || 'Paused by operator.',
|
|
},
|
|
ctx,
|
|
);
|
|
clearDeferredTimer();
|
|
continuationQueued = false;
|
|
if (!ctx.isIdle()) ctx.abort();
|
|
ctx.ui.notify('Mosaic goal paused.', 'info');
|
|
return;
|
|
}
|
|
if (command.action === 'cancel') {
|
|
persist(
|
|
{
|
|
...state,
|
|
phase: 'cancelled',
|
|
updatedAt: timestamp,
|
|
lastCheckSource: 'command',
|
|
lastCheckAt: timestamp,
|
|
lastCheckOutcome: 'cancelled',
|
|
stopReason: 'Cancelled by operator.',
|
|
},
|
|
ctx,
|
|
);
|
|
clearDeferredTimer();
|
|
continuationQueued = false;
|
|
if (!ctx.isIdle()) ctx.abort();
|
|
ctx.ui.notify('Mosaic goal cancelled.', 'info');
|
|
return;
|
|
}
|
|
if (command.action === 'resume') {
|
|
if (state.phase !== 'paused' && state.phase !== 'blocked' && state.phase !== 'exhausted') {
|
|
ctx.ui.notify(`Goal cannot be resumed from phase ${state.phase}.`, 'warning');
|
|
return;
|
|
}
|
|
persist(
|
|
{
|
|
...state,
|
|
phase: 'active',
|
|
updatedAt: timestamp,
|
|
turnCount: 0,
|
|
verificationPasses: 0,
|
|
noProgressReports: 0,
|
|
lastProgressFingerprint: undefined,
|
|
lastCheckSource: 'command',
|
|
lastCheckAt: timestamp,
|
|
lastCheckOutcome: 'resumed',
|
|
stopReason: undefined,
|
|
},
|
|
ctx,
|
|
);
|
|
continuationQueued = false;
|
|
ctx.ui.notify('Mosaic goal resumed with fresh bounded counters.', 'info');
|
|
if (ctx.isIdle()) queueContinuation(ctx, 'operator resume', false);
|
|
}
|
|
},
|
|
});
|
|
|
|
pi.registerTool({
|
|
name: GOAL_REPORT_TOOL,
|
|
label: 'Mosaic Goal Report',
|
|
description:
|
|
'Report structured progress for the active Mosaic /goal loop. Call it as the sole final tool when a work cycle is ready to stop, continue, verify, or block.',
|
|
promptSnippet: 'Report evidence-backed status for the active Mosaic goal loop',
|
|
promptGuidelines: [
|
|
'When a Mosaic goal is active, call mosaic_goal_report as the sole tool in the final assistant response for each work cycle.',
|
|
'Use mosaic_goal_report status=achieved only when concrete evidence covers the entire active goal.',
|
|
],
|
|
parameters: GoalReportParameters,
|
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
if (state === undefined || !isContinuingPhase(state.phase)) {
|
|
throw new Error(
|
|
'No active Mosaic goal can accept a report. Use /goal set or /goal resume.',
|
|
);
|
|
}
|
|
const input = parseGoalReportInput(params);
|
|
const fingerprint = reportFingerprint(input);
|
|
const timestamp = nowIso();
|
|
const report: GoalReport = {
|
|
...input,
|
|
fingerprint,
|
|
reportedAt: timestamp,
|
|
};
|
|
reportRollbackState = copyState(state);
|
|
|
|
let nextState: GoalState;
|
|
if (input.status === 'continue') {
|
|
const noProgressReports =
|
|
fingerprint === state.lastProgressFingerprint ? state.noProgressReports + 1 : 1;
|
|
const exhausted = noProgressReports >= state.maxNoProgressReports;
|
|
nextState = {
|
|
...state,
|
|
phase: exhausted ? 'exhausted' : 'active',
|
|
updatedAt: timestamp,
|
|
reportCount: state.reportCount + 1,
|
|
verificationPasses: 0,
|
|
noProgressReports,
|
|
lastProgressFingerprint: fingerprint,
|
|
lastReport: report,
|
|
lastCheckSource: 'report',
|
|
lastCheckAt: timestamp,
|
|
lastCheckOutcome: exhausted ? 'no-progress-limit' : 'continue',
|
|
...(exhausted
|
|
? {
|
|
stopReason: `Repeated no-progress report limit reached (${state.maxNoProgressReports}).`,
|
|
}
|
|
: { stopReason: undefined }),
|
|
};
|
|
} else if (input.status === 'achieved') {
|
|
const verificationPasses = state.phase === 'verifying' ? state.verificationPasses + 1 : 1;
|
|
const achieved = verificationPasses >= state.requiredVerificationPasses;
|
|
nextState = {
|
|
...state,
|
|
phase: achieved ? 'achieved' : 'verifying',
|
|
updatedAt: timestamp,
|
|
reportCount: state.reportCount + 1,
|
|
verificationPasses,
|
|
noProgressReports: 0,
|
|
lastProgressFingerprint: fingerprint,
|
|
lastReport: report,
|
|
lastCheckSource: 'report',
|
|
lastCheckAt: timestamp,
|
|
lastCheckOutcome: achieved ? 'verified-achieved' : 'provisional-achieved',
|
|
stopReason: undefined,
|
|
};
|
|
} else {
|
|
nextState = {
|
|
...state,
|
|
phase: 'blocked',
|
|
updatedAt: timestamp,
|
|
reportCount: state.reportCount + 1,
|
|
verificationPasses: 0,
|
|
noProgressReports: 0,
|
|
lastProgressFingerprint: fingerprint,
|
|
lastReport: report,
|
|
lastCheckSource: 'report',
|
|
lastCheckAt: timestamp,
|
|
lastCheckOutcome: 'blocked',
|
|
stopReason: input.summary,
|
|
};
|
|
}
|
|
|
|
persist(nextState, ctx);
|
|
if (nextState.phase === 'achieved') {
|
|
ctx.ui.notify(`Mosaic goal verified.\n${formatStatus(nextState)}`, 'info');
|
|
} else if (nextState.phase === 'verifying') {
|
|
ctx.ui.notify(
|
|
'Goal achievement is provisional; one verification pass is required.',
|
|
'info',
|
|
);
|
|
} else if (nextState.phase === 'blocked' || nextState.phase === 'exhausted') {
|
|
ctx.ui.notify(`Mosaic goal stopped in phase ${nextState.phase}.`, 'warning');
|
|
}
|
|
|
|
return {
|
|
content: [
|
|
{
|
|
type: 'text',
|
|
text:
|
|
nextState.phase === 'achieved'
|
|
? 'Goal verification complete.'
|
|
: `Goal report recorded; phase is ${nextState.phase}.`,
|
|
},
|
|
],
|
|
details: { state: copyState(nextState), report: copyReport(report) },
|
|
terminate: true,
|
|
};
|
|
},
|
|
});
|
|
|
|
pi.on('context', async (event) => {
|
|
const messages = event.messages.filter(
|
|
(message) =>
|
|
message.role !== 'custom' ||
|
|
(message.customType !== CONTEXT_MESSAGE_TYPE &&
|
|
message.customType !== CONTINUATION_MESSAGE_TYPE),
|
|
);
|
|
if (state === undefined || !isContinuingPhase(state.phase)) {
|
|
return messages.length === event.messages.length ? undefined : { messages };
|
|
}
|
|
messages.push({
|
|
role: 'custom',
|
|
customType: CONTEXT_MESSAGE_TYPE,
|
|
content: buildGoalContract(state),
|
|
display: false,
|
|
timestamp: Date.now(),
|
|
});
|
|
return { messages };
|
|
});
|
|
|
|
pi.on('turn_end', async (event, ctx) => {
|
|
if (state === undefined || state.phase === 'paused' || state.phase === 'cancelled') return;
|
|
const hasGoalReport = toolResultIncludesGoalReport(event.toolResults);
|
|
if (!isContinuingPhase(state.phase) && !hasGoalReport) return;
|
|
|
|
const timestamp = nowIso();
|
|
let nextState = {
|
|
...state,
|
|
updatedAt: timestamp,
|
|
turnCount: state.turnCount + 1,
|
|
lastCheckSource: 'turn' as const,
|
|
lastCheckAt: timestamp,
|
|
lastCheckOutcome: hasGoalReport ? 'reported' : 'checked-unreported',
|
|
};
|
|
|
|
if (
|
|
hasGoalReport &&
|
|
toolResultCount(event.toolResults) !== 1 &&
|
|
reportRollbackState !== undefined
|
|
) {
|
|
nextState = {
|
|
...reportRollbackState,
|
|
phase: 'active',
|
|
updatedAt: timestamp,
|
|
turnCount: reportRollbackState.turnCount + 1,
|
|
verificationPasses: 0,
|
|
lastCheckSource: 'turn',
|
|
lastCheckAt: timestamp,
|
|
lastCheckOutcome: 'mixed-goal-report-rejected',
|
|
stopReason: undefined,
|
|
};
|
|
ctx.ui.notify(
|
|
`${GOAL_REPORT_TOOL} must be the only tool call in its final response; the mixed report was ignored.`,
|
|
'warning',
|
|
);
|
|
}
|
|
reportRollbackState = undefined;
|
|
|
|
if (isContinuingPhase(nextState.phase) && nextState.turnCount >= nextState.maxTurns) {
|
|
nextState = {
|
|
...nextState,
|
|
phase: 'exhausted',
|
|
lastCheckOutcome: 'max-turn-limit',
|
|
stopReason: `Maximum autonomous turn limit reached (${nextState.maxTurns}).`,
|
|
};
|
|
persist(nextState, ctx);
|
|
ctx.ui.notify('Mosaic goal exhausted its autonomous turn limit.', 'warning');
|
|
ctx.abort();
|
|
return;
|
|
}
|
|
persist(nextState, ctx);
|
|
});
|
|
|
|
pi.on('agent_start', async () => {
|
|
continuationQueued = false;
|
|
clearDeferredTimer();
|
|
});
|
|
|
|
pi.on('agent_settled', async (_event, ctx) => {
|
|
if (state === undefined || !isContinuingPhase(state.phase)) return;
|
|
queueContinuation(ctx, state.phase === 'verifying' ? 'the provisional claim' : 'agent settle');
|
|
});
|
|
|
|
pi.on('session_compact', async (event, ctx) => {
|
|
if (state === undefined) return;
|
|
const timestamp = nowIso();
|
|
const wasContinuing = isContinuingPhase(state.phase);
|
|
persist(
|
|
{
|
|
...state,
|
|
phase: wasContinuing ? 'active' : state.phase,
|
|
updatedAt: timestamp,
|
|
verificationPasses: wasContinuing ? 0 : state.verificationPasses,
|
|
compactionCount: state.compactionCount + 1,
|
|
lastCheckSource: 'compact',
|
|
lastCheckAt: timestamp,
|
|
lastCheckOutcome: `checked-${event.reason}`,
|
|
...(wasContinuing ? { stopReason: undefined } : {}),
|
|
},
|
|
ctx,
|
|
);
|
|
if (wasContinuing) scheduleIdleContinuation(ctx, `${event.reason} compaction`);
|
|
});
|
|
|
|
pi.on('session_start', async (_event, ctx) => {
|
|
lifecycleGeneration += 1;
|
|
clearDeferredTimer();
|
|
restoreState(ctx);
|
|
if (state !== undefined && isContinuingPhase(state.phase)) {
|
|
const timestamp = nowIso();
|
|
persist(
|
|
{
|
|
...state,
|
|
updatedAt: timestamp,
|
|
lastCheckSource: 'restore',
|
|
lastCheckAt: timestamp,
|
|
lastCheckOutcome: 'session-start',
|
|
},
|
|
ctx,
|
|
);
|
|
scheduleIdleContinuation(ctx, 'session restore');
|
|
}
|
|
});
|
|
|
|
pi.on('session_tree', async (_event, ctx) => {
|
|
lifecycleGeneration += 1;
|
|
clearDeferredTimer();
|
|
restoreState(ctx);
|
|
if (state !== undefined && isContinuingPhase(state.phase)) {
|
|
const timestamp = nowIso();
|
|
persist(
|
|
{
|
|
...state,
|
|
updatedAt: timestamp,
|
|
lastCheckSource: 'restore',
|
|
lastCheckAt: timestamp,
|
|
lastCheckOutcome: 'tree-navigation',
|
|
},
|
|
ctx,
|
|
);
|
|
scheduleIdleContinuation(ctx, 'tree navigation');
|
|
}
|
|
});
|
|
|
|
pi.on('session_shutdown', async (_event, ctx) => {
|
|
lifecycleGeneration += 1;
|
|
clearDeferredTimer();
|
|
continuationQueued = false;
|
|
ctx.ui.setStatus(STATUS_KEY, undefined);
|
|
});
|
|
}
|