@amistio/cli 0.1.45 → 0.1.47

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.
Files changed (3) hide show
  1. package/README.md +1 -1
  2. package/dist/index.js +116 -4
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -84,7 +84,7 @@ When `--tool codex` uses the Codex SDK, intermediate progress can be quiet until
84
84
 
85
85
  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.
86
86
 
87
- 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. Before local AI/tool execution starts, implementation work checks PR handoff readiness: GitHub remote support, default-branch fetch, Git commit identity, local `gh` authentication, and repository visibility. 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, and runner-created no-change worktrees are removed after final clean checks. 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 with safe recovery choices; source files and patches are not uploaded to Amistio. The Work panel can queue scoped Retry handoff or Retry cleanup commands only to the runner that owns the preserved worktree for the same work item, branch, and worktree key. Retry handoff can publish a clean preserved local-only implementation commit without rerunning the implementation prompt. Rebase conflicts capture bounded repo-relative conflict files and try `git rebase --abort` so the implementation branch can be retried or manually reviewed without leaving an active rebase. Dirty, unmerged, or ambiguous worktrees are preserved rather than discarded.
87
+ 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. Before local AI/tool execution starts, implementation work checks PR handoff readiness: GitHub remote support, default-branch fetch, Git commit identity, local `gh` authentication, and repository visibility. 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, then classifies the final diff. Source-implementation work must include source, config, test, or other implementation-affecting changes before the runner opens or reuses a PR. If only project-brain or documentation artifacts changed, Amistio reports no implementation produced and preserves recovery choices instead of opening a misleading implementation PR. Explicit docs-only work can still create docs-only PRs. No-change completion requires no source changes and no approved artifact changes, and runner-created no-change worktrees are removed after final clean checks. 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 with safe recovery choices; source files and patches are not uploaded to Amistio. The Work panel can queue scoped Retry handoff or Retry cleanup commands only to the runner that owns the preserved worktree for the same work item, branch, and worktree key. Retry handoff can publish a clean preserved local-only implementation commit without rerunning the implementation prompt. Rebase conflicts capture bounded repo-relative conflict files and try `git rebase --abort` so the implementation branch can be retried or manually reviewed without leaving an active rebase. Dirty, unmerged, or ambiguous worktrees are preserved rather than discarded.
88
88
 
89
89
  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; Requeue all sends one backend batch that recomputes safe candidates, reports already-active and skipped rows, and still uses linked attempts. Requeue 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.
90
90
 
package/dist/index.js CHANGED
@@ -63,6 +63,14 @@ var documentTypeSchema = z.enum([
63
63
  ]);
64
64
  var documentContentFormatSchema = z.enum(["markdown", "html"]);
65
65
  var artifactFormatPreferenceSchema = z.enum(["markdown", "html", "both", "auto"]);
66
+ var githubPullRequestUrlSchema = z.string().trim().url().max(500).refine((value) => {
67
+ try {
68
+ const url = new URL(value);
69
+ return url.protocol === "https:" && url.hostname === "github.com" && /^\/[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+\/pull\/[1-9]\d*\/?$/.test(url.pathname);
70
+ } catch {
71
+ return false;
72
+ }
73
+ }, "Pull request URL must be an HTTPS github.com pull request URL.");
66
74
  var syncStateSchema = z.enum([
67
75
  "draft",
68
76
  "approved",
@@ -146,7 +154,8 @@ var autopilotGuardCheckSchema = z.object({
146
154
  status: z.enum(["passed", "failed", "warning"]),
147
155
  summary: z.string().trim().min(1).max(600)
148
156
  });
149
- var implementationHandoffStatusSchema = z.enum(["notStarted", "noChanges", "prReady", "blocked", "failed"]);
157
+ var implementationExpectedOutcomeSchema = z.enum(["sourceImplementation", "docsOnly", "analysisOnly", "verificationOnly"]);
158
+ var implementationHandoffStatusSchema = z.enum(["notStarted", "noChanges", "prReady", "blocked", "failed", "noImplementationProduced"]);
150
159
  var implementationHandoffCleanupStatusSchema = z.enum(["notApplicable", "pending", "completed", "failed"]);
151
160
  var implementationHandoffRecoveryCategorySchema = z.enum(["noChangeCleaned", "rebaseConflict", "dirtyWorktree", "unresolvedConflicts", "providerBlocked", "cleanupRetryAvailable", "manualReview", "artifactBlocked", "nonGithubProvider"]);
152
161
  var implementationHandoffRecoveryActionSchema = z.enum(["retryHandoff", "requeueFreshAttempt", "keepForManualRepair", "cleanNoChangeWorktree", "markSupersededNoChanges", "exportHandoffDetails", "retryCleanup"]);
@@ -215,15 +224,25 @@ var implementationHandoffRecoverySchema = z.object({
215
224
  var implementationVerificationOutcomeSchema = z.enum(["verifiedImplemented", "partiallyImplemented", "notImplemented", "inconclusive", "verificationBlocked"]);
216
225
  var implementationVerificationStatusSchema = z.enum(["queued", "running", "completed", "failed", "blocked", "stale"]);
217
226
  var implementationProofStatusSchema = z.enum(["unverified", "verificationQueued", "verificationRunning", "verifiedImplemented", "partiallyImplemented", "notImplemented", "inconclusive", "verificationBlocked", "humanOverride"]);
227
+ var implementationHandoffDiffClassificationSchema = z.enum(["noChanges", "sourceImplementation", "sourceAndArtifacts", "projectBrainOnly", "docsOnly"]);
228
+ var implementationHandoffDiffSummarySchema = z.object({
229
+ classification: implementationHandoffDiffClassificationSchema,
230
+ sourceImplementationFileCount: z.number().int().nonnegative().default(0),
231
+ projectBrainArtifactFileCount: z.number().int().nonnegative().default(0),
232
+ documentationFileCount: z.number().int().nonnegative().default(0),
233
+ fileSamples: z.array(safeRepoPathSchema).max(25).default([])
234
+ }).strict();
218
235
  var implementationHandoffSchema = z.object({
219
236
  provider: z.string().trim().min(1).max(80).optional(),
220
237
  status: implementationHandoffStatusSchema,
238
+ expectedOutcome: implementationExpectedOutcomeSchema.optional(),
239
+ diffSummary: implementationHandoffDiffSummarySchema.optional(),
221
240
  baseBranch: z.string().trim().min(1).max(200).optional(),
222
241
  headBranch: z.string().trim().min(1).max(200).optional(),
223
242
  remoteName: z.string().trim().min(1).max(80).optional(),
224
243
  commitSha: z.string().trim().min(7).max(64).optional(),
225
244
  prNumber: z.number().int().positive().optional(),
226
- prUrl: z.string().trim().url().max(500).optional(),
245
+ prUrl: githubPullRequestUrlSchema.optional(),
227
246
  cleanupStatus: implementationHandoffCleanupStatusSchema.optional(),
228
247
  cleanupMessage: z.string().trim().min(1).max(600).optional(),
229
248
  artifacts: implementationHandoffArtifactsSchema.optional(),
@@ -282,7 +301,7 @@ var autopilotCandidateLinksSchema = z.object({
282
301
  runnerId: z.string().min(1).optional(),
283
302
  branchName: z.string().trim().min(1).max(200).optional(),
284
303
  worktreeKey: z.string().trim().min(1).max(300).optional(),
285
- pullRequestUrl: z.string().trim().url().max(500).optional()
304
+ pullRequestUrl: githubPullRequestUrlSchema.optional()
286
305
  }).strict();
287
306
  var autopilotCandidateActionSchema = z.object({
288
307
  candidateId: z.string().trim().min(1).max(160),
@@ -850,6 +869,7 @@ var workItemSchema = baseItemSchema.extend({
850
869
  implementationTestGateId: z.string().min(1).optional(),
851
870
  projectContextRefreshId: z.string().min(1).optional(),
852
871
  contextMissId: z.string().min(1).optional(),
872
+ changeId: z.string().min(1).optional(),
853
873
  sourceWorkItemId: z.string().min(1).optional(),
854
874
  requeueReason: z.string().trim().min(1).max(600).optional(),
855
875
  requeuedByUserId: z.string().min(1).optional(),
@@ -860,6 +880,7 @@ var workItemSchema = baseItemSchema.extend({
860
880
  implementationProofStatus: implementationProofStatusSchema.optional(),
861
881
  implementationVerificationOutcome: implementationVerificationOutcomeSchema.optional(),
862
882
  implementationVerificationResultId: z.string().min(1).optional(),
883
+ implementationExpectedOutcome: implementationExpectedOutcomeSchema.optional(),
863
884
  humanImplementationOverrideReason: z.string().trim().min(1).max(1e3).optional(),
864
885
  repositoryLinkId: z.string().min(1).optional(),
865
886
  reviewThreadId: z.string().min(1).optional(),
@@ -1470,7 +1491,7 @@ var implementationVerificationResultSchema = z.object({
1470
1491
  checks: z.array(implementationVerificationCheckSchema).default([]),
1471
1492
  gaps: z.array(z.string().trim().min(1).max(600)).default([]),
1472
1493
  branch: z.string().trim().min(1).max(200).optional(),
1473
- pullRequestUrl: z.string().trim().url().max(500).optional(),
1494
+ pullRequestUrl: githubPullRequestUrlSchema.optional(),
1474
1495
  worktreeKey: z.string().trim().min(1).max(300).optional(),
1475
1496
  recommendation: implementationVerificationRecommendationSchema.default("none"),
1476
1497
  verificationPlan: z.array(z.string().trim().min(1).max(300)).default([]),
@@ -9688,6 +9709,7 @@ import { execFile as execFile6 } from "node:child_process";
9688
9709
  import path18 from "node:path";
9689
9710
  import { promisify as promisify6 } from "node:util";
9690
9711
  var execFileAsync6 = promisify6(execFile6);
9712
+ var DEFAULT_IMPLEMENTATION_EXPECTED_OUTCOME = "sourceImplementation";
9691
9713
  async function checkImplementationHandoffReadiness(input) {
9692
9714
  const run = input.commandRunner ?? defaultCommandRunner;
9693
9715
  const headBranch = input.worktreeIsolation?.branch ?? input.workItem.executionBranch;
@@ -9768,6 +9790,7 @@ async function completeImplementationHandoff(input) {
9768
9790
  const run = input.commandRunner ?? defaultCommandRunner;
9769
9791
  const headBranch = input.worktreeIsolation?.branch ?? input.workItem.executionBranch;
9770
9792
  const baseBranch = input.baseBranch ?? input.repositoryLink?.defaultBranch ?? "main";
9793
+ const expectedOutcome = input.workItem.implementationExpectedOutcome ?? DEFAULT_IMPLEMENTATION_EXPECTED_OUTCOME;
9771
9794
  if (!headBranch) {
9772
9795
  return blockedHandoff({ baseBranch, message: "Implementation handoff needs an execution branch before it can create a pull request." });
9773
9796
  }
@@ -9802,6 +9825,7 @@ async function completeImplementationHandoff(input) {
9802
9825
  });
9803
9826
  }
9804
9827
  const status = await gitOutput2(run, input.worktreePath, ["status", "--porcelain=v1", "-z", "--untracked-files=all"]);
9828
+ const diffSummary = classifyHandoffDiff(status);
9805
9829
  if (!status) {
9806
9830
  if (input.includeCleanLocalCommits) {
9807
9831
  const cleanBranchHandoff = await completeCleanLocalCommitHandoff({ artifactResult, baseBranch, common, headBranch, input, run });
@@ -9813,6 +9837,8 @@ async function completeImplementationHandoff(input) {
9813
9837
  return {
9814
9838
  ...common,
9815
9839
  status: "noChanges",
9840
+ expectedOutcome,
9841
+ diffSummary,
9816
9842
  cleanupStatus: cleanup2.status,
9817
9843
  ...cleanup2.message ? { cleanupMessage: cleanup2.message } : {},
9818
9844
  artifacts: artifactResult,
@@ -9826,6 +9852,24 @@ async function completeImplementationHandoff(input) {
9826
9852
  message: cleanup2.status === "completed" ? "Local execution completed with no repository changes to hand off and the local worktree was cleaned up." : "Local execution completed with no repository changes to hand off."
9827
9853
  };
9828
9854
  }
9855
+ if (expectedOutcome === "sourceImplementation" && diffSummary.sourceImplementationFileCount === 0) {
9856
+ return {
9857
+ ...common,
9858
+ status: "noImplementationProduced",
9859
+ expectedOutcome,
9860
+ diffSummary,
9861
+ cleanupStatus: "pending",
9862
+ artifacts: artifactResult,
9863
+ recovery: handoffRecovery(input, {
9864
+ category: "manualReview",
9865
+ availableActions: ["requeueFreshAttempt", "keepForManualRepair", "exportHandoffDetails"],
9866
+ cleanupEligible: false,
9867
+ rebaseAbortStatus: "notApplicable",
9868
+ summary: "The runner finished, but the final diff contains only project-brain or documentation artifacts. Requeue implementation or explicitly approve this as docs-only before opening a PR."
9869
+ }),
9870
+ message: "Implementation expected source, config, or test changes, but only documentation or project-brain artifacts changed. No implementation PR was opened."
9871
+ };
9872
+ }
9829
9873
  const remoteName = await resolveRemoteName(run, input.worktreePath);
9830
9874
  const remoteUrl = await gitOutput2(run, input.worktreePath, ["remote", "get-url", remoteName]);
9831
9875
  const provider = input.repositoryLink?.provider ?? inferProvider(remoteUrl);
@@ -9834,6 +9878,8 @@ async function completeImplementationHandoff(input) {
9834
9878
  ...common,
9835
9879
  provider,
9836
9880
  remoteName,
9881
+ expectedOutcome,
9882
+ diffSummary,
9837
9883
  artifacts: artifactResult,
9838
9884
  message: "Automated pull request handoff currently requires a GitHub remote. Commit and push manually, or link a GitHub repository.",
9839
9885
  recovery: handoffRecovery(input, {
@@ -9861,6 +9907,8 @@ async function completeImplementationHandoff(input) {
9861
9907
  return {
9862
9908
  provider: "github",
9863
9909
  status: "prReady",
9910
+ expectedOutcome,
9911
+ diffSummary,
9864
9912
  baseBranch,
9865
9913
  headBranch,
9866
9914
  remoteName,
@@ -10166,6 +10214,70 @@ function blockedHandoff(input) {
10166
10214
  ...input
10167
10215
  };
10168
10216
  }
10217
+ function classifyHandoffDiff(status) {
10218
+ const files = status ? safeRepoPathsFromStatus(status) : [];
10219
+ let sourceImplementationFileCount = 0;
10220
+ let projectBrainArtifactFileCount = 0;
10221
+ let documentationFileCount = 0;
10222
+ for (const file of files) {
10223
+ if (isProjectBrainArtifactPath(file)) {
10224
+ projectBrainArtifactFileCount += 1;
10225
+ } else if (isDocumentationPath(file)) {
10226
+ documentationFileCount += 1;
10227
+ } else {
10228
+ sourceImplementationFileCount += 1;
10229
+ }
10230
+ }
10231
+ const classification = sourceImplementationFileCount > 0 ? projectBrainArtifactFileCount > 0 || documentationFileCount > 0 ? "sourceAndArtifacts" : "sourceImplementation" : projectBrainArtifactFileCount > 0 ? documentationFileCount > 0 ? "docsOnly" : "projectBrainOnly" : documentationFileCount > 0 ? "docsOnly" : "noChanges";
10232
+ return {
10233
+ classification,
10234
+ sourceImplementationFileCount,
10235
+ projectBrainArtifactFileCount,
10236
+ documentationFileCount,
10237
+ fileSamples: files.slice(0, 25)
10238
+ };
10239
+ }
10240
+ function safeRepoPathsFromStatus(status) {
10241
+ const paths = [];
10242
+ const parts = status.split("\0").filter(Boolean);
10243
+ for (let index = 0; index < parts.length; index += 1) {
10244
+ const entry = parts[index];
10245
+ const statusCode = entry.slice(0, 2);
10246
+ const parsedPath = entry.slice(entry[2] === " " ? 3 : 0).trim();
10247
+ if (statusCode.includes("R") || statusCode.includes("C")) {
10248
+ const nextPath = parts[index + 1];
10249
+ if (nextPath && !/^[ MADRCU?!]{2} /.test(nextPath)) {
10250
+ index += 1;
10251
+ addSafePath(paths, nextPath.trim());
10252
+ continue;
10253
+ }
10254
+ }
10255
+ addSafePath(paths, parsedPath);
10256
+ }
10257
+ return [...new Set(paths)].slice(0, 100);
10258
+ }
10259
+ function addSafePath(paths, repoPath) {
10260
+ if (!repoPath || repoPath.startsWith("/") || /^[A-Za-z]:[\\/]/.test(repoPath) || repoPath.split(/[\\/]+/).includes("..")) {
10261
+ return;
10262
+ }
10263
+ paths.push(repoPath);
10264
+ }
10265
+ function isProjectBrainArtifactPath(repoPath) {
10266
+ return [
10267
+ "docs/architecture/",
10268
+ "docs/context/",
10269
+ "docs/decisions/",
10270
+ "docs/features/",
10271
+ "docs/memory/",
10272
+ "docs/plans/",
10273
+ "docs/prompts/",
10274
+ "docs/workflows/"
10275
+ ].some((prefix) => repoPath.startsWith(prefix));
10276
+ }
10277
+ function isDocumentationPath(repoPath) {
10278
+ const basename = repoPath.split("/").pop() ?? repoPath;
10279
+ return /\.(?:md|mdx|html)$/i.test(basename);
10280
+ }
10169
10281
  function inferProvider(remoteUrl) {
10170
10282
  try {
10171
10283
  return parseRepositoryCloneUrl(remoteUrl).provider ?? "other";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amistio/cli",
3
- "version": "0.1.45",
3
+ "version": "0.1.47",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",