@amistio/cli 0.1.26 → 0.1.28

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 CHANGED
@@ -38,7 +38,7 @@ amistio run --watch --background --tool opencode
38
38
  amistio runner status
39
39
  ```
40
40
 
41
- Provider-backed model preferences use sanitized catalog fields: `--provider`, `--model-id`, optional `--model-variant`, and `--reasoning-effort` (`auto`, `low`, `medium`, `high`, or `xhigh`). Provider credentials, API keys, and local secret paths stay in the local tool configuration; they are not stored in Amistio preferences or runner heartbeats.
41
+ Provider-backed model preferences use sanitized catalog fields: `--provider`, `--model-id`, optional `--model-variant`, and `--reasoning-effort` (`auto`, `low`, `medium`, `high`, or `xhigh`). Opencode catalog metadata is synthesized by Amistio or derived from readable local OpenCode JSON config until opencode exposes a native catalog. Provider credentials, API keys, and local secret paths stay in the local tool configuration; they are not stored in Amistio preferences or runner heartbeats.
42
42
 
43
43
  When `--tool copilot` uses the GitHub Copilot SDK, Amistio approves read-only permission requests by default and denies mutating, network, MCP, hook, memory, and shell requests. Set `AMISTIO_COPILOT_APPROVE_ALL=1` only on a local machine where broad Copilot SDK approval is intentional.
44
44
 
@@ -46,7 +46,7 @@ When `--tool codex` uses the Codex SDK, intermediate progress can be quiet until
46
46
 
47
47
  `amistio runner status` reports local background runner state, latest heartbeat, and bounded resource usage when available. Resource usage is latest-sample runner process memory/CPU plus safe aggregate system memory/load signals; it does not include source files, environment variables, command lines, process lists, credentials, or arbitrary local paths.
48
48
 
49
- The runner advertises its supported work kinds in heartbeats. Current runners can claim read-only `projectContextRefresh` jobs from the workspace Context panel and create due runner-driven refreshes when no fresh approved map exists. Context refreshes inspect the paired checkout locally without modifying files and submit only bounded summaries, slices, entities, relations, safe citations, confidence, freshness, and repo-relative paths. If a submitted context refresh contains unsafe evidence, unsafe paths, or a map too large to store safely, Amistio marks the refresh failed with a safe reason instead of storing the rejected raw result. Approved maps are reused as context packs for source-aware assistant and impact-preview work. Current runners can also claim read-only issue diagnosis jobs from the web Issues panel, generate root-cause analysis and a proposed fix, and submit that result without modifying source. They can claim manual read-only `appEvaluationScan` jobs from the workspace Evaluate panel and create at most one due hourly evaluation during normal watch/background polling when app evaluation is enabled for the repository link. Evaluation results contain bounded summaries, safe evidence, suggested actions, lifecycle proposals, and repo-relative paths only. Current runners can also claim manual read-only `securityPostureScan` jobs from the workspace Security panel and create due daily posture checks during normal watch/background polling. Security scan results contain bounded summaries, standard references, safe evidence, and repo-relative paths only. Current runners can claim manual read-only `testQualityScan` jobs from the workspace Test panel and create one due daily Test scan per repository when Test quality is enabled. Test scans run only existing lint, typecheck, test, coverage, build, or verify commands and submit bounded command summaries, coverage summaries, safe findings, blocked reasons, warnings, and repo-relative paths. Missing tests, missing coverage, low coverage, failing checks, flaky tests, and test gaps create reviewable plan-backed findings in the app. Current runners also claim `implementationTestGate` jobs before implementation completion, PR handoff, or runner-managed push; a passing gate is required unless the web Test panel records an audited override. Current runners can claim read-only `implementationVerification` jobs from Tasks to prove whether completed implementation work actually landed; verification submits bounded acceptance-criteria evidence, checks, gaps, outcome, and recommendation without mutating source. Source, secrets, environment variables, command lines, process lists, credentials, provider sessions, and arbitrary local paths stay local. Implementation or cleanup is queued separately only after the user approves an issue analysis, app evaluation finding, security remediation plan, or Test quality plan in the app.
49
+ The runner advertises its supported work kinds in heartbeats. Current runners can claim read-only `projectContextRefresh` jobs from the workspace Context panel and create due runner-driven refreshes when no fresh approved map exists. Context refreshes inspect the paired checkout locally without modifying files and submit only bounded summaries, slices, entities, relations, safe citations, confidence, freshness, and repo-relative paths. If a submitted context refresh contains unsafe evidence, unsafe paths, or a map too large to store safely, Amistio marks the refresh failed with a safe reason instead of storing the rejected raw result. Approved maps are reused as context packs for source-aware assistant and impact-preview work. Current runners can also claim read-only issue diagnosis jobs from the web Issues panel, generate root-cause analysis and a proposed fix, and submit that result without modifying source. They can claim manual read-only `appEvaluationScan` jobs from the workspace Evaluate panel and create at most one due hourly evaluation during normal watch/background polling when app evaluation is enabled for the repository link. Evaluation results contain bounded summaries, safe evidence, suggested actions, lifecycle proposals, and repo-relative paths only. Current runners can also claim manual read-only `securityPostureScan` jobs from the workspace Security panel and create due daily posture checks during normal watch/background polling. Security scan results contain bounded summaries, standard references, safe evidence, and repo-relative paths only. Current runners can claim manual read-only `testQualityScan` jobs from the workspace Test panel and create one due daily Test scan per repository when Test quality is enabled. Test scans run only existing lint, typecheck, test, coverage, build, or verify commands and submit bounded command summaries, coverage summaries, safe findings, blocked reasons, warnings, and repo-relative paths. Missing tests, missing coverage, low coverage, failing checks, flaky tests, and test gaps create reviewable plan-backed findings in the app. Current runners also claim `implementationTestGate` jobs before implementation completion, PR handoff, or runner-managed push; a passing gate is required unless the web Test panel records an audited override. Blocked implementation Test gates submit structured Test findings, such as `blockedEnvironment`, with safe evidence, a suggested action, and a verification plan. Current runners can claim read-only `implementationVerification` jobs from Tasks to prove whether completed implementation work actually landed; verification submits bounded acceptance-criteria evidence, checks, gaps, outcome, and recommendation without mutating source. Source, secrets, environment variables, command lines, process lists, credentials, provider sessions, and arbitrary local paths stay local. Implementation or cleanup is queued separately only after the user approves an issue analysis, app evaluation finding, security remediation plan, or Test quality plan in the app.
50
50
 
51
51
  Approved implementation work uses Git as the handoff boundary. During worktree preflight, the runner locally copies eligible ignored root dotenv files such as `.env.local` or `.env.test.local` from the paired checkout into the implementation worktree when the target is missing and ignored, so local tests can use the same machine configuration. Dotenv values, variable names, file contents, and local paths are not uploaded to Amistio, and copied dotenv files stay ignored so PR handoff does not commit them. After the local tool completes successfully, the runner materializes approved Markdown, MDX, and HTML project-brain artifacts for the same work scope into the isolated worktree before final Git status. It then commits all source and artifact changes, fetches and rebases from the linked remote's default branch, pushes an `amistio/work/...` branch, opens or reuses a pull request with the locally authenticated `gh` CLI, reports only safe PR and artifact-inclusion metadata to Amistio, and removes the local worktree after the PR URL is durable. Artifact-only materialization changes still create or reuse a PR; no-change completion requires no source changes and no approved artifact changes. Prepare the runner machine with Git commit identity, fetch/push permission to the linked remote, and `gh auth status`. If artifact materialization, commit, fetch/rebase, push, or PR creation fails, the work item is blocked and the branch/worktree stay on disk for manual recovery; source files and patches are not uploaded to Amistio.
52
52
 
package/dist/index.js CHANGED
@@ -1691,6 +1691,7 @@ var appEvaluationFindingResultSchema = z.object({
1691
1691
  proposedPlanTitle: z.string().trim().min(1).max(200).optional(),
1692
1692
  proposedPlanRepoPath: z.string().trim().min(1).max(300).optional(),
1693
1693
  proposedPlanContent: z.string().trim().min(1).max(3e4).optional(),
1694
+ dedupeKey: z.string().trim().min(1).max(240).optional(),
1694
1695
  status: appEvaluationFindingStatusSchema.default("open")
1695
1696
  });
1696
1697
  var appEvaluationScanResultSchema = z.object({
@@ -1736,6 +1737,7 @@ var appEvaluationFindingItemSchema = baseItemSchema.extend({
1736
1737
  verificationPlan: z.array(z.string().trim().min(1).max(300)).min(1),
1737
1738
  proposedLifecycleAction: appEvaluationPlanLifecycleActionSchema.default("none"),
1738
1739
  relatedArtifactIds: z.array(z.string().trim().min(1).max(160)).default([]),
1740
+ dedupeKey: z.string().trim().min(1).max(240).optional(),
1739
1741
  proposedPlanDocumentId: z.string().min(1).optional(),
1740
1742
  implementationWorkItemId: z.string().min(1).optional(),
1741
1743
  reviewNotes: z.string().trim().min(1).optional(),
@@ -3603,7 +3605,7 @@ async function detectLocalTools() {
3603
3605
  localToolAdapters.map(async (adapter) => {
3604
3606
  const sdkAvailable = await isSdkAvailable(adapter);
3605
3607
  const commandAvailable = adapter.executable ? await commandExists(adapter.executable) : false;
3606
- const providerCatalog = await detectProviderCatalog(adapter);
3608
+ const providerCatalog = await detectRunnerProviderCatalog(adapter);
3607
3609
  return {
3608
3610
  name: adapter.name,
3609
3611
  description: adapter.description,
@@ -3845,16 +3847,18 @@ async function commandExists(command) {
3845
3847
  lookup.on("close", (exitCode) => resolve(exitCode === 0));
3846
3848
  });
3847
3849
  }
3848
- async function detectProviderCatalog(adapter) {
3849
- const opencodeCatalog = adapter.name === "opencode" ? await loadOpencodeProviderCatalog() : void 0;
3850
- return mergeProviderCatalogs(adapter.providerCatalog, opencodeCatalog);
3850
+ async function detectRunnerProviderCatalog(adapter) {
3851
+ const localOpencodeConfigCatalog = adapter.name === "opencode" ? await loadLocalOpencodeProviderConfigCatalog() : void 0;
3852
+ return mergeProviderCatalogs(adapter.providerCatalog, localOpencodeConfigCatalog);
3851
3853
  }
3852
- async function loadOpencodeProviderCatalog() {
3853
- const configPaths = [
3854
+ function localOpencodeProviderConfigPaths() {
3855
+ return [
3854
3856
  path6.join(os3.homedir(), ".config", "opencode", "opencode.json"),
3855
3857
  path6.join(os3.homedir(), ".config", "opencode", "config.json"),
3856
3858
  path6.join(process.cwd(), "opencode.json")
3857
3859
  ];
3860
+ }
3861
+ async function loadLocalOpencodeProviderConfigCatalog(configPaths = localOpencodeProviderConfigPaths()) {
3858
3862
  for (const configPath of configPaths) {
3859
3863
  try {
3860
3864
  const parsed = JSON.parse(await readFile4(configPath, "utf8"));
@@ -5616,6 +5620,7 @@ function createTestQualityScanPrompt(workItem, context) {
5616
5620
  "Accepted command kind values: verify, test, coverage, lint, typecheck, build, focused.",
5617
5621
  "Accepted command status values: passed, failed, skipped, missing, blocked.",
5618
5622
  "Accepted finding categories: missingTests, missingCoverage, lowCoverage, failingTests, failingQuality, missingCommand, flakyTests, unverifiedImplementation, testGap, other.",
5623
+ "Omit optional fields when unavailable; do not emit null for optional command summary fields such as exitCode, durationMs, outputExcerpt, commandId, or safePaths.",
5619
5624
  "",
5620
5625
  testQualityStart,
5621
5626
  '{"summary":"Whole-app verification exists, but coverage reporting is missing for one package.","profile":{"testProfileId":"test_profile_detected","repositoryLinkId":"repository_link_placeholder","status":"detected","enabled":true,"detectedPackageManager":"pnpm","packageManagers":["pnpm"],"commands":[{"commandId":"verify","kind":"verify","label":"Whole-app verification","source":"detected","scriptName":"verify","workingDirectory":".","required":true}],"coverageThresholds":{"lines":80},"defaultWholeAppCommandId":"verify","focusedCommandIds":[],"lastDetectedAt":"2026-01-01T00:00:00.000Z"},"commandSummaries":[{"commandId":"verify","kind":"verify","label":"Whole-app verification","status":"passed","exitCode":0,"summary":"The repository verification script completed successfully.","safePaths":["package.json"]}],"coverage":{"status":"missing","thresholds":{"lines":80},"summary":"No coverage report was found."},"findings":[{"title":"Coverage report is missing","category":"missingCoverage","severity":"medium","confidence":"high","summary":"The repository test profile does not produce a coverage summary.","affectedSurfaces":["Test verification"],"evidence":["The scan found a test command but no coverage output."],"safePaths":["package.json"],"suggestedAction":"Add or document a coverage command for the affected package and include it in whole-app verification.","verificationPlan":["Run the new coverage command locally","Confirm coverage appears in the Test panel"],"dedupeKey":"missing-coverage"}],"blockedReasons":[],"redactionState":{"status":"clean","redactedFields":[]},"verificationPlan":["Review generated Test findings","Run the whole-app verification command before implementation handoff"],"warnings":[]}',
@@ -5655,10 +5660,20 @@ function createImplementationTestGatePrompt(workItem) {
5655
5660
  "## Output Contract",
5656
5661
  "",
5657
5662
  "Print exactly one JSON object between the markers below. The CLI will submit only this structured gate result back to Amistio.",
5658
- "Accepted outcome values: passed, failed, blocked, overridden.",
5663
+ "Accepted outcome values: passed, failed, blocked, missingTests, belowThreshold, overridden.",
5664
+ "Accepted command kind values: verify, test, coverage, lint, typecheck, build, focused.",
5665
+ "Accepted command status values: passed, failed, skipped, missing, blocked.",
5666
+ "Prefer repository-documented verification commands or approved test profile commands over ad hoc package-script inference.",
5667
+ "For this Amistio monorepo, if plain Corepack pnpm fails before scripts with spawnSync pnpm ENOENT, retry the documented command corepack pnpm --config.verify-deps-before-run=false verify before declaring whole-app verification blocked.",
5668
+ "Accepted finding categories: missingTests, missingCoverage, missingCommand, lowCoverage, failingTests, failingQuality, failingQualityCheck, staleScan, blockedEnvironment, weakTests, flakyTests, unverifiedImplementation, testGap, other.",
5669
+ "Accepted finding severity values: info, low, medium, high, critical.",
5670
+ "Accepted finding confidence values: low, medium, high.",
5671
+ "Every finding must include title, category, severity, summary, suggestedAction, and a non-empty verificationPlan.",
5672
+ "Finding optional fields may include confidence, affectedSurfaces, evidence, safePaths, proposedPlanTitle, proposedPlanRepoPath, proposedPlanContent, and dedupeKey.",
5673
+ "Omit optional fields when unavailable; do not emit null for optional command summary fields such as exitCode, durationMs, outputExcerpt, commandId, or safePaths.",
5659
5674
  "",
5660
5675
  implementationTestGateStart,
5661
- '{"outcome":"passed","summary":"Focused checks and whole-app verification passed.","commandSummaries":[{"commandId":"verify","kind":"verify","label":"Whole-app verification","status":"passed","exitCode":0,"summary":"The repository verification script completed successfully.","safePaths":["package.json"]}],"coverage":{"status":"unknown","thresholds":{},"summary":"No coverage threshold was configured for this gate."},"findings":[],"blockedReasons":[],"redactionState":{"status":"clean","redactedFields":[]},"verificationPlan":["Record this gate result before marking implementation complete"],"warnings":[]}',
5676
+ '{"outcome":"blocked","summary":"Whole-app verification could not run because the local package-manager wrapper failed before repository checks started.","commandSummaries":[{"commandId":"verify","kind":"verify","label":"Whole-app verification","status":"blocked","summary":"Corepack could not spawn pnpm before repository verification started.","safePaths":["package.json"]}],"coverage":{"status":"unknown","thresholds":{},"summary":"Coverage was not produced because verification did not run."},"findings":[{"title":"Local verification toolchain is blocked","category":"blockedEnvironment","severity":"medium","confidence":"high","summary":"The implementation test gate could not start the repository verification command because the local package-manager toolchain failed before checks ran.","affectedSurfaces":["Local runner verification"],"evidence":["The verification command failed before repository checks started."],"safePaths":["package.json"],"suggestedAction":"Restore the local package-manager toolchain or run the repository-documented verification command, then rerun the implementation test gate.","verificationPlan":["Run the documented whole-app verification command","Rerun the implementation test gate"],"dedupeKey":"blocked-environment-package-manager"}],"blockedReasons":["The local package-manager toolchain could not start verification."],"redactionState":{"status":"clean","redactedFields":[]},"verificationPlan":["Restore local verification tooling","Rerun the implementation test gate before marking implementation complete"],"warnings":[]}',
5662
5677
  implementationTestGateEnd,
5663
5678
  "",
5664
5679
  "Do not put Markdown fences around the markers."
@@ -5937,9 +5952,10 @@ function createAppEvaluationScanPrompt(workItem, context) {
5937
5952
  "Accepted severity values: info, low, medium, high, critical.",
5938
5953
  "Accepted confidence values: low, medium, high.",
5939
5954
  "Accepted proposedLifecycleAction values: createPlan, updatePlan, markCompleted, markImplemented, markSuperseded, markBlocked, archive, keepActive, none.",
5955
+ "Include a stable dedupeKey for each finding based on the underlying issue identity, not scan time, generated wording, or transient ordering.",
5940
5956
  "",
5941
5957
  appEvaluationStart,
5942
- '{"summary":"The app is generally healthy, but one release-readiness plan needs cleanup and verification evidence should be refreshed.","baselineVersion":"amistio-app-evaluation-v1","findings":[{"title":"Stale release plan should be marked superseded","category":"planCleanup","severity":"low","confidence":"high","summary":"A release plan appears to describe a workflow that has since been replaced by a newer accepted plan.","affectedSurfaces":["docs/plans"],"evidence":["A newer plan references the same scope and status, while the older plan remains proposed."],"suggestedAction":"Prepare a cleanup update that marks the stale plan superseded and links to the newer plan.","verificationPlan":["Review both plan files","Confirm the newer plan is accepted before changing lifecycle state"],"safePaths":["docs/plans/PLAN-example.md"],"proposedLifecycleAction":"markSuperseded","relatedArtifactIds":[]}],"verificationPlan":["Review App Evaluation findings for approval","Run the canonical verify gate before implementing approved actions"]}',
5958
+ '{"summary":"The app is generally healthy, but one release-readiness plan needs cleanup and verification evidence should be refreshed.","baselineVersion":"amistio-app-evaluation-v1","findings":[{"title":"Stale release plan should be marked superseded","category":"planCleanup","severity":"low","confidence":"high","dedupeKey":"plan-cleanup-docs-plans-plan-example-superseded","summary":"A release plan appears to describe a workflow that has since been replaced by a newer accepted plan.","affectedSurfaces":["docs/plans"],"evidence":["A newer plan references the same scope and status, while the older plan remains proposed."],"suggestedAction":"Prepare a cleanup update that marks the stale plan superseded and links to the newer plan.","verificationPlan":["Review both plan files","Confirm the newer plan is accepted before changing lifecycle state"],"safePaths":["docs/plans/PLAN-example.md"],"proposedLifecycleAction":"markSuperseded","relatedArtifactIds":[]}],"verificationPlan":["Review App Evaluation findings for approval","Run the canonical verify gate before implementing approved actions"]}',
5943
5959
  appEvaluationEnd,
5944
5960
  "",
5945
5961
  "Do not put Markdown fences around the markers. Do not implement improvements or cleanup."
@@ -6299,9 +6315,9 @@ function parseTestQualityScanResult(output) {
6299
6315
  }
6300
6316
  const payload = output.slice(start + testQualityStart.length, end).trim();
6301
6317
  const parsed = JSON.parse(stripJsonFence(payload));
6302
- return testQualityScanResultSchema.parse(normalizeTestQualityScanResultCommandKinds(parsed));
6318
+ return testQualityScanResultSchema.parse(normalizeTestQualityScanResult(parsed));
6303
6319
  }
6304
- function normalizeTestQualityScanResultCommandKinds(value) {
6320
+ function normalizeTestQualityScanResult(value) {
6305
6321
  if (!isObjectRecord(value)) {
6306
6322
  return value;
6307
6323
  }
@@ -6313,7 +6329,29 @@ function normalizeTestQualityScanResultCommandKinds(value) {
6313
6329
  };
6314
6330
  }
6315
6331
  if (Array.isArray(normalized.commandSummaries)) {
6316
- normalized.commandSummaries = normalized.commandSummaries.map(normalizeTestCommandKindObject);
6332
+ normalized.commandSummaries = normalized.commandSummaries.map(normalizeTestCommandSummaryObject);
6333
+ }
6334
+ return normalized;
6335
+ }
6336
+ function normalizeImplementationTestGateResult(value) {
6337
+ if (!isObjectRecord(value)) {
6338
+ return value;
6339
+ }
6340
+ const normalized = { ...value };
6341
+ if (Array.isArray(normalized.commandSummaries)) {
6342
+ normalized.commandSummaries = normalized.commandSummaries.map(normalizeTestCommandSummaryObject);
6343
+ }
6344
+ return normalized;
6345
+ }
6346
+ function normalizeTestCommandSummaryObject(value) {
6347
+ if (!isObjectRecord(value)) {
6348
+ return value;
6349
+ }
6350
+ const normalized = { ...value, kind: normalizeTestCommandKind(value.kind) };
6351
+ for (const key of ["commandId", "durationMs", "exitCode", "outputExcerpt", "safePaths"]) {
6352
+ if (normalized[key] === null) {
6353
+ delete normalized[key];
6354
+ }
6317
6355
  }
6318
6356
  return normalized;
6319
6357
  }
@@ -6351,7 +6389,7 @@ function parseImplementationTestGateResult(output) {
6351
6389
  }
6352
6390
  const payload = output.slice(start + implementationTestGateStart.length, end).trim();
6353
6391
  const parsed = JSON.parse(stripJsonFence(payload));
6354
- return implementationTestGateResultSchema.parse(parsed);
6392
+ return implementationTestGateResultSchema.parse(normalizeImplementationTestGateResult(parsed));
6355
6393
  }
6356
6394
  function projectContextRefreshSubmissionFailureSummary(result) {
6357
6395
  if (result.refresh.status !== "failed" && result.workItem.status !== "failed") {