@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.
- package/README.md +434 -438
- package/cli/commands/update.js +121 -121
- package/cli/presets/optimization.json +0 -5
- package/cli/steps/copy/commands.js +79 -95
- package/cli/steps/copy/index.js +88 -88
- package/cli/steps/copy/opencode-json.js +6 -7
- package/cli/steps/copy/skills.js +307 -299
- package/cli/steps/metadata/index.js +115 -112
- package/cli/steps/models/write.js +38 -34
- package/cli/steps/optimization/index.js +1 -6
- package/cli/utils/paths.js +80 -67
- package/package.json +1 -1
- package/skills/pc-plan-goal/SKILL.md +5 -1
- package/skills/pc-repo-help/SKILL.md +87 -89
- package/skills/pc-repo-initialize/SKILL.md +11 -4
- package/skills/pc-repo-onboard/SKILL.md +32 -32
- package/skills/pc-repo-verify/SKILL.md +150 -123
- package/cli/fragments/ops-evidence/az.md +0 -44
- package/cli/fragments/ops-evidence/gh.md +0 -53
- package/cli/fragments/ops-evidence/jira.md +0 -42
- package/cli/presets/quota.json +0 -20
- package/cli/steps/optimization/quota.js +0 -135
- package/harness/.opencode/commands/ops-evidence.md +0 -9
- package/skills/pc-ops-evidence/SKILL.md +0 -131
|
@@ -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.
|
package/cli/presets/quota.json
DELETED
|
@@ -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,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.
|