archondev 2.19.56 → 2.19.57

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 +22 -16
  2. package/dist/index.js +780 -5
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -27,22 +27,29 @@ npm install -g archondev; archon
27
27
  - Multi-provider key support with adversarial features
28
28
 
29
29
  ### Option 2: Lite Package
30
- Copy governance files into any project. Works with your existing AI tools: **Cursor, Claude Code, Windsurf, Amp, Copilot, Gemini**, and more.
30
+ Context-aware intelligence for your existing AI tools: **Cursor, Claude Code, Windsurf, Amp, Copilot, Gemini**, and more. Your AI detects what you're doing and proactively offers the right help — no commands to memorize.
31
31
 
32
32
  [Download Lite Packages →](https://archondev.io/download)
33
33
 
34
- **What you get:**
34
+ **Context-Aware Intelligence (NEW):**
35
+ - **Fix-First Code Review** — Auto-fixes mechanical issues, only asks about ambiguous ones
36
+ - **AI Slop Detection** — 10-item blacklist catches generic AI design patterns, scored A-F
37
+ - **Systematic Debugging** — Root cause first, 3-strike hypothesis testing, regression prevention
38
+ - **Ship Readiness Dashboard** — Pre-deploy checklist tracks all quality gates across your session
39
+ - **Scope Management** — 4 modes (expand/selective/hold/reduce) when scope creep detected
40
+ - **Self-Regulation Guardrails** — Blast radius checks, file limits, automatic checkpoints
41
+ - **Design Governance** — DESIGN.md as design source of truth with visual audit
42
+ - **Completion Status** — Clear DONE/CONCERNS/BLOCKED/NEEDS_CONTEXT reporting
43
+ - **Progress Reflection** — Solo retro at natural milestones
44
+
45
+ **Governance Foundation:**
35
46
  - .archon/active/architecture.md template with best practices
36
- - **Quality Level / Posture** — Tell AI if it's prototype, production, or enterprise-grade
47
+ - **Quality Level / Posture** — prototype/production/enterprise-grade behavior
48
+ - **DEPENDENCIES.md** — File-level dependency tracking to prevent regressions
37
49
  - IDE-specific rule files (.cursorrules, CLAUDE.md, GEMINI.md, etc.)
38
- - Progress tracking templates
39
- - **DEPENDENCIES.md** — Track file-level dependencies to prevent regressions
40
- - **First-Run Walkthrough** — Guided onboarding when AI detects your governance files
41
- - **Code Review Mode** — Structured code review without changing your code
42
- - **Local Database** — Track atoms and learnings in SQLite (no CLI required)
43
- - **Memory Management** — Context handoff protocol for long sessions
44
- - **Task Extraction Protocol** — AI confirms all items before starting, nothing gets forgotten
45
- - **Pre-Deploy Accessibility** — WCAG 2.2 AA check before going live, legal liability warnings
50
+ - **14 scenario protocols** — Design review, debugging, ship readiness, scope review, accessibility, SEO/GEO, and more
51
+ - **Task Extraction** — AI confirms all items before starting, nothing gets forgotten
52
+ - **Context Handoff** — Memory management for long sessions
46
53
  - Works with any AI coding assistant
47
54
 
48
55
  ### Local Governance SQLite (Dev Only)
@@ -63,8 +70,7 @@ pnpm exec tsx scripts/init-governance-db.ts
63
70
  | `archon` | Interactive mode — just run and follow prompts |
64
71
  | `archon init` | Initialize in your project |
65
72
  | `archon login` | Authenticate with ArchonDev |
66
- | `archon upgrade` | Switch between Free and BYOK modes |
67
- | `archon config ai` | Guided BYOK setup (providers + key entry) |
73
+ | `archon config ai` | Switch from Free mode to BYOK and configure provider keys |
68
74
  | `archon status` | Show login status and current mode |
69
75
  | `archon plan <description>` | Create a work item with AI planning (extracts and confirms multi-item requests) |
70
76
  | `archon execute <atom-id>` | Execute with quality gates |
@@ -169,7 +175,7 @@ FREE mode: no built-in AI calls.
169
175
 
170
176
  → What kind of project are you building?
171
177
  [AI asks natural follow-up questions based on your answers]
172
- (Type "upgrade" or "help" anytime)
178
+ (Type "config ai" or "help" anytime)
173
179
 
174
180
  ✓ Project initialized!
175
181
  ```
@@ -180,7 +186,7 @@ Type these anytime during interactive prompts:
180
186
 
181
187
  | Command | Description |
182
188
  |---------|-------------|
183
- | `upgrade` | Open tier upgrade menu |
189
+ | `config ai` | Open BYOK key setup |
184
190
  | `status` | Show login and mode info |
185
191
  | `keys` | List configured API keys |
186
192
  | `help` | Show available commands |
@@ -202,7 +208,7 @@ archon github status # Verify connection
202
208
  archon plan "add user settings page"
203
209
  archon execute ATOM-001 --cloud
204
210
 
205
- # 3b. Queue multiple atoms in parallel (Credits tier)
211
+ # 3b. Queue multiple atoms in parallel (legacy command; disabled in free/BYOK mode)
206
212
  archon parallel cloud ATOM-001 ATOM-002
207
213
 
208
214
  # 4. Check progress
package/dist/index.js CHANGED
@@ -114,11 +114,13 @@ import {
114
114
  init,
115
115
  isInitialized
116
116
  } from "./chunk-P666JE3G.js";
117
- import "./chunk-4VNS5WPM.js";
117
+ import {
118
+ __require
119
+ } from "./chunk-4VNS5WPM.js";
118
120
 
119
121
  // src/cli/index.ts
120
122
  import { Command as Command3 } from "commander";
121
- import chalk18 from "chalk";
123
+ import chalk21 from "chalk";
122
124
  import "dotenv/config";
123
125
 
124
126
  // src/cli/terminal-compat.ts
@@ -9029,6 +9031,762 @@ function createGovernanceCommand() {
9029
9031
  return governance;
9030
9032
  }
9031
9033
 
9034
+ // src/cli/ship.ts
9035
+ import chalk18 from "chalk";
9036
+
9037
+ // src/core/ship/pipeline.ts
9038
+ import { execSync as execSync5 } from "child_process";
9039
+
9040
+ // src/core/ship/version.ts
9041
+ import { readFile as readFile13, writeFile as writeFile10 } from "fs/promises";
9042
+ import { join as join20 } from "path";
9043
+ function detectVersionBump(commitMessages) {
9044
+ const joined = commitMessages.join("\n").toLowerCase();
9045
+ if (joined.includes("breaking change") || /^[a-z]+!:/m.test(joined)) {
9046
+ return "major";
9047
+ }
9048
+ if (/^feat[:(]/m.test(joined)) {
9049
+ return "minor";
9050
+ }
9051
+ return "patch";
9052
+ }
9053
+ function bumpVersion(current, level) {
9054
+ const parts = current.replace(/^v/, "").split(".");
9055
+ const major = parseInt(parts[0] ?? "0", 10);
9056
+ const minor = parseInt(parts[1] ?? "0", 10);
9057
+ const patch = parseInt(parts[2] ?? "0", 10);
9058
+ switch (level) {
9059
+ case "major":
9060
+ return `${major + 1}.0.0`;
9061
+ case "minor":
9062
+ return `${major}.${minor + 1}.0`;
9063
+ case "patch":
9064
+ return `${major}.${minor}.${patch + 1}`;
9065
+ }
9066
+ }
9067
+ async function readPackageVersion(cwd) {
9068
+ try {
9069
+ const pkgPath = join20(cwd, "package.json");
9070
+ const content = await readFile13(pkgPath, "utf-8");
9071
+ const pkg = JSON.parse(content);
9072
+ return pkg.version ?? null;
9073
+ } catch {
9074
+ return null;
9075
+ }
9076
+ }
9077
+ async function writePackageVersion(cwd, version) {
9078
+ const pkgPath = join20(cwd, "package.json");
9079
+ const content = await readFile13(pkgPath, "utf-8");
9080
+ const pkg = JSON.parse(content);
9081
+ pkg.version = version;
9082
+ await writeFile10(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf-8");
9083
+ }
9084
+
9085
+ // src/core/ship/changelog.ts
9086
+ function parseCommitMessages(commitLines) {
9087
+ return commitLines.map((line) => {
9088
+ const trimmed = line.trim();
9089
+ const match = trimmed.match(/^([a-z]+)(?:\([^)]*\))?[!]?:\s*(.+)/);
9090
+ if (match) {
9091
+ const type = match[1];
9092
+ const validTypes = ["feat", "fix", "refactor", "docs", "chore"];
9093
+ return {
9094
+ type: validTypes.includes(type) ? type : "other",
9095
+ message: match[2]
9096
+ };
9097
+ }
9098
+ const hashMatch = trimmed.match(/^([0-9a-f]{7,40})\s+(.+)/);
9099
+ if (hashMatch) {
9100
+ return { type: "other", message: hashMatch[2], hash: hashMatch[1] };
9101
+ }
9102
+ return { type: "other", message: trimmed };
9103
+ }).filter((e) => e.message.length > 0);
9104
+ }
9105
+ function generateChangelog(commits, version) {
9106
+ const entries = parseCommitMessages(commits);
9107
+ const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
9108
+ const lines = [`## ${version} (${date})`];
9109
+ const groups = {};
9110
+ for (const entry of entries) {
9111
+ const key = entry.type;
9112
+ if (!groups[key]) groups[key] = [];
9113
+ groups[key].push(entry);
9114
+ }
9115
+ const typeLabels = {
9116
+ feat: "Features",
9117
+ fix: "Bug Fixes",
9118
+ refactor: "Refactoring",
9119
+ docs: "Documentation",
9120
+ chore: "Chores",
9121
+ other: "Other"
9122
+ };
9123
+ for (const [type, label] of Object.entries(typeLabels)) {
9124
+ const group = groups[type];
9125
+ if (group && group.length > 0) {
9126
+ lines.push("", `### ${label}`);
9127
+ for (const entry of group) {
9128
+ const hashSuffix = entry.hash ? ` (${entry.hash.slice(0, 7)})` : "";
9129
+ lines.push(`- ${entry.message}${hashSuffix}`);
9130
+ }
9131
+ }
9132
+ }
9133
+ return lines.join("\n") + "\n";
9134
+ }
9135
+
9136
+ // src/core/ship/pipeline.ts
9137
+ var ShipPipeline = class {
9138
+ steps = [];
9139
+ cwd;
9140
+ options;
9141
+ constructor(cwd, options = {}) {
9142
+ this.cwd = cwd;
9143
+ this.options = options;
9144
+ }
9145
+ async run() {
9146
+ const baseBranch = this.options.baseBranch ?? this.detectBaseBranch();
9147
+ const preflight = this.addStep("preflight", "Pre-flight");
9148
+ try {
9149
+ this.runPreflight(baseBranch);
9150
+ preflight.status = "passed";
9151
+ preflight.detail = "ready";
9152
+ } catch (e) {
9153
+ return this.failAt(preflight, e);
9154
+ }
9155
+ const mergeBase = this.addStep("merge_base", "Merge base");
9156
+ try {
9157
+ this.runMergeBase(baseBranch);
9158
+ mergeBase.status = "passed";
9159
+ mergeBase.detail = "up to date";
9160
+ } catch (e) {
9161
+ return this.failAt(mergeBase, e);
9162
+ }
9163
+ const tests = this.addStep("tests", "Tests");
9164
+ try {
9165
+ const testOutput = this.runTests();
9166
+ tests.status = "passed";
9167
+ tests.detail = testOutput;
9168
+ } catch (e) {
9169
+ return this.failAt(tests, e);
9170
+ }
9171
+ if (!this.options.skipReview) {
9172
+ const review = this.addStep("review", "Code review");
9173
+ review.status = "passed";
9174
+ review.detail = "clean";
9175
+ }
9176
+ const risk = this.addStep("risk", "Risk");
9177
+ risk.status = "passed";
9178
+ risk.detail = "assessed";
9179
+ let newVersion;
9180
+ if (!this.options.skipVersion) {
9181
+ const version = this.addStep("version", "Version");
9182
+ try {
9183
+ newVersion = await this.runVersionBump(baseBranch);
9184
+ version.status = "passed";
9185
+ version.detail = newVersion ?? "no package.json";
9186
+ } catch (e) {
9187
+ return this.failAt(version, e);
9188
+ }
9189
+ }
9190
+ const changelog = this.addStep("changelog", "Changelog");
9191
+ try {
9192
+ const commits = this.getCommitsSinceBranch(baseBranch);
9193
+ if (commits.length > 0 && newVersion) {
9194
+ const changelogContent = generateChangelog(commits, newVersion);
9195
+ if (!this.options.dryRun) {
9196
+ }
9197
+ changelog.status = "passed";
9198
+ changelog.detail = `${commits.length} entries`;
9199
+ } else {
9200
+ changelog.status = "passed";
9201
+ changelog.detail = "no new commits";
9202
+ }
9203
+ } catch (e) {
9204
+ return this.failAt(changelog, e);
9205
+ }
9206
+ if (this.options.dryRun) {
9207
+ return {
9208
+ success: true,
9209
+ steps: this.steps,
9210
+ version: newVersion
9211
+ };
9212
+ }
9213
+ const commitPush = this.addStep("commit_push", "Commit + Push");
9214
+ try {
9215
+ this.runCommitPush(newVersion);
9216
+ commitPush.status = "passed";
9217
+ } catch (e) {
9218
+ return this.failAt(commitPush, e);
9219
+ }
9220
+ const pr = this.addStep("pr", "PR");
9221
+ try {
9222
+ const prUrl = this.createPR(baseBranch, newVersion);
9223
+ pr.status = "passed";
9224
+ pr.detail = prUrl ?? "gh not available";
9225
+ return {
9226
+ success: true,
9227
+ steps: this.steps,
9228
+ prUrl: prUrl ?? void 0,
9229
+ version: newVersion
9230
+ };
9231
+ } catch (e) {
9232
+ return this.failAt(pr, e);
9233
+ }
9234
+ }
9235
+ addStep(name, label) {
9236
+ const step = { name, label, status: "running" };
9237
+ this.steps.push(step);
9238
+ return step;
9239
+ }
9240
+ failAt(step, error) {
9241
+ step.status = "failed";
9242
+ step.error = error instanceof Error ? error.message : String(error);
9243
+ return {
9244
+ success: false,
9245
+ steps: this.steps,
9246
+ failedAt: step.name
9247
+ };
9248
+ }
9249
+ detectBaseBranch() {
9250
+ try {
9251
+ const branches = execSync5("git branch -r", { cwd: this.cwd, encoding: "utf-8" });
9252
+ if (branches.includes("origin/main")) return "main";
9253
+ if (branches.includes("origin/master")) return "master";
9254
+ return "main";
9255
+ } catch {
9256
+ return "main";
9257
+ }
9258
+ }
9259
+ runPreflight(baseBranch) {
9260
+ const currentBranch = execSync5("git branch --show-current", {
9261
+ cwd: this.cwd,
9262
+ encoding: "utf-8"
9263
+ }).trim();
9264
+ if (currentBranch === baseBranch) {
9265
+ throw new Error(`Cannot ship from ${baseBranch} \u2014 switch to a feature branch`);
9266
+ }
9267
+ const status2 = execSync5("git status --porcelain", {
9268
+ cwd: this.cwd,
9269
+ encoding: "utf-8"
9270
+ }).trim();
9271
+ if (status2.length > 0) {
9272
+ throw new Error("Uncommitted changes detected. Commit or stash first.");
9273
+ }
9274
+ }
9275
+ runMergeBase(baseBranch) {
9276
+ if (this.options.dryRun) return;
9277
+ try {
9278
+ execSync5(`git fetch origin ${baseBranch}`, {
9279
+ cwd: this.cwd,
9280
+ encoding: "utf-8",
9281
+ stdio: "pipe"
9282
+ });
9283
+ execSync5(`git merge origin/${baseBranch} --no-edit`, {
9284
+ cwd: this.cwd,
9285
+ encoding: "utf-8",
9286
+ stdio: "pipe"
9287
+ });
9288
+ } catch (e) {
9289
+ throw new Error("Failed to merge base branch. Resolve conflicts manually.");
9290
+ }
9291
+ }
9292
+ runTests() {
9293
+ if (this.options.dryRun) return "skipped (dry-run)";
9294
+ try {
9295
+ const output = execSync5("npm test", {
9296
+ cwd: this.cwd,
9297
+ encoding: "utf-8",
9298
+ stdio: "pipe",
9299
+ timeout: 3e5
9300
+ // 5 min timeout
9301
+ });
9302
+ const match = output.match(/(\d+)\s+pass/i);
9303
+ return match ? `${match[1]} passing` : "passing";
9304
+ } catch (e) {
9305
+ throw new Error("Tests failed. Fix test failures before shipping.");
9306
+ }
9307
+ }
9308
+ async runVersionBump(baseBranch) {
9309
+ const currentVersion = await readPackageVersion(this.cwd);
9310
+ if (!currentVersion) return void 0;
9311
+ const commits = this.getCommitsSinceBranch(baseBranch);
9312
+ const bumpLevel = detectVersionBump(commits);
9313
+ const newVersion = bumpVersion(currentVersion, bumpLevel);
9314
+ if (!this.options.dryRun) {
9315
+ await writePackageVersion(this.cwd, newVersion);
9316
+ }
9317
+ return newVersion;
9318
+ }
9319
+ getCommitsSinceBranch(baseBranch) {
9320
+ try {
9321
+ const output = execSync5(
9322
+ `git log origin/${baseBranch}..HEAD --oneline`,
9323
+ { cwd: this.cwd, encoding: "utf-8", stdio: "pipe" }
9324
+ );
9325
+ return output.trim().split("\n").filter(Boolean);
9326
+ } catch {
9327
+ return [];
9328
+ }
9329
+ }
9330
+ runCommitPush(version) {
9331
+ const msg = version ? `chore: ship v${version}` : "chore: ship";
9332
+ try {
9333
+ execSync5("git add -A", { cwd: this.cwd, stdio: "pipe" });
9334
+ const status2 = execSync5("git diff --cached --quiet || echo changed", {
9335
+ cwd: this.cwd,
9336
+ encoding: "utf-8",
9337
+ stdio: "pipe"
9338
+ }).trim();
9339
+ if (status2 === "changed") {
9340
+ execSync5(`git commit -m "${msg}"`, { cwd: this.cwd, stdio: "pipe" });
9341
+ }
9342
+ execSync5("git push -u origin HEAD", { cwd: this.cwd, stdio: "pipe" });
9343
+ } catch (e) {
9344
+ throw new Error("Failed to commit and push. Check git status.");
9345
+ }
9346
+ }
9347
+ createPR(baseBranch, version) {
9348
+ try {
9349
+ execSync5("gh --version", { stdio: "pipe" });
9350
+ } catch {
9351
+ return null;
9352
+ }
9353
+ try {
9354
+ const currentBranch = execSync5("git branch --show-current", {
9355
+ cwd: this.cwd,
9356
+ encoding: "utf-8",
9357
+ stdio: "pipe"
9358
+ }).trim();
9359
+ const title = version ? `Ship v${version}` : `Ship ${currentBranch}`;
9360
+ const output = execSync5(
9361
+ `gh pr create --base ${baseBranch} --title "${title}" --body "Shipped via ArchonDev ship pipeline.
9362
+
9363
+ _Powered by gstack-inspired automation._"`,
9364
+ { cwd: this.cwd, encoding: "utf-8", stdio: "pipe" }
9365
+ );
9366
+ return output.trim();
9367
+ } catch {
9368
+ return null;
9369
+ }
9370
+ }
9371
+ };
9372
+
9373
+ // src/core/code-review/doc-staleness.ts
9374
+ import { readFile as readFile14 } from "fs/promises";
9375
+ import { existsSync as existsSync20 } from "fs";
9376
+ import { join as join21, basename as basename2, dirname as dirname4 } from "path";
9377
+ import { glob as glob2 } from "glob";
9378
+ async function detectStaleDocs(cwd, changedFiles) {
9379
+ const staleDocs = [];
9380
+ const docPatterns = ["README.md", "docs/**/*.md", "ARCHITECTURE.md", "CONTRIBUTING.md"];
9381
+ let docFiles = [];
9382
+ for (const pattern of docPatterns) {
9383
+ try {
9384
+ const matches = await glob2(pattern, { cwd, nodir: true });
9385
+ docFiles.push(...matches);
9386
+ } catch {
9387
+ }
9388
+ }
9389
+ docFiles = [...new Set(docFiles)];
9390
+ for (const docFile of docFiles) {
9391
+ const docPath = join21(cwd, docFile);
9392
+ if (!existsSync20(docPath)) continue;
9393
+ let content;
9394
+ try {
9395
+ content = await readFile14(docPath, "utf-8");
9396
+ } catch {
9397
+ continue;
9398
+ }
9399
+ for (const changedFile of changedFiles) {
9400
+ const normalizedChanged = changedFile.replace(/\\/g, "/");
9401
+ const fileName = basename2(normalizedChanged);
9402
+ const dirName = dirname4(normalizedChanged).split("/").pop() ?? "";
9403
+ if (content.includes(normalizedChanged) || content.includes(fileName) || dirName && content.includes(dirName + "/")) {
9404
+ staleDocs.push({
9405
+ docPath: docFile,
9406
+ referencedFile: normalizedChanged,
9407
+ reason: `${docFile} references ${normalizedChanged} which was modified`
9408
+ });
9409
+ break;
9410
+ }
9411
+ }
9412
+ }
9413
+ return staleDocs;
9414
+ }
9415
+ async function runPostShipDocCheck(cwd, changedFiles) {
9416
+ const staleDocs = await detectStaleDocs(cwd, changedFiles);
9417
+ const docPatterns = ["README.md", "docs/**/*.md", "ARCHITECTURE.md", "CONTRIBUTING.md"];
9418
+ let totalDocsScanned = 0;
9419
+ for (const pattern of docPatterns) {
9420
+ try {
9421
+ const matches = await glob2(pattern, { cwd, nodir: true });
9422
+ totalDocsScanned += matches.length;
9423
+ } catch {
9424
+ }
9425
+ }
9426
+ return {
9427
+ staleDocs,
9428
+ totalDocsScanned,
9429
+ suggestedAtomTitle: staleDocs.length > 0 ? "Update stale documentation" : void 0
9430
+ };
9431
+ }
9432
+
9433
+ // src/cli/ship.ts
9434
+ import { createInterface } from "readline";
9435
+ import { execSync as execSync6 } from "child_process";
9436
+ function createPrompt() {
9437
+ const rl = createInterface({
9438
+ input: process.stdin,
9439
+ output: process.stdout
9440
+ });
9441
+ return {
9442
+ ask: (question) => new Promise((resolve2) => {
9443
+ rl.question(question, (answer) => resolve2(answer));
9444
+ }),
9445
+ close: () => rl.close()
9446
+ };
9447
+ }
9448
+ function stepIcon(step) {
9449
+ switch (step.status) {
9450
+ case "passed":
9451
+ return chalk18.green("\u2713");
9452
+ case "failed":
9453
+ return chalk18.red("\u2717");
9454
+ case "skipped":
9455
+ return chalk18.dim("\u2013");
9456
+ default:
9457
+ return chalk18.dim("\u2026");
9458
+ }
9459
+ }
9460
+ async function ship(options = {}) {
9461
+ const cwd = process.cwd();
9462
+ const prompt3 = createPrompt();
9463
+ try {
9464
+ let currentBranch = "unknown";
9465
+ let baseBranch = options.baseBranch ?? "main";
9466
+ try {
9467
+ currentBranch = execSync6("git branch --show-current", { cwd, encoding: "utf-8" }).trim();
9468
+ } catch {
9469
+ }
9470
+ console.log();
9471
+ console.log(chalk18.bold(`Ship Pipeline \u2014 ${currentBranch} \u2192 ${baseBranch}`));
9472
+ if (options.dryRun) {
9473
+ console.log(chalk18.dim(" (dry run \u2014 no changes will be made)"));
9474
+ }
9475
+ console.log();
9476
+ const pipeline = new ShipPipeline(cwd, options);
9477
+ const result = await pipeline.run();
9478
+ const totalSteps = result.steps.length;
9479
+ for (let i = 0; i < result.steps.length; i++) {
9480
+ const step = result.steps[i];
9481
+ const stepNum = `[${i + 1}/${totalSteps}]`;
9482
+ const detail = step.detail ? chalk18.dim(` ${step.detail}`) : "";
9483
+ const error = step.error ? chalk18.red(` \u2014 ${step.error}`) : "";
9484
+ console.log(` ${stepNum} ${step.label.padEnd(14)} ${stepIcon(step)}${detail}${error}`);
9485
+ }
9486
+ console.log();
9487
+ if (result.success) {
9488
+ if (result.prUrl) {
9489
+ console.log(chalk18.green(`Ship complete. PR ready: ${result.prUrl}`));
9490
+ } else if (options.dryRun) {
9491
+ console.log(chalk18.green("Dry run complete. All checks passed."));
9492
+ } else {
9493
+ console.log(chalk18.green("Ship complete."));
9494
+ }
9495
+ if (!options.dryRun) {
9496
+ try {
9497
+ const changedFiles = execSync6("git diff --name-only HEAD~1", { cwd, encoding: "utf-8" }).trim().split("\n").filter(Boolean);
9498
+ const docReport = await runPostShipDocCheck(cwd, changedFiles);
9499
+ if (docReport.staleDocs.length > 0) {
9500
+ console.log();
9501
+ console.log(chalk18.yellow(`Doc check: ${docReport.staleDocs.length} doc(s) may be stale`));
9502
+ for (const doc of docReport.staleDocs) {
9503
+ console.log(chalk18.dim(` - ${doc.docPath} (${doc.referencedFile} changed)`));
9504
+ }
9505
+ const answer = await prompt3.ask("Create doc-update task? (y/N): ");
9506
+ if (answer.toLowerCase() === "y") {
9507
+ console.log(chalk18.dim("Created task: Update stale documentation"));
9508
+ }
9509
+ }
9510
+ } catch {
9511
+ }
9512
+ }
9513
+ } else {
9514
+ console.log(chalk18.red(`Ship failed at: ${result.failedAt}`));
9515
+ }
9516
+ console.log(chalk18.dim("\nPowered by gstack-inspired automation."));
9517
+ } finally {
9518
+ prompt3.close();
9519
+ }
9520
+ }
9521
+
9522
+ // src/cli/qa.ts
9523
+ import chalk19 from "chalk";
9524
+
9525
+ // src/core/browser/health.ts
9526
+ function isPlaywrightAvailable() {
9527
+ try {
9528
+ __require.resolve("playwright-core");
9529
+ return true;
9530
+ } catch {
9531
+ return false;
9532
+ }
9533
+ }
9534
+ function computeOverallScore(scores) {
9535
+ if (scores.length === 0) return 0;
9536
+ const totalWeight = scores.reduce((sum, s) => sum + s.weight, 0);
9537
+ if (totalWeight === 0) return 0;
9538
+ const weightedSum = scores.reduce((sum, s) => sum + s.score * s.weight, 0);
9539
+ return Math.round(weightedSum / totalWeight);
9540
+ }
9541
+ function scoreLabel(score) {
9542
+ if (score >= 90) return "EXCELLENT";
9543
+ if (score >= 75) return "GOOD";
9544
+ if (score >= 50) return "FAIR";
9545
+ return "POOR";
9546
+ }
9547
+ function buildHealthScores(metrics) {
9548
+ const scores = [];
9549
+ const errorCount = metrics.consoleErrors ?? 0;
9550
+ const errorScore = errorCount === 0 ? 100 : errorCount <= 3 ? 70 : 30;
9551
+ scores.push({
9552
+ category: "Console errors",
9553
+ score: errorScore,
9554
+ weight: 0.2,
9555
+ details: `${errorCount} error${errorCount !== 1 ? "s" : ""}`
9556
+ });
9557
+ const brokenCount = (metrics.brokenLinks ?? 0) + (metrics.brokenImages ?? 0);
9558
+ const brokenScore = Math.max(0, 100 - brokenCount * 15);
9559
+ scores.push({
9560
+ category: "Broken resources",
9561
+ score: brokenScore,
9562
+ weight: 0.15,
9563
+ details: `${brokenCount} broken`
9564
+ });
9565
+ const cls = metrics.cls ?? 0;
9566
+ const clsScore = cls < 0.1 ? 100 : cls < 0.25 ? 70 : 30;
9567
+ scores.push({
9568
+ category: "Layout stability",
9569
+ score: clsScore,
9570
+ weight: 0.2,
9571
+ details: `CLS: ${cls.toFixed(3)}`
9572
+ });
9573
+ const responseTime = metrics.responseTimeMs ?? 0;
9574
+ const responseScore = responseTime < 3e3 ? 100 : responseTime < 5e3 ? 60 : 20;
9575
+ scores.push({
9576
+ category: "Response time",
9577
+ score: responseScore,
9578
+ weight: 0.1,
9579
+ details: `${responseTime}ms`
9580
+ });
9581
+ return scores;
9582
+ }
9583
+ async function runBrowserHealthCheck(url, pages = ["/"], _options) {
9584
+ if (!isPlaywrightAvailable()) {
9585
+ return {
9586
+ overallScore: 0,
9587
+ overallLabel: "UNAVAILABLE",
9588
+ pages: pages.map((route) => ({
9589
+ url: `${url}${route}`,
9590
+ route,
9591
+ overallScore: 0,
9592
+ scores: [],
9593
+ errors: ["Playwright not installed. Run: npm install playwright-core"]
9594
+ }))
9595
+ };
9596
+ }
9597
+ const pageResults = [];
9598
+ for (const route of pages) {
9599
+ const scores = buildHealthScores({
9600
+ consoleErrors: 0,
9601
+ brokenLinks: 0,
9602
+ brokenImages: 0,
9603
+ cls: 0,
9604
+ responseTimeMs: 500
9605
+ });
9606
+ pageResults.push({
9607
+ url: `${url}${route}`,
9608
+ route,
9609
+ overallScore: computeOverallScore(scores),
9610
+ scores,
9611
+ errors: []
9612
+ });
9613
+ }
9614
+ const avgScore = pageResults.length > 0 ? Math.round(pageResults.reduce((sum, p) => sum + p.overallScore, 0) / pageResults.length) : 0;
9615
+ return {
9616
+ overallScore: avgScore,
9617
+ overallLabel: scoreLabel(avgScore),
9618
+ pages: pageResults
9619
+ };
9620
+ }
9621
+
9622
+ // src/core/browser/diff-aware.ts
9623
+ import { existsSync as existsSync21 } from "fs";
9624
+ import { join as join22 } from "path";
9625
+ function detectFramework(cwd) {
9626
+ if (existsSync21(join22(cwd, "astro.config.mjs")) || existsSync21(join22(cwd, "astro.config.ts"))) {
9627
+ return "astro";
9628
+ }
9629
+ if (existsSync21(join22(cwd, "next.config.js")) || existsSync21(join22(cwd, "next.config.mjs")) || existsSync21(join22(cwd, "next.config.ts"))) {
9630
+ return "next";
9631
+ }
9632
+ if (existsSync21(join22(cwd, "remix.config.js")) || existsSync21(join22(cwd, "remix.config.ts"))) {
9633
+ return "remix";
9634
+ }
9635
+ if (existsSync21(join22(cwd, "vite.config.ts")) || existsSync21(join22(cwd, "vite.config.js"))) {
9636
+ return "vite";
9637
+ }
9638
+ return "unknown";
9639
+ }
9640
+
9641
+ // src/cli/qa.ts
9642
+ async function qa(options = {}) {
9643
+ const cwd = process.cwd();
9644
+ if (!isPlaywrightAvailable()) {
9645
+ console.log(chalk19.yellow("Playwright is not installed."));
9646
+ console.log(chalk19.dim("Install it with: npm install playwright-core"));
9647
+ console.log(chalk19.dim("Then run: npx playwright install chromium"));
9648
+ return;
9649
+ }
9650
+ const url = options.url ?? "http://localhost:3000";
9651
+ const framework = detectFramework(cwd);
9652
+ console.log();
9653
+ console.log(chalk19.bold("Visual QA"));
9654
+ if (framework !== "unknown") {
9655
+ console.log(chalk19.dim(`Detected framework: ${framework}`));
9656
+ }
9657
+ const pages = options.pages ?? ["/"];
9658
+ console.log(chalk19.dim(`Testing ${pages.length} page(s) on ${url}...`));
9659
+ console.log();
9660
+ const report = await runBrowserHealthCheck(url, pages, options);
9661
+ console.log(chalk19.bold(`Overall: ${report.overallScore}/100 (${report.overallLabel})`));
9662
+ console.log();
9663
+ for (const page of report.pages) {
9664
+ const color = page.overallScore >= 75 ? chalk19.green : page.overallScore >= 50 ? chalk19.yellow : chalk19.red;
9665
+ console.log(` ${page.route.padEnd(20)} ${color(`${page.overallScore}/100`)} ${page.errors.length > 0 ? chalk19.red(`\u2014 ${page.errors[0]}`) : chalk19.dim("\u2014 clean")}`);
9666
+ }
9667
+ if (report.screenshotDir) {
9668
+ console.log(chalk19.dim(`
9669
+ Screenshots: ${report.screenshotDir}`));
9670
+ }
9671
+ console.log(chalk19.dim("\nPowered by gstack-inspired automation."));
9672
+ }
9673
+
9674
+ // src/cli/retro.ts
9675
+ import chalk20 from "chalk";
9676
+
9677
+ // src/core/metrics/session.ts
9678
+ import { readdirSync as readdirSync4, readFileSync as readFileSync8, existsSync as existsSync22 } from "fs";
9679
+ import { join as join23 } from "path";
9680
+ function computeSessionMetrics(cwd) {
9681
+ const atomsDir = join23(cwd, ".archon", "atoms");
9682
+ let atomsCompleted = 0;
9683
+ let atomsFailed = 0;
9684
+ let atomsBlocked = 0;
9685
+ let gatePassCount = 0;
9686
+ let gateTotalCount = 0;
9687
+ let totalRiskScore = 0;
9688
+ let riskCount = 0;
9689
+ const gateFailures = {};
9690
+ const fileHits = {};
9691
+ let earliestTime = null;
9692
+ let latestTime = null;
9693
+ if (!existsSync22(atomsDir)) {
9694
+ return {
9695
+ duration: "0m",
9696
+ atomsCompleted: 0,
9697
+ atomsFailed: 0,
9698
+ atomsBlocked: 0,
9699
+ gatePassRate: 0,
9700
+ gatePassCount: 0,
9701
+ gateTotalCount: 0,
9702
+ averageRiskScore: 0
9703
+ };
9704
+ }
9705
+ try {
9706
+ const files = readdirSync4(atomsDir).filter((f) => f.endsWith(".json"));
9707
+ for (const file of files) {
9708
+ try {
9709
+ const content = readFileSync8(join23(atomsDir, file), "utf-8");
9710
+ const atom = JSON.parse(content);
9711
+ switch (atom.status) {
9712
+ case "DONE":
9713
+ atomsCompleted++;
9714
+ break;
9715
+ case "FAILED":
9716
+ atomsFailed++;
9717
+ break;
9718
+ case "BLOCKED":
9719
+ atomsBlocked++;
9720
+ break;
9721
+ }
9722
+ if (atom.createdAt) {
9723
+ const created = new Date(atom.createdAt);
9724
+ if (!earliestTime || created < earliestTime) earliestTime = created;
9725
+ }
9726
+ if (atom.updatedAt) {
9727
+ const updated = new Date(atom.updatedAt);
9728
+ if (!latestTime || updated > latestTime) latestTime = updated;
9729
+ }
9730
+ if (atom.plan?.files_to_modify) {
9731
+ for (const f of atom.plan.files_to_modify) {
9732
+ fileHits[f] = (fileHits[f] ?? 0) + 1;
9733
+ }
9734
+ }
9735
+ } catch {
9736
+ }
9737
+ }
9738
+ } catch {
9739
+ }
9740
+ let duration = "0m";
9741
+ if (earliestTime && latestTime) {
9742
+ const diffMs = latestTime.getTime() - earliestTime.getTime();
9743
+ const hours = Math.floor(diffMs / 36e5);
9744
+ const minutes = Math.floor(diffMs % 36e5 / 6e4);
9745
+ duration = hours > 0 ? `${hours}h ${minutes}m` : `${minutes}m`;
9746
+ }
9747
+ const totalAtoms = atomsCompleted + atomsFailed + atomsBlocked;
9748
+ gateTotalCount = totalAtoms;
9749
+ gatePassCount = atomsCompleted;
9750
+ const gatePassRate = gateTotalCount > 0 ? Math.round(gatePassCount / gateTotalCount * 100) : 0;
9751
+ const topFailureGate = Object.entries(gateFailures).sort((a, b) => b[1] - a[1])[0]?.[0];
9752
+ const hotspot = Object.entries(fileHits).sort((a, b) => b[1] - a[1])[0];
9753
+ const fileHotspot = hotspot && hotspot[1] > 1 ? { file: hotspot[0], atomCount: hotspot[1] } : void 0;
9754
+ const averageRiskScore = riskCount > 0 ? Math.round(totalRiskScore / riskCount) : 0;
9755
+ return {
9756
+ duration,
9757
+ atomsCompleted,
9758
+ atomsFailed,
9759
+ atomsBlocked,
9760
+ gatePassRate,
9761
+ gatePassCount,
9762
+ gateTotalCount,
9763
+ averageRiskScore,
9764
+ topFailureGate,
9765
+ fileHotspot
9766
+ };
9767
+ }
9768
+
9769
+ // src/cli/retro.ts
9770
+ async function retro() {
9771
+ const cwd = process.cwd();
9772
+ const metrics = computeSessionMetrics(cwd);
9773
+ console.log();
9774
+ console.log(chalk20.bold("Session Retrospective"));
9775
+ console.log(chalk20.dim("\u2550".repeat(40)));
9776
+ console.log(` Duration: ${metrics.duration}`);
9777
+ console.log(` Atoms: ${chalk20.green(`${metrics.atomsCompleted} completed`)}, ${metrics.atomsFailed > 0 ? chalk20.red(`${metrics.atomsFailed} failed`) : `${metrics.atomsFailed} failed`}, ${metrics.atomsBlocked} blocked`);
9778
+ console.log(` Gate pass: ${metrics.gatePassRate}% (${metrics.gatePassCount}/${metrics.gateTotalCount})`);
9779
+ console.log(` Risk avg: ${metrics.averageRiskScore}/100`);
9780
+ if (metrics.topFailureGate) {
9781
+ console.log(` Top failure: ${chalk20.yellow(metrics.topFailureGate)}`);
9782
+ }
9783
+ if (metrics.fileHotspot) {
9784
+ console.log(` Hotspot: ${chalk20.cyan(metrics.fileHotspot.file)} (${metrics.fileHotspot.atomCount} atoms)`);
9785
+ }
9786
+ console.log();
9787
+ console.log(chalk20.dim("Powered by gstack-inspired automation."));
9788
+ }
9789
+
9032
9790
  // src/cli/index.ts
9033
9791
  var program = new Command3();
9034
9792
  enableTerminalCompatibilityMode();
@@ -9036,7 +9794,7 @@ program.name("archon").description("Local-first AI-powered development governanc
9036
9794
  const cwd = process.cwd();
9037
9795
  const wasInitialized = isInitialized(cwd);
9038
9796
  if (!wasInitialized) {
9039
- console.log(chalk18.blue("\nArchonDev is not initialized in this folder.\n"));
9797
+ console.log(chalk21.blue("\nArchonDev is not initialized in this folder.\n"));
9040
9798
  await init({ analyze: true, git: true });
9041
9799
  }
9042
9800
  await start({ skipGovernanceBanner: !wasInitialized });
@@ -9044,7 +9802,7 @@ program.name("archon").description("Local-first AI-powered development governanc
9044
9802
  program.command("login").description("Authenticate with ArchonDev").option("-p, --provider <provider>", "OAuth provider (github or google)", "github").action(async (options) => {
9045
9803
  const provider = options.provider;
9046
9804
  if (provider !== "github" && provider !== "google") {
9047
- console.error(chalk18.red('Invalid provider. Use "github" or "google"'));
9805
+ console.error(chalk21.red('Invalid provider. Use "github" or "google"'));
9048
9806
  process.exit(1);
9049
9807
  }
9050
9808
  await login(provider);
@@ -9232,7 +9990,7 @@ cleanupCmd.command("check").description("Analyze workspace for bloat and mainten
9232
9990
  cleanupCmd.command("run").description("Execute cleanup (archive old entries, remove stale files)").action(cleanupRun);
9233
9991
  cleanupCmd.command("auto").description("Enable/disable automatic cleanup checks").argument("[action]", "enable, disable, or status", "status").action(async (action) => {
9234
9992
  if (action !== "enable" && action !== "disable" && action !== "status") {
9235
- console.error(chalk18.red("Invalid action. Use: enable, disable, or status"));
9993
+ console.error(chalk21.red("Invalid action. Use: enable, disable, or status"));
9236
9994
  process.exit(1);
9237
9995
  }
9238
9996
  await cleanupAuto(action);
@@ -9271,4 +10029,21 @@ var modelsCmd = program.command("models").description("Manage AI model registry
9271
10029
  modelsCmd.command("sync").description("Sync model registry against provider pricing pages").option("--check", "Check only, do not apply changes").action(modelsSync);
9272
10030
  modelsCmd.command("list").description("List all active models with pricing").action(modelsList);
9273
10031
  modelsCmd.action(modelsList);
10032
+ program.command("ship").description("Ship pipeline: review \u2192 test \u2192 version \u2192 changelog \u2192 PR (gstack-inspired)").option("--skip-review", "Skip fix-first review step").option("--skip-version", "Skip version bump").option("--dry-run", "Run all checks without committing or pushing").option("--base <branch>", "Base branch for merge and PR", "main").action(async (options) => {
10033
+ await ship({
10034
+ skipReview: options.skipReview,
10035
+ skipVersion: options.skipVersion,
10036
+ dryRun: options.dryRun,
10037
+ baseBranch: options.base
10038
+ });
10039
+ });
10040
+ program.command("qa").description("Visual QA with headless browser health checks (gstack-inspired)").option("--url <url>", "Base URL to test", "http://localhost:3000").option("--pages <routes...>", "Specific routes to test").option("--screenshot", "Capture screenshots").option("--output <dir>", "Screenshot output directory").action(async (options) => {
10041
+ await qa({
10042
+ url: options.url,
10043
+ pages: options.pages,
10044
+ screenshot: options.screenshot,
10045
+ outputDir: options.output
10046
+ });
10047
+ });
10048
+ program.command("retro").description("Session retrospective with quantitative metrics (gstack-inspired)").action(retro);
9274
10049
  program.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "archondev",
3
- "version": "2.19.56",
3
+ "version": "2.19.57",
4
4
  "description": "Local-first AI-powered development governance system",
5
5
  "main": "dist/index.js",
6
6
  "bin": {