@amistio/cli 0.1.17 → 0.1.19

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
@@ -15,7 +15,7 @@ Runner lifecycle controls in the web app, such as update, restart, and remove, a
15
15
 
16
16
  Runner Update installs the official `@amistio/cli` package and then refreshes the runner runtime. Background runners attempt a replacement restart so the next heartbeat reports the new CLI version. If replacement restart metadata is missing or restart fails after a successful install, the old runner still stops and reports manual restart guidance instead of continuing to heartbeat the stale runtime. Foreground `amistio run --watch` sessions stop after a successful install with restart guidance; start the command again to load the updated package.
17
17
 
18
- Current runners advertise the work kinds they can claim. Older runners that do not send this capability can continue legacy brain generation, implementation, and plan revision work, but they will skip source-aware assistant, impact-preview, project-context refresh, issue-diagnosis, app-evaluation, and security-posture work until updated.
18
+ Current runners advertise the work kinds they can claim. Older runners that do not send this capability can continue legacy brain generation, implementation, and plan revision work, but they will skip source-aware assistant, impact-preview, project-context refresh, issue-diagnosis, app-evaluation, security-posture, and implementation-verification work until updated.
19
19
 
20
20
  Repository brain auto-sync is disabled until the repository link option is enabled in the app. After pairing, run `amistio sync watch` from the paired checkout to push recognized external brain Markdown/MDX files and HTML companions under `docs/html/<area>/`, including local ADRs, plans, prompts, workflows, memory, context, architecture, and feature docs, to the app for review. `amistio run --watch` also runs the same cycle between work polls when the option is enabled. The CLI skips templates, unsupported paths, oversized files, unchanged managed docs, and conflicts instead of silently overwriting web state.
21
21
 
@@ -43,14 +43,18 @@ When `--tool codex` uses the Codex SDK, intermediate progress can be quiet until
43
43
 
44
44
  `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.
45
45
 
46
- 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; 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, or security remediation plan in the app.
46
+ 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 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, or security remediation plan in the app.
47
47
 
48
48
  Approved implementation work uses Git as the handoff boundary. After the local tool completes successfully, the runner commits the isolated worktree, pushes an `amistio/work/...` branch to the linked GitHub remote, opens or reuses a pull request with the locally authenticated `gh` CLI, reports only safe PR metadata to Amistio, and removes the local worktree after the PR URL is durable. Prepare the runner machine with Git commit identity, push permission to the linked remote, and `gh auth status`. If commit, 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.
49
49
 
50
+ Failed or stale work can be requeued from the web Tasks panel. Requeue creates a new linked work attempt and preserves the original terminal attempt for audit history; it is blocked while equivalent work is already active or when the paired runner does not advertise the needed work kind. Completed implementation status is separate from proof: queue `implementationVerification` from Tasks when a plan needs source-aware evidence before cleanup or implementation status decisions.
51
+
50
52
  Runner setup and local-tool execution use bounded failure controls. `amistio run --watch` retries Git worktree preflight failures by releasing the claim for another attempt, then fails the work item after `--max-preflight-attempts` attempts, defaulting to 3. Active local-tool runs renew the work lease, and `--tool-timeout-seconds` caps tool execution, defaulting to 1800 seconds.
51
53
 
52
54
  Watch mode prints a completed-work success once per work item, keeps fresh completion visible briefly, and returns old completed work to the ready state when no queued, running, blocked, failed, review, or runner-readiness action needs attention.
53
55
 
56
+ Known validation failures such as `unsafe_context_path` are printed with attention-needed next steps. For project-context refresh path-safety failures, deploy the latest web/API fix, update and restart the runner when applicable, retry the refresh, and capture only bounded non-secret output if it repeats.
57
+
54
58
  For headless startup after login on supported user-level service managers:
55
59
 
56
60
  ```sh
package/dist/index.js CHANGED
@@ -26,6 +26,7 @@ var itemTypeSchema = z.enum([
26
26
  "securityPostureSnapshot",
27
27
  "appEvaluationScan",
28
28
  "appEvaluationFinding",
29
+ "implementationVerification",
29
30
  "projectContextMap",
30
31
  "projectContextRefresh",
31
32
  "projectSystemDiagram",
@@ -78,9 +79,12 @@ var workStatusSchema = z.enum([
78
79
  ]);
79
80
  var sourceSchema = z.enum(["web", "repo", "generated", "runner"]);
80
81
  var executionModeSchema = z.enum(["localRunner", "cloudSandbox"]);
81
- var workKindSchema = z.enum(["brainGeneration", "implementation", "planRevision", "assistantQuestion", "impactPreview", "issueDiagnosis", "securityPostureScan", "appEvaluationScan", "projectContextRefresh"]);
82
+ var workKindSchema = z.enum(["brainGeneration", "implementation", "planRevision", "assistantQuestion", "impactPreview", "issueDiagnosis", "securityPostureScan", "appEvaluationScan", "projectContextRefresh", "implementationVerification"]);
82
83
  var implementationHandoffStatusSchema = z.enum(["notStarted", "noChanges", "prReady", "blocked", "failed"]);
83
84
  var implementationHandoffCleanupStatusSchema = z.enum(["notApplicable", "pending", "completed", "failed"]);
85
+ var implementationVerificationOutcomeSchema = z.enum(["verifiedImplemented", "partiallyImplemented", "notImplemented", "inconclusive", "verificationBlocked"]);
86
+ var implementationVerificationStatusSchema = z.enum(["queued", "running", "completed", "failed", "blocked", "stale"]);
87
+ var implementationProofStatusSchema = z.enum(["unverified", "verificationQueued", "verificationRunning", "verifiedImplemented", "partiallyImplemented", "notImplemented", "inconclusive", "verificationBlocked", "humanOverride"]);
84
88
  var implementationHandoffSchema = z.object({
85
89
  provider: z.string().trim().min(1).max(80).optional(),
86
90
  status: implementationHandoffStatusSchema,
@@ -105,7 +109,7 @@ var issueStatusSchema = z.enum(["queued", "diagnosing", "analysisReady", "approv
105
109
  var issueApprovalStateSchema = z.enum(["proposed", "approved", "changesRequested", "rejected", "implemented", "blocked"]);
106
110
  var securityScanStatusSchema = z.enum(["queued", "running", "completed", "failed", "stale", "skipped"]);
107
111
  var securityScanSourceSchema = z.enum(["nightly", "manual", "runner"]);
108
- var securityFindingCategorySchema = z.enum(["owasp", "dependency", "supplyChain", "secretHygiene", "authentication", "authorization", "tenantIsolation", "headers", "redirects", "csrf", "cors", "ciCd", "logging", "runnerBoundary", "other"]);
112
+ var securityFindingCategorySchema = z.enum(["owasp", "dependency", "supplyChain", "secretHygiene", "authentication", "authorization", "tenantIsolation", "headers", "redirects", "csrf", "cors", "ciCd", "logging", "rateLimiting", "runnerBoundary", "other"]);
109
113
  var securityFindingSeveritySchema = z.enum(["info", "low", "medium", "high", "critical"]);
110
114
  var securityFindingConfidenceSchema = z.enum(["low", "medium", "high"]);
111
115
  var securityFindingStatusSchema = z.enum(["open", "planReady", "approved", "changesRequested", "dismissed", "acceptedRisk", "remediationQueued", "resolved", "failed"]);
@@ -156,6 +160,14 @@ var activityEventTypeSchema = z.enum([
156
160
  "appEvaluationFindingReviewed",
157
161
  "appEvaluationPlanCreated",
158
162
  "appEvaluationImplementationQueued",
163
+ "workRequeueRequested",
164
+ "workRequeueQueued",
165
+ "workRequeueBlocked",
166
+ "implementationVerificationQueued",
167
+ "implementationVerificationStarted",
168
+ "implementationVerificationCompleted",
169
+ "implementationVerificationBlocked",
170
+ "implementationVerificationOverride",
159
171
  "projectContextRefreshQueued",
160
172
  "projectContextRefreshStarted",
161
173
  "projectContextRefreshCompleted",
@@ -423,8 +435,20 @@ var workItemSchema = baseItemSchema.extend({
423
435
  securityFindingId: z.string().min(1).optional(),
424
436
  appEvaluationScanId: z.string().min(1).optional(),
425
437
  appEvaluationFindingId: z.string().min(1).optional(),
438
+ implementationVerificationId: z.string().min(1).optional(),
426
439
  projectContextRefreshId: z.string().min(1).optional(),
427
440
  contextMissId: z.string().min(1).optional(),
441
+ sourceWorkItemId: z.string().min(1).optional(),
442
+ requeueReason: z.string().trim().min(1).max(600).optional(),
443
+ requeuedByUserId: z.string().min(1).optional(),
444
+ requeuedAt: isoDateTimeSchema.optional(),
445
+ sourceAttempt: z.number().int().nonnegative().optional(),
446
+ sourceTerminalStatus: workStatusSchema.optional(),
447
+ sourceFailureSummary: z.string().trim().min(1).max(1e3).optional(),
448
+ implementationProofStatus: implementationProofStatusSchema.optional(),
449
+ implementationVerificationOutcome: implementationVerificationOutcomeSchema.optional(),
450
+ implementationVerificationResultId: z.string().min(1).optional(),
451
+ humanImplementationOverrideReason: z.string().trim().min(1).max(1e3).optional(),
428
452
  repositoryLinkId: z.string().min(1).optional(),
429
453
  reviewThreadId: z.string().min(1).optional(),
430
454
  reviewDocumentId: z.string().min(1).optional(),
@@ -965,6 +989,59 @@ var projectContextPackSchema = z.object({
965
989
  tokenEstimate: z.number().int().nonnegative().default(0),
966
990
  generatedAt: isoDateTimeSchema
967
991
  });
992
+ var implementationVerificationCheckStatusSchema = z.enum(["passed", "failed", "blocked", "skipped"]);
993
+ var implementationVerificationRecommendationSchema = z.enum(["none", "requeue", "createFollowUpPlan", "requestHumanReview", "markBlocked"]);
994
+ var implementationVerificationEvidenceSchema = z.object({
995
+ acceptanceCriterion: z.string().trim().min(1).max(500),
996
+ status: z.enum(["satisfied", "partial", "missing", "unknown"]),
997
+ summary: z.string().trim().min(1).max(1200),
998
+ citations: z.array(projectContextCitationSchema).default([])
999
+ });
1000
+ var implementationVerificationCheckSchema = z.object({
1001
+ name: z.string().trim().min(1).max(200),
1002
+ status: implementationVerificationCheckStatusSchema,
1003
+ summary: z.string().trim().min(1).max(1200),
1004
+ safePaths: z.array(projectContextRepoPathSchema).default([])
1005
+ });
1006
+ var implementationVerificationResultSchema = z.object({
1007
+ outcome: implementationVerificationOutcomeSchema,
1008
+ summary: z.string().trim().min(1).max(2e3),
1009
+ evidence: z.array(implementationVerificationEvidenceSchema).min(1),
1010
+ checks: z.array(implementationVerificationCheckSchema).default([]),
1011
+ gaps: z.array(z.string().trim().min(1).max(600)).default([]),
1012
+ branch: z.string().trim().min(1).max(200).optional(),
1013
+ pullRequestUrl: z.string().trim().url().max(500).optional(),
1014
+ worktreeKey: z.string().trim().min(1).max(300).optional(),
1015
+ recommendation: implementationVerificationRecommendationSchema.default("none"),
1016
+ verificationPlan: z.array(z.string().trim().min(1).max(300)).default([]),
1017
+ warnings: z.array(z.string().trim().min(1).max(600)).default([])
1018
+ });
1019
+ var implementationVerificationItemSchema = baseItemSchema.extend({
1020
+ type: z.literal("implementationVerification"),
1021
+ projectId: z.string().min(1),
1022
+ implementationVerificationId: z.string().min(1),
1023
+ status: implementationVerificationStatusSchema,
1024
+ source: z.enum(["manual", "runner", "appEvaluation", "planCleanup", "system"]).default("manual"),
1025
+ planDocumentId: z.string().min(1).optional(),
1026
+ planDocumentRevision: z.number().int().nonnegative().optional(),
1027
+ sourceWorkItemId: z.string().min(1).optional(),
1028
+ verificationWorkItemId: z.string().min(1).optional(),
1029
+ repositoryLinkId: z.string().min(1).optional(),
1030
+ runnerId: z.string().min(1).optional(),
1031
+ sourceRevision: z.string().trim().min(1).max(160).optional(),
1032
+ brainRevision: z.number().int().nonnegative().optional(),
1033
+ contextPackId: z.string().min(1).optional(),
1034
+ outcome: implementationVerificationOutcomeSchema.optional(),
1035
+ result: implementationVerificationResultSchema.optional(),
1036
+ summary: z.string().trim().min(1).max(2e3).optional(),
1037
+ startedAt: isoDateTimeSchema.optional(),
1038
+ completedAt: isoDateTimeSchema.optional(),
1039
+ failedAt: isoDateTimeSchema.optional(),
1040
+ overriddenByUserId: z.string().min(1).optional(),
1041
+ overrideReason: z.string().trim().min(1).max(1e3).optional(),
1042
+ overriddenAt: isoDateTimeSchema.optional(),
1043
+ error: z.string().max(1e3).optional()
1044
+ });
968
1045
  var contextMissItemSchema = baseItemSchema.extend({
969
1046
  type: z.literal("contextMiss"),
970
1047
  projectId: z.string().min(1),
@@ -1275,6 +1352,7 @@ var activityEventItemSchema = baseItemSchema.extend({
1275
1352
  relatedSecurityFindingId: z.string().min(1).optional(),
1276
1353
  relatedAppEvaluationScanId: z.string().min(1).optional(),
1277
1354
  relatedAppEvaluationFindingId: z.string().min(1).optional(),
1355
+ relatedImplementationVerificationId: z.string().min(1).optional(),
1278
1356
  relatedProjectContextRefreshId: z.string().min(1).optional(),
1279
1357
  relatedProjectSystemDiagramId: z.string().min(1).optional(),
1280
1358
  relatedContextMissId: z.string().min(1).optional(),
@@ -1340,6 +1418,7 @@ var projectItemUnionSchema = z.discriminatedUnion("type", [
1340
1418
  securityPostureSnapshotItemSchema,
1341
1419
  appEvaluationScanItemSchema,
1342
1420
  appEvaluationFindingItemSchema,
1421
+ implementationVerificationItemSchema,
1343
1422
  projectContextMapItemSchema,
1344
1423
  projectContextRefreshItemSchema,
1345
1424
  projectSystemDiagramItemSchema,
@@ -1583,6 +1662,73 @@ function decideSyncState(state) {
1583
1662
  }
1584
1663
 
1585
1664
  // ../shared/src/next-action.ts
1665
+ var workFailureGuidanceCodes = ["unsafe_context_path", "unsafe_context_result", "invalid_context_result", "context_map_too_large", "context_map_store_failed"];
1666
+ var projectContextFailureGuidance = {
1667
+ unsafe_context_path: {
1668
+ code: "unsafe_context_path",
1669
+ title: "Attention needed",
1670
+ message: "Project context refresh was rejected because a returned path looked unsafe.",
1671
+ steps: [
1672
+ "Deploy the latest web/API fix and update the local runner if this started after a path-safety release.",
1673
+ "Retry the project context refresh.",
1674
+ "If it fails again, capture the failing repo-relative path or bounded runner output without sharing secrets."
1675
+ ]
1676
+ },
1677
+ unsafe_context_result: {
1678
+ code: "unsafe_context_result",
1679
+ title: "Attention needed",
1680
+ message: "Project context refresh was rejected because the result looked like it contained unsafe evidence.",
1681
+ steps: [
1682
+ "Review the local runner output for accidental secret-looking evidence without pasting secrets into Amistio.",
1683
+ "Retry the project context refresh after removing or narrowing unsafe evidence.",
1684
+ "If it repeats, capture a bounded redacted summary of the runner output."
1685
+ ]
1686
+ },
1687
+ invalid_context_result: {
1688
+ code: "invalid_context_result",
1689
+ title: "Attention needed",
1690
+ message: "Project context refresh returned structured data the server could not accept.",
1691
+ steps: [
1692
+ "Update and restart the local runner so it uses the latest context refresh schema.",
1693
+ "Retry the project context refresh.",
1694
+ "If it fails again, capture the bounded marker-delimited result without source dumps or secrets."
1695
+ ]
1696
+ },
1697
+ context_map_too_large: {
1698
+ code: "context_map_too_large",
1699
+ title: "Attention needed",
1700
+ message: "Project context refresh produced a map too large to store safely.",
1701
+ steps: [
1702
+ "Retry the refresh after narrowing the context scope or waiting for smaller incremental refresh support.",
1703
+ "Keep raw source and large dumps out of the context result.",
1704
+ "If this repeats on normal output, split the context-map storage model before retrying large repositories."
1705
+ ]
1706
+ },
1707
+ context_map_store_failed: {
1708
+ code: "context_map_store_failed",
1709
+ title: "Attention needed",
1710
+ message: "Project context refresh could not be stored safely after validation.",
1711
+ steps: [
1712
+ "Retry the project context refresh once.",
1713
+ "If it repeats, check the web/API storage logs for the safe rejection reason.",
1714
+ "Keep the runner output bounded and avoid sharing raw source or secrets."
1715
+ ]
1716
+ }
1717
+ };
1718
+ function getWorkFailureGuidance(input) {
1719
+ const normalizedMessage = (input.message ?? "").toLowerCase();
1720
+ const code = workFailureGuidanceCodes.find((candidate) => normalizedMessage.includes(candidate));
1721
+ if (!code) {
1722
+ return void 0;
1723
+ }
1724
+ if (input.workKind && input.workKind !== "projectContextRefresh" && !normalizedMessage.includes("project context")) {
1725
+ return void 0;
1726
+ }
1727
+ return projectContextFailureGuidance[code];
1728
+ }
1729
+ function formatWorkFailureGuidanceDetail(guidance) {
1730
+ return `Next steps: ${guidance.steps.join(" ")}`;
1731
+ }
1586
1732
  var nextActionRunnerHeartbeatFreshMs = 15 * 60 * 1e3;
1587
1733
  var nextActionCompletedWorkFreshMs = 60 * 60 * 1e3;
1588
1734
  function computeProjectNextAction(input) {
@@ -1647,11 +1793,11 @@ function computeProjectNextAction(input) {
1647
1793
  }
1648
1794
  const failedBrainGeneration = latestWorkItem(input.workItems.filter((item) => item.workKind === "brainGeneration" && item.status === "failed"));
1649
1795
  if (failedBrainGeneration) {
1650
- return workAction(failedBrainGeneration, "brainGenerationFailed", "user", "danger", "Brain generation failed", failedBrainGeneration.lastStatusMessage ?? "Retry generation after checking the runner output.");
1796
+ return failedWorkAction(failedBrainGeneration, "brainGenerationFailed", "Brain generation failed", failedBrainGeneration.lastStatusMessage ?? "Retry generation after checking the runner output.");
1651
1797
  }
1652
1798
  const failedPlanRevision = latestWorkItem(input.workItems.filter((item) => item.workKind === "planRevision" && item.status === "failed"));
1653
1799
  if (failedPlanRevision) {
1654
- return workAction(failedPlanRevision, "planRevisionFailed", "user", "danger", "Plan revision failed", failedPlanRevision.lastStatusMessage ?? "Review the conversation and request another revision if needed.");
1800
+ return failedWorkAction(failedPlanRevision, "planRevisionFailed", "Plan revision failed", failedPlanRevision.lastStatusMessage ?? "Review the conversation and request another revision if needed.");
1655
1801
  }
1656
1802
  const blockedWork = latestWorkItem(input.workItems.filter((item) => item.status === "blocked" || item.status === "changesRequested"));
1657
1803
  if (blockedWork) {
@@ -1659,7 +1805,7 @@ function computeProjectNextAction(input) {
1659
1805
  }
1660
1806
  const failedWork = latestWorkItem(input.workItems.filter((item) => item.status === "failed"));
1661
1807
  if (failedWork) {
1662
- return workAction(failedWork, "workFailed", "user", "danger", "Work failed", failedWork.lastStatusMessage ?? "Review the failure and retry or update the plan.");
1808
+ return failedWorkAction(failedWork, "workFailed", "Work failed", failedWork.lastStatusMessage ?? "Review the failure and retry or update the plan.");
1663
1809
  }
1664
1810
  const queuedBrainGeneration = latestWorkItem(input.workItems.filter((item) => item.workKind === "brainGeneration" && item.status === "approved"));
1665
1811
  const queuedPlanRevision = latestWorkItem(input.workItems.filter((item) => item.workKind === "planRevision" && item.status === "approved"));
@@ -1698,7 +1844,7 @@ function computeProjectNextAction(input) {
1698
1844
  };
1699
1845
  }
1700
1846
  function formatProjectNextAction(action) {
1701
- return `${action.title}: ${action.message}`;
1847
+ return `${action.title}: ${action.message}${action.detail ? ` ${action.detail}` : ""}`;
1702
1848
  }
1703
1849
  function runnerWaitAction(readiness, fallbackTitle) {
1704
1850
  const repositoryName = readiness.repositoryLink?.repoName ?? "the paired repository";
@@ -1761,13 +1907,21 @@ function isGeneratedDocument(document) {
1761
1907
  const generatedDraftId = document.frontmatter.generatedDraftId;
1762
1908
  return typeof generatedDraftId === "string" && generatedDraftId.length > 0;
1763
1909
  }
1764
- function workAction(workItem, kind, actor, tone, title, message) {
1910
+ function failedWorkAction(workItem, kind, fallbackTitle, fallbackMessage) {
1911
+ const guidance = getWorkFailureGuidance({ message: workItem.lastStatusMessage, workKind: workItem.workKind });
1912
+ if (guidance) {
1913
+ return workAction(workItem, kind, "user", "danger", guidance.title, guidance.message, formatWorkFailureGuidanceDetail(guidance));
1914
+ }
1915
+ return workAction(workItem, kind, "user", "danger", fallbackTitle, fallbackMessage);
1916
+ }
1917
+ function workAction(workItem, kind, actor, tone, title, message, detail) {
1765
1918
  return {
1766
1919
  kind,
1767
1920
  actor,
1768
1921
  tone,
1769
1922
  title,
1770
1923
  message,
1924
+ ...detail ? { detail } : {},
1771
1925
  workItemId: workItem.workItemId,
1772
1926
  ...workItem.claimedByRunnerId ? { runnerId: workItem.claimedByRunnerId } : {},
1773
1927
  updatedAt: workItem.lastStatusAt
@@ -2339,6 +2493,16 @@ var ApiClient = class {
2339
2493
  }
2340
2494
  );
2341
2495
  }
2496
+ async submitImplementationVerificationResult(projectId, workItemId, result) {
2497
+ return this.request(
2498
+ `/projects/${projectId}/work-items/${workItemId}/implementation-verification-result`,
2499
+ z3.object({ verification: implementationVerificationItemSchema, workItem: workItemSchema, sourceWorkItem: workItemSchema.optional() }),
2500
+ {
2501
+ method: "POST",
2502
+ body: JSON.stringify(result)
2503
+ }
2504
+ );
2505
+ }
2342
2506
  async request(urlPath, schema, init) {
2343
2507
  const response = await fetch(resolveApiUrl(this.options.apiUrl, urlPath), {
2344
2508
  ...init,
@@ -4392,9 +4556,60 @@ var appEvaluationStart = "AMISTIO_APP_EVALUATION_START";
4392
4556
  var appEvaluationEnd = "AMISTIO_APP_EVALUATION_END";
4393
4557
  var projectContextRefreshStart = "AMISTIO_PROJECT_CONTEXT_REFRESH_START";
4394
4558
  var projectContextRefreshEnd = "AMISTIO_PROJECT_CONTEXT_REFRESH_END";
4559
+ var implementationVerificationStart = "AMISTIO_IMPLEMENTATION_VERIFICATION_START";
4560
+ var implementationVerificationEnd = "AMISTIO_IMPLEMENTATION_VERIFICATION_END";
4395
4561
  var validProjectContextSliceKinds = /* @__PURE__ */ new Set(["overview", "architecture", "domain", "data", "api", "frontend", "backend", "cli", "workflow", "operations", "security", "testing", "unknown"]);
4396
4562
  var validProjectContextEntityTypes = /* @__PURE__ */ new Set(["project", "system", "component", "domain", "tool", "decision", "feature", "risk", "team", "workflow", "unknown"]);
4397
4563
  var validProjectContextRelationTypes = /* @__PURE__ */ new Set(["uses", "depends_on", "decides", "supersedes", "touches", "blocks", "implements", "mentions"]);
4564
+ function createImplementationVerificationPrompt(workItem) {
4565
+ return [
4566
+ "# Amistio Implementation Verification",
4567
+ "",
4568
+ "You are running locally through the Amistio CLI inside the user's repository.",
4569
+ "Verify whether the linked implementation work was actually completed. This is a read-only proof pass.",
4570
+ "Do not modify files, create branches, commit, install packages, run implementation prompts, or make destructive changes.",
4571
+ "Use local repository inspection and focused verification commands only when they are safe for this checkout.",
4572
+ "",
4573
+ "## Work Item",
4574
+ "",
4575
+ `Title: ${workItem.title}`,
4576
+ `Work item ID: ${workItem.workItemId}`,
4577
+ `Project ID: ${workItem.projectId}`,
4578
+ `Implementation verification ID: ${workItem.implementationVerificationId ?? "unknown"}`,
4579
+ `Source work item ID: ${workItem.sourceWorkItemId ?? "unknown"}`,
4580
+ "",
4581
+ "## Verification Request",
4582
+ "",
4583
+ workItem.sourceWish ?? "Verify that the linked implementation plan was completed in the local repository.",
4584
+ "",
4585
+ "## Verification Requirements",
4586
+ "",
4587
+ "- Compare the approved plan or prompt requirements against the current local repository state.",
4588
+ "- Look beyond runner status. Use source evidence, generated handoff details, PR/branch/worktree metadata when present, and targeted checks.",
4589
+ "- Treat missing acceptance criteria as an explicit gap instead of guessing.",
4590
+ "- Record evidence for at least one acceptance criterion with safe repository-relative citations.",
4591
+ "- Use outcome verifiedImplemented only when the implementation is materially present and checks support it.",
4592
+ "- Use partiallyImplemented, notImplemented, inconclusive, or verificationBlocked when evidence is incomplete, absent, contradictory, or checks cannot run.",
4593
+ "",
4594
+ "## Data Safety",
4595
+ "",
4596
+ "- Persist summaries, safe paths, short citation excerpts, and verification status only.",
4597
+ "- Do not include raw source dumps, secrets, env vars, process lists, absolute local paths, credential values, tokens, provider sessions, or destructive shell output.",
4598
+ "- Citations and safePaths must be repository-relative and must not use ../ traversal.",
4599
+ "",
4600
+ "## Output Contract",
4601
+ "",
4602
+ "Print exactly one JSON object between the markers below. The CLI will submit only this structured verification result back to Amistio.",
4603
+ "Accepted outcome values: verifiedImplemented, partiallyImplemented, notImplemented, inconclusive, verificationBlocked.",
4604
+ "Accepted recommendation values: none, requeue, createFollowUpPlan, requestHumanReview, markBlocked.",
4605
+ "",
4606
+ implementationVerificationStart,
4607
+ '{"outcome":"partiallyImplemented","summary":"The implementation is present in the workspace, but one acceptance check could not be confirmed.","evidence":[{"acceptanceCriterion":"The requested behavior is wired through the runner lifecycle.","status":"partial","summary":"The route and UI wiring exist, but the runner result submission still needs validation.","citations":[{"source":"localSource","repoPath":"src/apps/web/components/workspace-client.tsx","excerpt":"Implementation verification queued for the local runner."}]}],"checks":[{"name":"Focused typecheck","status":"passed","summary":"The touched package typechecked successfully.","safePaths":["src/apps/web"]}],"gaps":["Runner result submission was not exercised end to end."],"recommendation":"requestHumanReview","verificationPlan":["Run focused CLI tests","Review the verification evidence in Amistio"],"warnings":[]}',
4608
+ implementationVerificationEnd,
4609
+ "",
4610
+ "Do not put Markdown fences around the markers. Do not implement or fix anything during this verification pass."
4611
+ ].join("\n");
4612
+ }
4398
4613
  function createWorkExecutionPrompt(workItem, context) {
4399
4614
  if (workItem.workKind === "brainGeneration") {
4400
4615
  return createBrainGenerationPrompt(workItem);
@@ -4420,6 +4635,9 @@ function createWorkExecutionPrompt(workItem, context) {
4420
4635
  if (workItem.workKind === "projectContextRefresh") {
4421
4636
  return createProjectContextRefreshPrompt(workItem, context?.projectContextRefresh);
4422
4637
  }
4638
+ if (workItem.workKind === "implementationVerification") {
4639
+ return createImplementationVerificationPrompt(workItem);
4640
+ }
4423
4641
  return [
4424
4642
  "# Amistio Work Execution",
4425
4643
  "",
@@ -4570,6 +4788,7 @@ function createSecurityPostureScanPrompt(workItem, context) {
4570
4788
  "## Output Contract",
4571
4789
  "",
4572
4790
  "Print exactly one JSON object between the markers below. The CLI will submit only this structured scan result back to Amistio.",
4791
+ "Accepted category values: owasp, dependency, supplyChain, secretHygiene, authentication, authorization, tenantIsolation, headers, redirects, csrf, cors, ciCd, logging, rateLimiting, runnerBoundary, other.",
4573
4792
  "",
4574
4793
  securityPostureStart,
4575
4794
  '{"summary":"Security posture is mostly healthy, but dependency advisory review is not automated.","baselineVersion":"amistio-security-baseline-v1","postureScore":82,"postureGrade":"B","categorySummaries":[{"category":"dependency","status":"warning","summary":"Dependency audit automation is incomplete."}],"findings":[{"title":"Dependency audit is not enforced in CI","category":"dependency","severity":"medium","confidence":"high","summary":"The repository has a lockfile, but CI does not appear to fail on known vulnerable dependencies.","standardReferences":[{"standard":"OWASP ASVS","control":"V14.2 Dependency"}],"affectedSurfaces":["CI verification"],"evidence":["No dependency audit step was found in the CI verification summary."],"recommendedRemediation":"Add a dependency advisory check to the existing verification pipeline and document how failures are handled.","verificationPlan":["Run the updated CI verification command locally","Confirm a known advisory fails the check in a controlled test"],"safePaths":["package.json","pnpm-lock.yaml"]}],"verificationPlan":["Run focused dependency and CI checks","Review Security panel findings for approval"]}',
@@ -4970,11 +5189,23 @@ function parseProjectContextRefreshResult(output, options = {}) {
4970
5189
  const normalized = normalizeProjectContextRefreshPaths(normalizeProjectContextRefreshEnums(parsed), options);
4971
5190
  return projectContextRefreshResultSchema.parse(normalized);
4972
5191
  }
5192
+ function parseImplementationVerificationResult(output) {
5193
+ const start = output.indexOf(implementationVerificationStart);
5194
+ const end = output.indexOf(implementationVerificationEnd, start + implementationVerificationStart.length);
5195
+ if (start === -1 || end === -1 || end <= start) {
5196
+ throw new Error("Local AI verification did not return an Amistio implementation verification block.");
5197
+ }
5198
+ const payload = output.slice(start + implementationVerificationStart.length, end).trim();
5199
+ const parsed = JSON.parse(stripJsonFence(payload));
5200
+ return implementationVerificationResultSchema.parse(parsed);
5201
+ }
4973
5202
  function projectContextRefreshSubmissionFailureSummary(result) {
4974
5203
  if (result.refresh.status !== "failed" && result.workItem.status !== "failed") {
4975
5204
  return void 0;
4976
5205
  }
4977
- return result.refresh.error ?? result.workItem.lastStatusMessage ?? "Server rejected the project context refresh result.";
5206
+ const summary = result.refresh.error ?? result.workItem.lastStatusMessage ?? "Server rejected the project context refresh result.";
5207
+ const guidance = getWorkFailureGuidance({ message: summary, workKind: "projectContextRefresh" });
5208
+ return guidance ? `${summary} ${formatWorkFailureGuidanceDetail(guidance)}` : summary;
4978
5209
  }
4979
5210
  function createBrainGenerationPrompt(workItem) {
4980
5211
  const wish = workItem.sourceWish ?? workItem.title;
@@ -6041,7 +6272,7 @@ var DEFAULT_MAX_PREFLIGHT_ATTEMPTS = 3;
6041
6272
  var DEFAULT_TOOL_TIMEOUT_SECONDS = 30 * 60;
6042
6273
  var RUNNER_WORK_LEASE_SECONDS = 300;
6043
6274
  var RUNNER_WORK_LEASE_RENEWAL_MS = 12e4;
6044
- var runnerSupportedWorkKinds = ["brainGeneration", "implementation", "planRevision", "assistantQuestion", "impactPreview", "issueDiagnosis", "securityPostureScan", "appEvaluationScan", "projectContextRefresh"];
6275
+ var runnerSupportedWorkKinds = ["brainGeneration", "implementation", "planRevision", "assistantQuestion", "impactPreview", "issueDiagnosis", "securityPostureScan", "appEvaluationScan", "projectContextRefresh", "implementationVerification"];
6045
6276
  program.name("amistio").description("Amistio project brain CLI").version(CLI_VERSION);
6046
6277
  program.command("init").description("Create Amistio control-plane folders for a new project").option("--root <path>", "Repository root", defaultRoot).action(async (options) => {
6047
6278
  const created = await initControlPlane(options.root);
@@ -7077,6 +7308,24 @@ async function runNextWorkItem({
7077
7308
  return recordFinalizationFailure({ apiClient, error, isolationTelemetry, projectId, repositoryLinkId, runnerId, sessionContext, toolConfig, toolName: preview.toolName, workItem: result.workItem, durationMs: Date.now() - startedAt });
7078
7309
  }
7079
7310
  }
7311
+ if (result.workItem.workKind === "implementationVerification") {
7312
+ try {
7313
+ return await finalizeImplementationVerificationWork({
7314
+ apiClient,
7315
+ durationMs: Date.now() - startedAt,
7316
+ projectId,
7317
+ repositoryLinkId,
7318
+ runnerId,
7319
+ sessionContext,
7320
+ toolConfig,
7321
+ toolName: preview.toolName,
7322
+ toolResult,
7323
+ workItem: result.workItem
7324
+ });
7325
+ } catch (error) {
7326
+ return recordFinalizationFailure({ apiClient, error, isolationTelemetry, projectId, repositoryLinkId, runnerId, sessionContext, toolConfig, toolName: preview.toolName, workItem: result.workItem, durationMs: Date.now() - startedAt });
7327
+ }
7328
+ }
7080
7329
  let finalStatus = toolResult.exitCode === 0 ? "completed" : "failed";
7081
7330
  const durationMs = Date.now() - startedAt;
7082
7331
  const failureExcerpt = toolResult.exitCode === 0 ? void 0 : truncateLogExcerpt(toolResult.stderr || toolResult.stdout);
@@ -8014,6 +8263,92 @@ ${toolResult.stderr}`, { repositoryRoot: executionRoot });
8014
8263
  console.error(refreshError ?? "Local runner project context refresh failed.");
8015
8264
  return { status: "failed", exitCode: toolResult.exitCode || 1 };
8016
8265
  }
8266
+ async function finalizeImplementationVerificationWork({
8267
+ apiClient,
8268
+ durationMs,
8269
+ projectId,
8270
+ repositoryLinkId,
8271
+ runnerId,
8272
+ sessionContext,
8273
+ toolConfig,
8274
+ toolName,
8275
+ toolResult,
8276
+ workItem
8277
+ }) {
8278
+ let verificationResult = void 0;
8279
+ let verificationError;
8280
+ if (toolResult.exitCode === 0) {
8281
+ try {
8282
+ verificationResult = parseImplementationVerificationResult(`${toolResult.stdout}
8283
+ ${toolResult.stderr}`);
8284
+ } catch (error) {
8285
+ verificationError = errorMessage3(error);
8286
+ }
8287
+ } else {
8288
+ verificationError = truncateLogExcerpt(toolResult.stderr || toolResult.stdout) || `${toolName} exited with code ${toolResult.exitCode}.`;
8289
+ }
8290
+ const finalStatus = verificationResult ? "completed" : "failed";
8291
+ const updatedToolSession = await finalizeToolSession({
8292
+ apiClient,
8293
+ projectId,
8294
+ status: finalStatus,
8295
+ runnerId,
8296
+ workItemId: workItem.workItemId,
8297
+ stdout: toolResult.stdout,
8298
+ ...sessionContext.toolSession ? { session: sessionContext.toolSession } : {},
8299
+ ...toolResult.messageCount !== void 0 ? { messageCount: toolResult.messageCount } : {},
8300
+ ...toolResult.tokensIn !== void 0 ? { tokensIn: toolResult.tokensIn } : {},
8301
+ ...toolResult.tokensOut !== void 0 ? { tokensOut: toolResult.tokensOut } : {},
8302
+ ...toolResult.costUsd !== void 0 ? { costUsd: toolResult.costUsd } : {}
8303
+ });
8304
+ const sessionTelemetry = {
8305
+ sessionPolicy: sessionContext.policy,
8306
+ sessionDecision: sessionContext.decision,
8307
+ sessionDecisionReason: sessionContext.reason,
8308
+ ...updatedToolSession ? { toolSessionId: updatedToolSession.toolSessionId } : {},
8309
+ ...updatedToolSession?.sessionGroupKey ? { sessionGroupKey: updatedToolSession.sessionGroupKey } : {}
8310
+ };
8311
+ if (verificationResult) {
8312
+ const result = await apiClient.submitImplementationVerificationResult(projectId, workItem.workItemId, {
8313
+ status: "completed",
8314
+ runnerId,
8315
+ idempotencyKey: `implementation_verification_${workItem.workItemId}_${randomUUID()}`,
8316
+ result: verificationResult,
8317
+ tool: toolName,
8318
+ durationMs,
8319
+ ...sessionTelemetry,
8320
+ message: `${toolName} returned implementation verification proof.`
8321
+ });
8322
+ await recordRunnerMilestone(apiClient, projectId, workItem, runnerId, repositoryLinkId, {
8323
+ status: verificationResult.outcome === "verifiedImplemented" ? "completed" : verificationResult.outcome === "verificationBlocked" ? "blocked" : "warning",
8324
+ summary: verificationResult.summary,
8325
+ idempotencyKey: `runner_milestone_implementation_verification_completed_${workItem.workItemId}_${result.workItem.idempotencyKey}`,
8326
+ metadata: { tool: toolName, durationMs, outcome: verificationResult.outcome, recommendation: verificationResult.recommendation, gapCount: verificationResult.gaps.length, verificationSummary: verificationResult.verificationPlan.join(" | ") }
8327
+ });
8328
+ await apiClient.sendRunnerHeartbeat(projectId, runnerId, repositoryLinkId, "online", runnerHeartbeatMetadata(toolConfig));
8329
+ console.log("Implementation verification returned for review.");
8330
+ return { status: "completed", exitCode: 0 };
8331
+ }
8332
+ const failedResult = await apiClient.submitImplementationVerificationResult(projectId, workItem.workItemId, {
8333
+ status: "failed",
8334
+ runnerId,
8335
+ idempotencyKey: `implementation_verification_${workItem.workItemId}_${randomUUID()}`,
8336
+ tool: toolName,
8337
+ durationMs,
8338
+ ...sessionTelemetry,
8339
+ message: `${toolName} did not produce valid implementation verification proof.`,
8340
+ ...verificationError ? { error: verificationError } : {}
8341
+ });
8342
+ await recordRunnerMilestone(apiClient, projectId, workItem, runnerId, repositoryLinkId, {
8343
+ status: "failed",
8344
+ summary: verificationError ?? `${toolName} did not produce valid implementation verification proof.`,
8345
+ idempotencyKey: `runner_milestone_implementation_verification_failed_${workItem.workItemId}_${failedResult.workItem.idempotencyKey}`,
8346
+ metadata: { tool: toolName, durationMs, verificationSummary: "Implementation verification output did not include valid structured JSON." }
8347
+ });
8348
+ await apiClient.sendRunnerHeartbeat(projectId, runnerId, repositoryLinkId, "online", runnerHeartbeatMetadata(toolConfig));
8349
+ console.error(verificationError ?? "Local runner implementation verification failed.");
8350
+ return { status: "failed", exitCode: toolResult.exitCode || 1 };
8351
+ }
8017
8352
  async function createRunnerWorkPrompt(apiClient, projectId, workItem) {
8018
8353
  if (workItem.workKind === "assistantQuestion") {
8019
8354
  const emptyProjectContext = { maps: [], refreshes: [], misses: [] };