@plainconceptsplatform/agent-harness 3.0.3 → 3.1.1

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.
@@ -1,53 +0,0 @@
1
- GitHub data comes from the `gh` CLI; a page fetch of github.com is denied (pc-system-reminders). If `gh` is unavailable, skip publishing and report it; do not fail the pipeline unless the caller declared publishing a ship gate. Always pass `--repo {owner}/{repo}` (or `repos/{owner}/{repo}` for `gh api`) explicitly.
2
-
3
- Publish one status comment for every manifest. A `blocked` or `failed` manifest must include its status and reason, never a success claim.
4
-
5
- ### Step 1 — Build commit-pinned repository links
6
-
7
- An embedded image must be a raw URL pinned to the commit that actually contains it, and that commit must be pushed. For each asset in `evidence.json`:
8
-
9
- ```bash
10
- SHA="$(git log -n 1 --format=%H -- '{asset-path}')" # the commit that added this asset
11
- [ -z "$SHA" ] && echo "asset not committed: {asset-path}" && exit-skip
12
- gh api "repos/{owner}/{repo}/contents/{asset-path}?ref=$SHA" --silent # 404 → not pushed yet → skip embedding
13
- ```
14
-
15
- Build a blob link for every committed manifest and asset: `https://github.com/{owner}/{repo}/blob/{SHA}/{asset-path}`. Use the raw URL only for a verified embeddable image: `https://raw.githubusercontent.com/{owner}/{repo}/{SHA}/{asset-path}`. If verification fails, include the asset path and SHA as text; never post a dead link.
16
-
17
- ### Step 2 — Build the comment body with a stable marker (idempotent)
18
-
19
- Prefix the body with a hidden marker so re-runs update the same comment instead of piling on:
20
-
21
- ```
22
- <!-- pc-visual-evidence:{change-id} -->
23
- <!-- pc-visual-evidence-status:{status} -->
24
-
25
- Status: `{status}`
26
-
27
- {reason?}
28
-
29
- Manifest: {commit-pinned evidence.json link}
30
-
31
- Assets: {commit-pinned asset links}
32
-
33
- {prMarkdown}
34
- ```
35
-
36
- ### Step 3 — Upsert the comment on BOTH the issue and the PR
37
-
38
- For each target number (the originating issue, and the PR number when provided), find an existing marked comment and PATCH it, else POST a new one:
39
-
40
- ```bash
41
- # find existing
42
- ID="$(gh api "repos/{owner}/{repo}/issues/{number}/comments" --paginate --jq \
43
- '.[] | select(.body | contains("<!-- pc-visual-evidence:{change-id} -->")) | .id' | head -1)"
44
-
45
- if [ -n "$ID" ]; then
46
- gh api --method PATCH "repos/{owner}/{repo}/issues/comments/$ID" -f body="$BODY" --silent
47
- else
48
- gh api --method POST "repos/{owner}/{repo}/issues/{number}/comments" -f body="$BODY" --silent
49
- fi
50
- ```
51
-
52
- - Text-only (no verified image, or `default` mode / nothing pushed): drop the image lines, keep the summary (tasks N/N, verification result, commits).
53
- - If a comment call fails: report it. Fail the run ONLY when the caller declared publishing a ship gate; otherwise continue.
@@ -1,42 +0,0 @@
1
- Jira data comes from the `acli` CLI; a page fetch of atlassian.net is denied (pc-system-reminders). If `acli` is unavailable, skip publishing and report it; do not fail the pipeline unless the caller declared publishing a ship gate.
2
-
3
- Publish one status comment for every manifest. A `blocked` or `failed` manifest must include its status and reason, never a success claim.
4
-
5
- ### Step 1 — Image hosting caveat
6
-
7
- Jira comments cannot embed an image from a repo blob URL, and `acli` does not upload attachments inline. Use text evidence and derive a commit-pinned repository URL from `git remote get-url origin` when possible. Otherwise include the committed asset path, branch, and SHA:
8
-
9
- ```
10
- Screenshot committed at: {asset-path} (branch {branch}, commit {sha})
11
- ```
12
-
13
- ### Step 2 — Build the comment with a stable marker (idempotent)
14
-
15
- ```
16
- <!-- pc-visual-evidence:{change-id} -->
17
- <!-- pc-visual-evidence-status:{status} -->
18
-
19
- Status: `{status}`
20
-
21
- {reason?}
22
-
23
- Manifest and assets: {commit-pinned links when available, otherwise committed paths and SHA}
24
-
25
- {prMarkdown}
26
-
27
- {image-line?}
28
- ```
29
-
30
- ### Step 3 — Upsert the comment on the issue
31
-
32
- `acli` cannot edit a comment in place, so match on the status probe rather than the change id alone. Matching the change id alone leaves a stale `blocked` comment standing after a later run succeeds:
33
-
34
- ```bash
35
- # Skip only when this exact status is already published for this change.
36
- acli jira issue comment list --key {issue-key} 2>/dev/null | grep -q "pc-visual-evidence-status:{status}" \
37
- || acli jira issue comment --key {issue-key} --body "$BODY"
38
- ```
39
-
40
- When an earlier comment carries this change id with a different status, this one supersedes it: open the body with `Supersedes the earlier evidence comment for this change.` so a reader can tell which is current.
41
-
42
- - If the comment command fails: report it. Fail the run ONLY when publishing was declared a ship gate; otherwise continue.
@@ -1,20 +0,0 @@
1
- {
2
- "plugin": "@slkiser/opencode-quota@5.0.1",
3
- "prompt": {
4
- "message": "Install opencode-quota with recommended defaults?",
5
- "default": true,
6
- "timeoutMs": 20000
7
- },
8
- "defaults": {
9
- "enabledProviders": "auto",
10
- "formatStyle": "singleWindow",
11
- "percentDisplayMode": "used",
12
- "showSessionTokens": true,
13
- "enableToast": false,
14
- "resetNotifications": { "enabled": false },
15
- "tuiCommandDisplay": "inline",
16
- "tuiCompactStatus": { "enabled": false },
17
- "tuiPromptBar": { "enabled": false },
18
- "tuiSidebarPanel": { "enabled": true }
19
- }
20
- }
@@ -1,135 +0,0 @@
1
- import { confirm } from '@inquirer/prompts'
2
- import fse from 'fs-extra'
3
- import path from 'node:path'
4
- import { fileURLToPath } from 'url'
5
- import { applyEdits, modify, parse as parseJsonc } from 'jsonc-parser'
6
- import { error, header, info, loading, success, warn } from '../../utils/exec.js'
7
-
8
- const __dirname = path.dirname(fileURLToPath(import.meta.url))
9
- const QUOTA_PRESET_PATH = path.resolve(__dirname, '../../presets/quota.json')
10
- const quotaPreset = await fse.readJson(QUOTA_PRESET_PATH)
11
- const PLUGIN = quotaPreset.plugin
12
-
13
- export function splitPackageSpec(spec) {
14
- const separator = spec.startsWith('@') ? spec.indexOf('@', 1) : spec.lastIndexOf('@')
15
- if (separator === -1) return { name: spec, version: 'latest' }
16
- return { name: spec.slice(0, separator), version: spec.slice(separator + 1) }
17
- }
18
-
19
- export async function installQuota(options = {}) {
20
- if (!options.skipHeader) header('Installing opencode-quota')
21
-
22
- let shouldInstall = true
23
- if (!options.skipPrompt && process.stdin.isTTY) {
24
- // Same abort-on-timeout pattern as tokenOptimizationStep: never race a
25
- // live inquirer prompt, it leaks raw-mode stdin and unhandled rejections.
26
- const controller = new AbortController()
27
- const timer = setTimeout(() => controller.abort(), quotaPreset.prompt.timeoutMs)
28
- try {
29
- shouldInstall = await confirm(
30
- {
31
- message: quotaPreset.prompt.message,
32
- default: quotaPreset.prompt.default,
33
- },
34
- { signal: controller.signal },
35
- )
36
- } catch (err) {
37
- if (err.name !== 'AbortPromptError') throw err
38
- shouldInstall = true
39
- } finally {
40
- clearTimeout(timer)
41
- }
42
- }
43
-
44
- if (!shouldInstall) {
45
- warn('Skipped opencode-quota installation')
46
- return { optedIn: false, installed: false }
47
- }
48
-
49
- loading('configuring opencode-quota...')
50
-
51
- try {
52
- const opencodeDir = path.join(process.cwd(), '.opencode')
53
- const opencodePath = path.join(process.cwd(), 'opencode.jsonc')
54
- const pkgPath = path.join(opencodeDir, 'package.json')
55
-
56
- // opencode-quota 5.x reads its settings from `opencode-quota/quota-toast.jsonc`
57
- // next to the OpenCode config (project root), not from `.opencode/`. The old
58
- // `.opencode/opencode-quota/quota-toast.json` is the v4 location and is migrated
59
- // below so the settings actually take effect.
60
- const quotaDir = path.join(process.cwd(), 'opencode-quota')
61
- const quotaPath = path.join(quotaDir, 'quota-toast.jsonc')
62
- const legacyQuotaPath = path.join(opencodeDir, 'opencode-quota', 'quota-toast.json')
63
-
64
- let opencodeText = JSON.stringify({ $schema: 'https://opencode.ai/config.json' }, null, 2)
65
- let opencode
66
- if (await fse.pathExists(opencodePath)) {
67
- const errors = []
68
- opencodeText = await fse.readFile(opencodePath, 'utf-8')
69
- opencode = parseJsonc(opencodeText, errors)
70
- if (errors.length > 0 || !opencode || typeof opencode !== 'object' || Array.isArray(opencode)) {
71
- throw new Error('opencode.jsonc could not be parsed')
72
- }
73
- } else {
74
- opencode = { $schema: 'https://opencode.ai/config.json' }
75
- }
76
-
77
- const pkg = await fse.pathExists(pkgPath)
78
- ? await fse.readJson(pkgPath)
79
- : {}
80
-
81
- // V2 configures server plugins under `plugins`. A V1 `plugin` array found
82
- // here is carried over so the quota entry is not lost when the config
83
- // patcher has not run first.
84
- const plugins = Array.isArray(opencode.plugins)
85
- ? [...opencode.plugins]
86
- : Array.isArray(opencode.plugin)
87
- ? [...opencode.plugin]
88
- : []
89
- if (!plugins.includes(PLUGIN)) {
90
- plugins.push(PLUGIN)
91
- opencodeText = applyEdits(opencodeText, modify(opencodeText, ['plugins'], plugins, {
92
- formattingOptions: { insertSpaces: true, tabSize: 2 },
93
- }))
94
- }
95
-
96
- if (!pkg.dependencies) pkg.dependencies = {}
97
- const { name: pkgName, version: pkgVersion } = splitPackageSpec(PLUGIN)
98
- if (pkg.dependencies[pkgName] !== pkgVersion) pkg.dependencies[pkgName] = pkgVersion
99
-
100
- await fse.ensureDir(opencodeDir)
101
- await fse.writeFile(opencodePath, opencodeText, 'utf-8')
102
- await fse.writeJson(pkgPath, pkg, { spaces: 2 })
103
-
104
- // Carry over any existing settings (new location first, then the legacy
105
- // v4 file) and layer the recommended defaults on top. The edge defaults
106
- // disable toasts and reset notifications and keep slash reports inline.
107
- let quotaConfig = {}
108
- if (await fse.pathExists(quotaPath)) {
109
- quotaConfig = await fse.readJson(quotaPath)
110
- } else if (await fse.pathExists(legacyQuotaPath)) {
111
- quotaConfig = await fse.readJson(legacyQuotaPath)
112
- }
113
-
114
- Object.assign(quotaConfig, quotaPreset.defaults)
115
-
116
- await fse.ensureDir(quotaDir)
117
- await fse.writeJson(quotaPath, quotaConfig, { spaces: 2 })
118
-
119
- if (await fse.pathExists(legacyQuotaPath)) {
120
- await fse.remove(legacyQuotaPath)
121
- const legacyDir = path.dirname(legacyQuotaPath)
122
- if ((await fse.readdir(legacyDir)).length === 0) {
123
- await fse.remove(legacyDir)
124
- }
125
- info('Moved opencode-quota settings to opencode-quota/quota-toast.jsonc')
126
- }
127
-
128
- success('opencode-quota configured (popups and notifications disabled)')
129
- info('Restart OpenCode and run /quota to verify')
130
- return { optedIn: true, installed: true }
131
- } catch (err) {
132
- error(`Failed to configure opencode-quota: ${err.message}`)
133
- return { optedIn: true, installed: false }
134
- }
135
- }
@@ -1,9 +0,0 @@
1
- ---
2
- description: Capture a screenshot (or text summary) of a completed change and post it as a comment on the originating issue or work item.
3
- ---
4
-
5
- Load the `pc-ops-evidence` skill and execute it.
6
-
7
- Input:
8
-
9
- $ARGUMENTS
@@ -1,131 +0,0 @@
1
- ---
2
- name: pc-ops-evidence
3
- description: Writes a capturePlan in evidence.json for the Visual Evidence CI workflow to execute on a runner with Docker and Chrome access. Load after a change is implemented. Invoked by /ops-evidence.
4
- license: MIT
5
- ---
6
-
7
- # Ops Evidence
8
-
9
- Write a capture plan so the Visual Evidence CI workflow can capture screenshots on a runner with full Docker and Chrome access.
10
-
11
- The agent runs inside the awf sandbox where Docker-in-Docker is unsupported and headless Chromium's sandbox is blocked by the container security policy. The agent cannot capture screenshots itself. Instead, it analyzes the diff and writes a `capturePlan` in `evidence.json`. A separate CI workflow executes the plan.
12
-
13
- ## Convention
14
-
15
- Every platform project has `pnpm run dev` at root that starts the **full stack** (database + API + web). The app runs with mock auth in development mode (no real authentication needed). This is the only contract — no per-project evidence harness, fixture apps, or scenario registries.
16
-
17
- ## Input
18
-
19
- The caller provides (all optional):
20
- - change id: locates `openspec/changes/{change-id}/` (or the archived `archive/*{change-id}/`).
21
- - issue / work-item ref and PR number: where to publish.
22
- - output mode (`default` / `push` / `pr`): whether the branch was pushed.
23
- - operation: `capture` (default), `publish`, or `both`.
24
-
25
- ## Part 1: Capture (operation: capture / both)
26
-
27
- **Step 1: Decide whether evidence is required.** Inspect the change's diff:
28
- - Required when changed files include user-visible UI: `*.tsx/jsx/vue/svelte`, `*.css/scss/less`, pages, layouts, components, navigation.
29
- - Skipped when docs-only, internal refactor, dependency-only, test-only, backend-only.
30
- - Mixed or unknown: required (be safe).
31
-
32
- If skipped: write `evidence.json` with `status: "skipped"` and reason. Done.
33
-
34
- **Step 2: Discover routes from git diff.** Parse changed files to determine which routes to screenshot:
35
- - `pages/**/*.tsx` or `app/**/page.tsx` → extract the route path
36
- - `features/**/*.tsx` or `components/**/*.tsx` → screenshot the homepage and any routes that import the changed component
37
- - If no routes found → screenshot `/` only
38
- - Always include `/` (homepage) as a baseline
39
-
40
- **Step 3: Write `evidence.json` with `capturePlan`.** The agent never attempts to start the app stack or launch a browser — those always fail inside the awf sandbox. Instead, write a `capturePlan` immediately.
41
-
42
- ### The `capturePlan` schema
43
-
44
- ```
45
- capturePlan:
46
- routes: # Array of route objects to screenshot (at minimum [{ path: "/" }])
47
- - path: string # URL path, e.g. "/items/:id"
48
- sampleId: # string | "first" | "any" — how to resolve dynamic segments
49
- caption: # string — human-readable description of what this screenshot shows
50
- viewports: # Array of viewport objects
51
- - width: number
52
- height: number
53
- label: string # "desktop" | "mobile" | custom
54
- requireApi: # boolean — true when the route needs the backend API running
55
- requireLogin: # boolean — true when the route needs authentication
56
- loginMethod: # string — "mock-sso" | "none" | custom method identifier
57
- reason: # string — why evidence was blocked and what the screenshots should show
58
- ```
59
-
60
- ### Rules for writing capturePlan
61
-
62
- 1. `routes` MUST always include `{ path: "/", caption: "Homepage" }` as the first entry.
63
- 2. Every additional route discovered from the diff goes after the homepage entry.
64
- 3. `sampleId: "first"` means the CI workflow should use the first record returned by the API. `sampleId: "any"` means any valid ID.
65
- 4. `requireApi` is `true` when any route needs the backend to return data. It is `false` only for purely static pages (login, not-found).
66
- 5. `requireLogin` is `true` when any route needs authentication. For dev mode with mock auth, `loginMethod` is `"mock-sso"`.
67
- 6. `reason` should explain both WHY capture was blocked and WHAT the screenshots should show once captured.
68
-
69
- ### Example `evidence.json`
70
-
71
- Adapt the routes and captions to this repository. Edits between the `PC-PROJECT-EXAMPLE` markers are carried over when the harness updates; anything outside them is replaced by the shipped version.
72
-
73
- <!-- PC-PROJECT-EXAMPLE-START -->
74
- ```json
75
- {
76
- "version": 1,
77
- "changeId": "status-badge-in-detail-panel",
78
- "required": true,
79
- "status": "blocked",
80
- "assets": [],
81
- "capturePlan": {
82
- "routes": [
83
- { "path": "/", "sampleId": "any", "caption": "Homepage" },
84
- { "path": "/items/:id", "sampleId": "first", "caption": "Item detail with the new status badge" }
85
- ],
86
- "viewports": [
87
- { "width": 1280, "height": 720, "label": "desktop" },
88
- { "width": 375, "height": 667, "label": "mobile" }
89
- ],
90
- "requireApi": true,
91
- "requireLogin": true,
92
- "loginMethod": "mock-sso",
93
- "reason": "Status badge added to the item detail panel; visual change on the detail page."
94
- },
95
- "reason": "Visual evidence cannot be captured inside the awf sandbox (Docker-in-Docker unsupported, headless Chromium sandbox blocked). A capturePlan has been written for the Visual Evidence CI workflow.",
96
- "prMarkdown": "## Evidence\n\nVisual evidence for this change is **blocked** in this agent run. A `capturePlan` has been written to `evidence.json` — the Visual Evidence CI workflow will execute it on a runner with full Docker and Chrome access.\n\nAutomated verification that did run:\n- Lint: clean\n- Tests: all pass\n- Build: success"
97
- }
98
- ```
99
- <!-- PC-PROJECT-EXAMPLE-END -->
100
-
101
- ### When evidence is not required
102
-
103
- If evidence is skipped (no UI files changed), write without a `capturePlan`:
104
-
105
- ```json
106
- {
107
- "version": 1,
108
- "changeId": "{change-id}",
109
- "required": false,
110
- "status": "skipped",
111
- "assets": [],
112
- "reason": "No user-visible UI files changed in this PR.",
113
- "prMarkdown": "## Evidence\n\nSkipped: no user-visible UI changes."
114
- }
115
- ```
116
-
117
- Capture never commits, stages, or pushes. The caller owns git.
118
-
119
- ## Part 2: Publish (operation: publish / both)
120
-
121
- Preconditions:
122
- - An issue/PR number was provided. Else skip.
123
- - Image URLs resolve only if the branch was pushed (`pr`/`push` modes).
124
- - Backlog platform from `.opencode/harness.json`; `none` means skip.
125
-
126
- <!-- PC-PLATFORM-EVIDENCE-START -->
127
- <!-- PC-PLATFORM-EVIDENCE-END -->
128
-
129
- ## Report
130
-
131
- One block: the `status` (passed/skipped/failed/blocked) and why; capturePlan written or why not. Never present a blocked capture as passed.