Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ export CLAUDECODE_HOME="$ISOLATED_ROOT/claudecode-home"
export AMP_CLI_HOME="$ISOLATED_ROOT/ampcli-home"
cd "$WORKDIR"
npm init -y
npm install <public-repo-root>/opendevbrowser-0.0.21.tgz
npm install <public-repo-root>/opendevbrowser-0.0.22.tgz
npx --no-install opendevbrowser --help
npx --no-install opendevbrowser help
```
Expand Down
1 change: 1 addition & 0 deletions docs/CLI.md
Original file line number Diff line number Diff line change
Expand Up @@ -776,6 +776,7 @@ npx opendevbrowser macro-resolve --expression '@community.search("browser automa
Notes:
- Default mode is resolve-only (returns the resolved action/provenance payload).
- `--execute` runs the resolved provider action and returns additive execution metadata (`meta.tier.selected`, `meta.tier.reasonCode`, `meta.provenance.provider`, `meta.provenance.retrievalPath`, `meta.provenance.retrievedAt`).
- Resolve-only and execute responses now both emit `followthroughSummary`, `suggestedNextAction`, and `suggestedSteps` so the next rerun command stays explicit even when execution blocks.
- `--timeout-ms` sets client-side daemon transport timeout for slow `--execute` runs.
- `--challenge-automation-mode` is accepted for `--execute` runs and maps to `challengeAutomationMode` with the same `run > session > config` precedence as workflow commands.
- `opendevbrowser --help` includes this timeout flag in the global flag inventory.
Expand Down
2 changes: 1 addition & 1 deletion docs/FIRST_RUN_ONBOARDING.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ Expected:
- the `Find It Fast` block includes the exact lookup terms `screencast / browser replay`, `desktop observation`, and `computer use / browser-scoped computer use`
- the block maps replay to `screencast-start` / `screencast-stop`
- the block maps desktop observation to the public `desktop-*` family
- the block maps browser-scoped computer use to `--challenge-automation-mode` on `research run`, `shopping run`, `product-video run`, and `macro-resolve --execute`
- the block maps browser-scoped computer use to `--challenge-automation-mode` on `research run`, `shopping run`, `product-video run`, `inspiredesign run`, and `macro-resolve --execute`
- the block includes a concrete browser-scoped entry command such as `npx opendevbrowser research run --topic "account recovery flow" --source-selection auto --challenge-automation-mode browser --mode json --output-format json`
- help then opens the `Agent Quick Start` block
- the block explicitly points agents to `opendevbrowser_prompting_guide`
Expand Down
7 changes: 7 additions & 0 deletions scripts/docs-drift-check.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,7 @@ export function runDocsDriftChecks() {
&& onboardingDoc.includes(onboardingMetadata.quickStartCommands.promptingGuide)
&& onboardingDoc.includes(onboardingMetadata.quickStartCommands.skillLoad)
&& onboardingDoc.includes(onboardingMetadata.quickStartCommands.computerUseEntry)
&& onboardingDoc.includes("inspiredesign run")
&& onboardingDoc.includes(onboardingMetadata.referencePaths.skillDoc)
&& onboardingDoc.includes("docs/RELEASE_RUNBOOK.md"),
detail: "docs/FIRST_RUN_ONBOARDING.md must document the generated-help quick-start path, computer-use entry command, canonical skill runbook, and the separate release runbook for published npm proof."
Expand Down Expand Up @@ -811,6 +812,12 @@ export function runDocsDriftChecks() {
detail: `docs/CLI.md should reference opendevbrowser-${version}.tgz`
});

checks.push({
id: "doc.readme.current_package_version_ref",
ok: publicReadme.includes(`opendevbrowser-${version}.tgz`),
detail: `README.md should reference opendevbrowser-${version}.tgz`
});

checks.push({
id: "doc.onboarding.current_package_version_ref",
ok: onboardingDoc.includes(`opendevbrowser-${version}.tgz`),
Expand Down
18 changes: 12 additions & 6 deletions scripts/generate-public-surface-manifest.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -119,12 +119,14 @@ function renderManifestModule(manifest, outPath) {
" {",
" description: command.description,",
" usage: command.usage,",
" flags: [...command.flags]",
" flags: [...command.flags],",
" examples: [...command.examples],",
" notes: [...command.notes]",
" } satisfies CommandHelpDetail",
" ] as const)",
") as unknown as Record<PublicSurfaceCliCommandName, CommandHelpDetail>;",
"",
"const COMMANDS_BY_GROUP = new Map(",
"const COMMANDS_BY_GROUP = Object.fromEntries(",
" PUBLIC_SURFACE_MANIFEST.cli.groups.map((group) => [",
" group.id,",
" PUBLIC_SURFACE_MANIFEST.cli.commands",
Expand All @@ -133,22 +135,26 @@ function renderManifestModule(manifest, outPath) {
" name: command.name,",
" description: command.description,",
" usage: command.usage,",
" flags: [...command.flags]",
" flags: [...command.flags],",
" examples: [...command.examples],",
" notes: [...command.notes]",
" }))",
" ] as const)",
");",
") as Record<string, PublicSurfaceCliCommandGroupDefinition['commands']>;",
"",
"export const PUBLIC_CLI_COMMAND_GROUPS = PUBLIC_SURFACE_MANIFEST.cli.groups.map((group) => ({",
" id: group.id,",
" title: group.title,",
" summary: group.summary,",
" commands: COMMANDS_BY_GROUP.get(group.id) ?? []",
" commands: COMMANDS_BY_GROUP[group.id]",
"})) as readonly PublicSurfaceCliCommandGroupDefinition[];",
"",
"export const TOOL_SURFACE_ENTRIES = PUBLIC_SURFACE_MANIFEST.tools.entries.map((entry) => ({",
" name: entry.name,",
" description: entry.description,",
" ...(entry.cliEquivalent ? { cliEquivalent: entry.cliEquivalent } : {})",
" ...(entry.cliEquivalent ? { cliEquivalent: entry.cliEquivalent } : {}),",
" example: entry.example,",
" ...(entry.notes ? { notes: [...entry.notes] } : {})",
"})) as readonly ToolSurfaceEntry[];",
""
].join("\n");
Expand Down
70 changes: 57 additions & 13 deletions scripts/provider-direct-runs.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,46 @@ function hasLinkedInAuthWall(records) {
return gated.length > 0 && gated.length === records.length;
}

function getDeferredChallengeClassification(challengeOrchestration) {
const classification = readStringField(challengeOrchestration, "classification");
if (
readStringField(challengeOrchestration, "status") !== "deferred"
|| (classification !== "auth_required" && classification !== "checkpoint_or_friction")
) {
return null;
}

const verification = readJsonRecordField(challengeOrchestration, "verification");
const bundle = readJsonRecordField(verification, "bundle");
const continuity = readJsonRecordField(bundle, "continuity");
if (!continuity) {
return null;
}

const loginRefs = Array.isArray(continuity.loginRefs)
? continuity.loginRefs.filter((entry) => typeof entry === "string" && entry.length > 0)
: [];
const checkpointRefs = Array.isArray(continuity.checkpointRefs)
? continuity.checkpointRefs.filter((entry) => typeof entry === "string" && entry.length > 0)
: [];
const likelyLoginPage = continuity.likelyLoginPage === true;
const likelyHumanVerification = continuity.likelyHumanVerification === true;

if (classification === "auth_required" && (likelyLoginPage || loginRefs.length > 0)) {
return {
status: "env_limited",
detail: "deferred_auth_wall_only"
};
}
if (classification === "checkpoint_or_friction" && (likelyHumanVerification || checkpointRefs.length > 0)) {
return {
status: "env_limited",
detail: "deferred_checkpoint_only"
};
}
return null;
}

function normalizePlainText(value) {
return typeof value === "string"
? value.replace(/\s+/g, " ").trim()
Expand Down Expand Up @@ -971,20 +1011,24 @@ function evaluateMacroCase(testCase, result) {

const reasonCodes = normalizedCodesFromFailures(execution.failures);
const linkedinAuthWall = testCase.providerId === "social/linkedin" && hasLinkedInAuthWall(execution.records);
const deferredChallengeClassification = testCase.providerId.startsWith("social/")
? getDeferredChallengeClassification(challengeOrchestration)
: null;
const shellOnlyClassification = classifyMacroRecordQuality(testCase, execution);
const classified = linkedinAuthWall
? { status: "env_limited", detail: "linkedin_auth_wall_only" }
: (
shellOnlyClassification
?? classifyRecords(
execution.records.length,
execution.failures,
{
allowExpectedUnavailable: testCase.allowExpectedUnavailable === true,
allowNoRecordsNoFailures: testCase.providerId.startsWith("social/")
}
)
);
const classified = deferredChallengeClassification
?? (linkedinAuthWall
? { status: "env_limited", detail: "linkedin_auth_wall_only" }
: (
shellOnlyClassification
?? classifyRecords(
execution.records.length,
execution.failures,
{
allowExpectedUnavailable: testCase.allowExpectedUnavailable === true,
allowNoRecordsNoFailures: testCase.providerId.startsWith("social/")
}
)
));
const { rawFailure, verdict } = resolveDirectHarnessVerdict({
classified,
detail: result.detail,
Expand Down
2 changes: 1 addition & 1 deletion scripts/provider-live-matrix.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ const BROWSER_REALWORLD_TARGETS = [
{ id: 'x.search', url: 'https://x.com/home' },
{ id: 'youtube.search', url: 'https://www.youtube.com/results?search_query=browser+automation+anti+bot' },
{ id: 'instagram.explore', url: 'https://www.instagram.com/explore/' },
{ id: 'facebook.search', url: 'https://www.facebook.com/search/top/?q=browser%20automation' },
{ id: 'facebook.search', url: 'https://www.facebook.com/watch/search/?q=browser%20automation' },
{ id: 'linkedin.search', url: 'https://www.linkedin.com/search/results/content/?keywords=browser%20automation' }
];
export const WORKFLOW_RESEARCH_PROBE_ARGS = [
Expand Down
27 changes: 27 additions & 0 deletions src/cli/commands/macro-resolve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,40 @@ const asRecord = (value: unknown): Record<string, unknown> | null => {
return value as Record<string, unknown>;
};

const readNonEmptyString = (value: unknown): string | null => (
typeof value === "string" && value.trim().length > 0
? value.trim()
: null
);

const hasExecutionBlocker = (result: unknown): boolean => {
const execution = asRecord(asRecord(result)?.execution);
const meta = asRecord(execution?.meta);
return asRecord(meta?.blocker) !== null;
};

const readMacroResolveSummary = (result: unknown): string | null => (
readNonEmptyString(asRecord(result)?.followthroughSummary)
);

const readMacroResolveNextStep = (result: unknown): string | null => {
const record = asRecord(result);
const explicit = readNonEmptyString(record?.suggestedNextAction);
if (explicit) {
return explicit;
}
const [firstStep] = Array.isArray(record?.suggestedSteps)
? record.suggestedSteps.filter((step): step is Record<string, unknown> => Boolean(step) && typeof step === "object")
: [];
return readNonEmptyString(firstStep?.command) ?? readNonEmptyString(firstStep?.reason);
};

const buildMacroResolveMessage = (execute: boolean, result: unknown): string => {
const summary = readMacroResolveSummary(result);
const nextStep = readMacroResolveNextStep(result);
if (summary) {
return nextStep ? `${summary} Next step: ${nextStep}` : summary;
}
if (!execute) {
return "Macro resolved.";
}
Expand Down
22 changes: 20 additions & 2 deletions src/cli/daemon-commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
runResearchWorkflow,
runShoppingWorkflow
} from "../providers/workflows";
import { buildMacroResolveSuccessHandoff } from "../providers/workflow-handoff";
import { isChallengeAutomationMode, type ChallengeAutomationMode } from "../challenges";
import {
type MacroExecutionPayload,
Expand Down Expand Up @@ -2047,6 +2048,9 @@ async function resolveMacroExpression(
resolution: MacroResolution;
catalog?: Array<{ name: string; pack?: string; description?: string }>;
execution?: MacroExecutionPayload;
followthroughSummary: string;
suggestedNextAction: string;
suggestedSteps: Array<{ reason: string; command?: string }>;
}> {
const runtime = await loadMacroRuntime();
const registry = runtime?.createDefaultMacroRegistry?.();
Expand All @@ -2071,10 +2075,17 @@ async function resolveMacroExpression(
}

if (!options.execute) {
const handoff = buildMacroResolveSuccessHandoff({
expression: options.expression,
defaultProvider: options.defaultProvider,
execute: false,
blocked: false
});
return {
runtime: resolvedRuntime,
resolution,
...(catalog ? { catalog } : {})
...(catalog ? { catalog } : {}),
...handoff
};
}

Expand All @@ -2087,10 +2098,17 @@ async function resolveMacroExpression(
timeoutMs: options.timeoutMs,
challengeAutomationMode: options.challengeAutomationMode
});
const handoff = buildMacroResolveSuccessHandoff({
expression: options.expression,
defaultProvider: options.defaultProvider,
execute: true,
blocked: Boolean(execution.meta.blocker)
});
return {
runtime: resolvedRuntime,
resolution,
...(catalog ? { catalog } : {}),
execution
execution,
...handoff
};
}
20 changes: 16 additions & 4 deletions src/cli/help.ts
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,9 @@ function assertCommandCoverage(): void {
if (!detail || !detail.description.trim() || !detail.usage.trim()) {
throw new Error(`Missing command help metadata: ${command}`);
}
if (detail.examples.length === 0) {
throw new Error(`Missing command example metadata: ${command}`);
}
for (const flag of detail.flags) {
if (!FLAG_SET.has(flag)) {
throw new Error(`Command help metadata references unknown flag ${flag} for ${command}`);
Expand Down Expand Up @@ -433,6 +436,9 @@ function assertToolCoverage(): void {
if (!entry.description.trim()) {
throw new Error(`Help tool is missing a description: ${entry.name}`);
}
if (!entry.example?.trim()) {
throw new Error(`Help tool is missing an example: ${entry.name}`);
}
if (seen.has(entry.name)) {
throw new Error(`Help tool appears multiple times: ${entry.name}`);
}
Expand All @@ -457,7 +463,9 @@ function formatCommandGroups(): string {
description: detail.description,
details: [
{ label: "usage:", value: detail.usage },
{ label: "flags:", value: formatFlags(detail.flags) }
{ label: "flags:", value: formatFlags(detail.flags) },
...detail.examples.map((example) => ({ label: "example:", value: example })),
...detail.notes.map((note) => ({ label: "note:", value: note }))
]
};
});
Expand All @@ -483,9 +491,13 @@ function formatToolEntries(): string {
return formatRows(HELP_TOOL_ENTRIES.map((entry) => ({
label: entry.name,
description: entry.description,
details: entry.cliEquivalent
? [{ label: "cli:", value: entry.cliEquivalent }]
: [{ label: "scope:", value: "tool-only" }]
details: [
...(entry.cliEquivalent
? [{ label: "cli:", value: entry.cliEquivalent }]
: [{ label: "scope:", value: "tool-only" }]),
...(entry.example ? [{ label: "example:", value: entry.example }] : []),
...((entry.notes ?? []).map((note) => ({ label: "note:", value: note })))
]
})));
}

Expand Down
26 changes: 25 additions & 1 deletion src/cli/utils/workflow-message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,14 @@ const readNonEmptyString = (value: unknown): string | null => (
: null
);

const UNRESOLVED_COMMAND_PLACEHOLDER_RE = /<[^>\n]+>/;

const readRunnableStepCommand = (step: Record<string, unknown>): string | null => {
const command = readNonEmptyString(step.command);
if (!command) return null;
return UNRESOLVED_COMMAND_PLACEHOLDER_RE.test(command) ? null : command;
};

const readMeta = (data: unknown): Record<string, unknown> | null => {
return asRecord(asRecord(data)?.meta);
};
Expand Down Expand Up @@ -84,6 +92,22 @@ export const readSuggestedNextAction = (data: unknown): string | null => {
?? readNonEmptyString(asRecord(record.sessionInspector)?.suggestedNextAction);
};

export const readSuggestedStepCommand = (data: unknown): string | null => {
let current = asRecord(data);

while (current) {
const command = readSuggestedSteps(current)
.map(readRunnableStepCommand)
.find((step): step is string => Boolean(step));
if (command) {
return command;
}
current = asRecord(current.challengePlan);
}

return null;
};

export const readSuggestedStepReason = (data: unknown): string | null => {
let current = asRecord(data);

Expand Down Expand Up @@ -117,7 +141,7 @@ export const buildWorkflowCompletionMessage = (workflowLabel: string, data: unkn
if (followthroughSummary) {
return buildNextStepMessage(
`${workflowLabel} completed. ${followthroughSummary}`,
readSuggestedNextAction(data) ?? readSuggestedStepReason(data)
readSuggestedNextAction(data) ?? readSuggestedStepCommand(data) ?? readSuggestedStepReason(data)
);
}
return `${workflowLabel} completed.`;
Expand Down
Loading
Loading