software-defence-factory 0.6.0 → 0.8.0

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.
@@ -4,7 +4,7 @@ import { resolve, join } from 'node:path';
4
4
  import { randomBytes } from 'node:crypto';
5
5
  import { spawn } from 'node:child_process';
6
6
  import { createServer } from 'node:net';
7
- import { ROOT, PINS, DEFAULT_STATE, configAt, save, json, run, stream, digest, api, sleep, stopContainers } from '../factory/lib.mjs';
7
+ import { ROOT, PINS, DEFAULT_STATE, configAt, save, json, run, stream, digest, api, sleep, stopContainers, PUBLICATION_API_TIMEOUT_MS } from '../factory/lib.mjs';
8
8
  import { assertInstalledJobImage, installCustomJobImage, installStandardJobImage, inspectImageInstallation } from '../factory/image-install.mjs';
9
9
  import { listIssues, readIssue } from '../factory/issue-intake.mjs';
10
10
  import { recommendWork } from '../factory/intake.mjs';
@@ -14,6 +14,8 @@ import { admitIncident } from '../factory/incident.mjs';
14
14
  import { DEFAULT_DEMO_STATE } from '../factory/paths.mjs';
15
15
  import { bootstrap, registerInstallation, VERSION } from '../factory/updates.mjs';
16
16
  import { hasService, manageService, serviceDefinition, withServiceOperation, isManagedLaunch } from '../factory/services.mjs';
17
+ import { runCandidateGit } from '../factory/git-environment.mjs';
18
+ import { initializeDemoRepository } from '../factory/demo-fixture.mjs';
17
19
 
18
20
  try {
19
21
  const handled = await bootstrap(process.argv.slice(2));
@@ -32,22 +34,22 @@ let state = resolve(flags.state || DEFAULT_STATE);
32
34
  if(existsSync(state))state=realpathSync(state);
33
35
  const alive = pid => { try { process.kill(pid,0); return true; } catch(error) { if(error.code === 'ESRCH')return false; throw error; } };
34
36
 
35
- function init(repo, harness='codex', check='', port=7331) {
37
+ function init(repo, harness='codex', check='', port=7331, sourceRef='HEAD', delivery, inferenceProvider) {
36
38
  repo=realpathSync(resolve(repo));
37
39
  if (existsSync(join(state,'factory.json'))) throw new Error('Already configured; edit the private factory.json explicitly or choose another --state');
38
40
  if ([repo,state,ROOT].some(p=>/[,\n\r]/.test(p))) throw new Error('Paths cannot contain commas or line breaks');
39
- if (run('git',['-C',repo,'rev-parse','--show-toplevel']) !== repo) throw new Error('--repo must be the Git root');
40
- run('git',['-C',repo,'rev-parse','HEAD']);
41
+ if (runCandidateGit(repo,'rev-parse','--show-toplevel') !== repo) throw new Error('--repo must be the Git root');
42
+ runCandidateGit(repo,'rev-parse','HEAD');
41
43
  const presets={codex:['codex','exec','--json','--ephemeral','--sandbox','danger-full-access','-'],pi:['pi','--mode','json','--print','--no-session','--no-extensions','--skill','/factory-skills'],mock:['node','/opt/factory/mock.mjs']};
42
44
  const argv=harness==='custom'?JSON.parse(flags['command-json'] || 'null'):presets[harness];
43
45
  if (!argv) throw new Error('Select codex, pi, mock or custom with --command-json');
44
46
  if (flags.model && ['codex','pi'].includes(harness)) argv.splice(harness==='codex'?argv.length-1:argv.length,0,'--model',flags.model);
45
47
  mkdirSync(state,{recursive:true,mode:0o700});state=realpathSync(state);chmodSync(state,0o700);
46
- save(join(state,'factory.json'),{version:1,repo,harness,command:argv,check,port:Number(port),image:PINS.jobImage,network:harness==='mock'?'none':'bridge',timeoutSeconds:1800,memoryMiB:2048,model:flags.model || null,
48
+ save(join(state,'factory.json'),{version:1,repo,sourceRef,harness,command:argv,check,port:Number(port),image:PINS.jobImage,network:harness==='mock'?'none':'bridge',timeoutSeconds:1800,memoryMiB:2048,model:flags.model || null,...(inferenceProvider?{inferenceProvider}:{}),...(delivery?{delivery}:{}),
47
49
  scope:{project:'pilot',service:'app',environment:'test',owner:'operator'}});
48
50
  configAt(state);
49
51
  writeFileSync(join(state,'worker.token'),randomBytes(32).toString('hex')+'\n',{mode:0o600});
50
- writeFileSync(join(state,'model.env'),'# Only inference credentials belong here. Never add GitHub, deploy or cloud credentials.\n',{mode:0o600});
52
+ writeFileSync(join(state,'model.env'),'# Supported inference settings only. Never add host, forge, deploy, Docker or cloud identity credentials.\n',{mode:0o600});
51
53
  registerInstallation(state);
52
54
  console.log(`Configured ${state}\nApp files were not changed. Only committed code is cloned into jobs.`);
53
55
  }
@@ -98,10 +100,10 @@ async function stop() {
98
100
  }
99
101
  stopContainers(state);console.log('Controller and its labelled containers stopped.');
100
102
  }
101
- async function submit(workflow,spec) {
103
+ async function submit(workflow,spec,sourceRef=flags['source-ref']) {
102
104
  if(Buffer.byteLength(spec)>240000)throw new Error('Task exceeds 240 KB');
103
105
  const title=workflow==='defence'?'Private incident triage':spec.split('\n').find(s=>s.trim())?.replace(/^#+\s*/, '').slice(0,100)||'Software task';
104
- return api(state,'/api/v1/jobs',{workflow,repository:'app',spec,title});
106
+ return api(state,'/api/v1/jobs',{workflow,repository:'app',spec,title,...(sourceRef===undefined?{}:{source_ref:sourceRef})});
105
107
  }
106
108
  async function jobAction(action) {
107
109
  const id=positional[0];if(!/^job_[a-z0-9]+$/.test(id || ''))throw new Error('A job ID is required');
@@ -118,12 +120,43 @@ async function jobAction(action) {
118
120
  }
119
121
  // The controller validates the current run and owns reconciliation atomically.
120
122
  // A CLI-side stop after a stale snapshot could terminate a newer attempt.
121
- await api(state,`/api/v1/jobs/${id}/${action}`,{run_id:current?.id,...(feedback===undefined?{}:{feedback})});
123
+ await api(state,`/api/v1/jobs/${id}/${action}`,{run_id:current?.id,...(feedback===undefined?{}:{feedback}),...(flags['source-ref']===undefined?{}:{source_ref:flags['source-ref']})});
122
124
  console.log(`${action}: ${id}`);
123
125
  }
124
126
 
127
+ async function publishJob(jobId) {
128
+ if (!/^job_[a-f0-9]{24}$/.test(jobId || '')) throw new Error('publish requires a Factory JOB_ID');
129
+ const snapshot=await api(state,'/api/v1/status'),job=snapshot.jobs.find(item=>item.id===jobId);
130
+ if(!job)throw new Error('Job not found');
131
+ const current=job.runs.at(-1);
132
+ const receipt=await api(state,`/api/v1/jobs/${jobId}/publish`,{run_id:current?.id},undefined,{timeoutMs:PUBLICATION_API_TIMEOUT_MS});
133
+ console.log(JSON.stringify(receipt,null,2));
134
+ }
135
+
136
+ async function abandonDeliveryJob(jobId) {
137
+ if (!/^job_[a-f0-9]{24}$/.test(jobId || '')) throw new Error('abandon-delivery requires a Factory JOB_ID');
138
+ const branchSha = flags['branch-sha'];
139
+ if (!/^[a-f0-9]{40}$/.test(branchSha || '')) throw new Error('abandon-delivery requires --branch-sha with the inspected remote SHA');
140
+ const snapshot=await api(state,'/api/v1/status'),job=snapshot.jobs.find(item=>item.id===jobId);
141
+ if(!job)throw new Error('Job not found');
142
+ const delivery=job.delivery_status;
143
+ if(!delivery?.can_abandon)throw new Error('This job has no resolvable pre-write branch collision; inspect status and reconcile unresolved provider effects.');
144
+ if(branchSha!==delivery.remote_collision?.sha)throw new Error('The supplied branch SHA differs from current status; inspect the current remote branch before resolving.');
145
+ const result=await api(state,`/api/v1/jobs/${jobId}/abandon-delivery`,{
146
+ run_id:job.runs.at(-1)?.id,delivery_identity:delivery.identity,branch_sha:branchSha,
147
+ },undefined,{timeoutMs:PUBLICATION_API_TIMEOUT_MS});
148
+ console.log(JSON.stringify(result,null,2));
149
+ }
150
+
125
151
  try {
126
- if(command==='init') { if(!flags.repo)throw new Error('init requires --repo /path/to/existing/git/repo');if(flags.harness && flags.agent && flags.harness !== flags.agent)throw new Error('--harness conflicts with legacy --agent');init(flags.repo,flags.harness || flags.agent,flags.check,flags.port); }
152
+ if(command==='init') {
153
+ if(!flags.repo)throw new Error('init requires --repo /path/to/existing/git/repo');
154
+ if(flags.harness && flags.agent && flags.harness !== flags.agent)throw new Error('--harness conflicts with legacy --agent');
155
+ const deliveryFlags=[flags['delivery-provider'],flags['delivery-repository'],flags['delivery-target']];
156
+ if(deliveryFlags.some(Boolean)&&deliveryFlags.some(value=>!value))throw new Error('Trusted PR delivery requires --delivery-provider github --delivery-repository URL --delivery-target main|dev');
157
+ const delivery=deliveryFlags.every(Boolean)?{provider:flags['delivery-provider'],repository:flags['delivery-repository'],target:flags['delivery-target']}:undefined;
158
+ init(flags.repo,flags.harness || flags.agent,flags.check,flags.port,flags['source-ref'] || 'HEAD',delivery,flags['inference-provider']);
159
+ }
127
160
  else if(command==='install')await withServiceOperation('install',install);
128
161
  else if(command==='up') { if(hasService(state))await manageService('controller','start',state);else await withServiceOperation('up',up); }
129
162
  else if(command==='stop') { if(hasService(state))await manageService('controller','stop',state);else await withServiceOperation('stop',stop); }
@@ -198,7 +231,7 @@ try {
198
231
  input.title=flags.title || input.title;
199
232
  if(typeof input.title!=='string'||!input.title.trim()||input.title.length>160)throw new Error('Provide a title of 1–160 characters (use --title for a blank issue)');
200
233
  if(flags.workflow==='software'&&!configAt(state).check?.trim())throw new Error('Configure an app check before submitting software work');
201
- console.log(JSON.stringify(await api(state,'/api/v1/jobs',{...input,workflow:flags.workflow,repository:'app',model:flags.model || ''}),null,2));
234
+ console.log(JSON.stringify(await api(state,'/api/v1/jobs',{...input,workflow:flags.workflow,repository:'app',model:flags.model || '',...(flags['source-ref']===undefined?{}:{source_ref:flags['source-ref']})}),null,2));
202
235
  } else throw new Error('Use issue list|connection|templates|preview|recommend|draft|create|start|submissions|recover; see help');
203
236
  } else if(command==='issues') {
204
237
  console.log(JSON.stringify(await listIssues(configAt(state).repo,Number(flags.page || 1)),null,2));
@@ -220,14 +253,14 @@ try {
220
253
  if(!flags.file)throw new Error('Use --file incident.json; see factory/examples/incident.json');
221
254
  console.log(JSON.stringify(await admitIncident(state,json(resolve(flags.file)),submit)));
222
255
  } else if(['approve','cancel','retry'].includes(command))await jobAction(command);
256
+ else if(command==='publish')await publishJob(positional[0]);
257
+ else if(command==='abandon-delivery')await abandonDeliveryJob(positional[0]);
223
258
  else if(command==='revise')await jobAction('request_changes');
224
259
  else if(command==='demo') {
225
260
  state=resolve(flags.state || DEFAULT_DEMO_STATE);
226
261
  const repo=join(state,'sample-app');
227
262
  if(!existsSync(join(state,'factory.json'))) {
228
- mkdirSync(repo,{recursive:true,mode:0o700});run('git',['init','-b','main',repo]);
229
- writeFileSync(join(repo,'value.txt'),'broken\n');run('git',['-C',repo,'add','value.txt']);
230
- run('git',['-C',repo,'-c','user.name=Factory demo','-c','user.email=demo@localhost','commit','-m','Synthetic fixture']);
263
+ initializeDemoRepository(repo);
231
264
  init(repo,'mock',"test \"$(cat value.txt)\" = fixed",Number(flags.port || 7332));
232
265
  } else if(harnessOf(configAt(state))!=='mock')throw new Error('Demo requires a mock configuration');
233
266
  await withServiceOperation('demo startup',async()=>{await install();await up();});console.log(JSON.stringify(await submit('software','Synthetic installation qualification: fix value.txt. No inference is used.')));
@@ -243,7 +276,9 @@ try {
243
276
  kit --output NEW_DIRECTORY Export the portable method without a runtime
244
277
  demo Install and run a synthetic sample (no model key)
245
278
  qualify --state PATH Exercise recovery and isolation with a stopped demo job
246
- init --repo PATH --harness codex|pi|custom --check "npm ci && npm test"
279
+ init --repo PATH --harness codex|pi|custom --check "npm ci && npm test" [--source-ref REF]
280
+ [--inference-provider PROVIDER]
281
+ [--delivery-provider github --delivery-repository https://github.com/OWNER/REPO --delivery-target main|dev]
247
282
  install [--image LOCAL_REF] Build the standard image, or select an existing local image
248
283
  doctor | up | status | stop Inspect / operate your private installation
249
284
  foundation Read the operator setup skill; no installation required
@@ -272,15 +307,19 @@ try {
272
307
  issue connection | submissions Inspect provider identity or durable creation receipts
273
308
  issue recover --key REQUEST_ID Reconcile an uncertain creation without another write
274
309
  issue start --draft draft.json | --url URL | --file brief.md --title TITLE
275
- --workflow software|defence [--model MODEL]
310
+ --workflow software|defence [--source-ref REF] [--model MODEL]
276
311
  Create a local issue and start work; no GitHub write
277
312
  issues [--page N] Browse open project issues, with next_page for more
278
313
  recommend --file task.md | --issue URL Suggest a work type without starting work
279
314
  run --file task.md | --issue URL Submit software (default), or --workflow defence
315
+ [--source-ref REF] Pin a configured-repository ref before admission
280
316
  incident --file incident.json Submit a private, read-only incident draft
281
317
  approve JOB_ID | cancel JOB_ID Review gate / stop this attempt
318
+ publish JOB_ID Publish/reconcile the accepted candidate as one draft PR
319
+ abandon-delivery JOB_ID --branch-sha SHA Resolve an inspected pre-write collision; keep the remote branch
282
320
  retry JOB_ID Prove stop; retain old checkout and retry
283
- revise JOB_ID --file feedback.md New build/check/review after a stopped review
321
+ revise JOB_ID --file feedback.md [--source-ref REF]
322
+ New build/check/review; source stays pinned unless a new ref is explicit
284
323
  version Show the active CLI version
285
324
  update | update --check Update the npm CLI / inspect the latest release
286
325
  update --auto on|off Control automatic daily CLI updates
@@ -289,7 +328,7 @@ Runtime commands accept --state PATH. Default: ${DEFAULT_STATE}
289
328
  Demo default: ${DEFAULT_DEMO_STATE}
290
329
  The npm CLI keeps state outside the package; updates wait for stopped installations.
291
330
  Dashboard binds only to loopback; use SSH for remote access.
292
- Setup plan: ${join(ROOT, 'docs/setup.md')}
331
+ Setup plan: ${join(ROOT, 'docs/setup.md')}
293
332
  See docs/quickstart.md for task execution, evidence and recovery.`);
294
333
  else throw new Error(`Unknown command: ${command}`);
295
334
  } catch(error) {console.error(`Factory: ${error.message}`);process.exitCode=1;}
@@ -32,8 +32,8 @@ shell endpoint or a second scheduler.
32
32
  | SSH tunnels | `tunnel` | No tunnel endpoint | None | Client-host ownership; distinguish operator machine from worker |
33
33
  | Method export | `kit --output` | No export endpoint | None | Equivalent download/export preserving staging-only adoption |
34
34
  | Synthetic qualification | `demo`, `qualify` | No qualification endpoint | Synthetic disclosure only | Explicit separate state; never target an application accidentally |
35
- | Immutable source admission | Controlled-checkout workaround | Not implemented | Not implemented | #28, same recorded source in both interfaces |
36
- | Trusted PR handoff | Operator applies accepted patch | Not implemented | Not implemented | #29; credentials remain outside jobs |
35
+ | Immutable source admission | `init --source-ref`, `run --source-ref`, `issue start --source-ref`; status and build evidence carry the resolved SHA | `POST /api/v1/jobs` resolves/retains before acknowledgement; shared source metadata in status | New issue and revision forms accept a ref; task detail shows requested ref, resolved SHA and prior source commits | Build/retry use retained objects; revisions keep the recorded source unless a new ref is explicit; legacy source remains unknown |
36
+ | Trusted PR handoff | `publish JOB_ID` publishes/reconciles; `abandon-delivery JOB_ID --branch-sha SHA` records a checked local resolution for a pre-write branch collision | Authenticated `POST /api/v1/jobs/:id/publish` and `/abandon-delivery`; shared receipt, conflict identity and removal policy | Publish/reconcile and explicit “Abandon local delivery; keep remote branch” actions share controller state; errors/results and inspected branch identity are visible | New writes require matching protected Codex/Pi build/review provenance, deterministic verify/handoff provenance, non-synthetic bound artifacts and a qualified GitHub Actions tree. Shared delivery status exposes `workflow_qualification` and the same reason blocks CLI/API/dashboard capability and publication/retry. Candidate workflow changes, unsupported triggers/syntax, or active generated-push, selected-ref-dispatch and PR jobs with write/secrets/environment/OIDC/deploy access, self-hosted runners or ambiguous privileged guards refuse trusted writes. Supported ASCII guard comparisons follow GitHub's case-insensitive string semantics; unknown PR refs, non-ASCII mismatches, and glob/escaped branch filters cannot prove a privileged job inactive. The shared summary's `action_mode` distinguishes new/resumable publication from read-only reconciliation and drives idle and pending task button wording. Branch-only collisions and unknown/abandoned states offer neither; known PR receipts and pending PR-creation checkpoints retain read-only reconciliation. Abandonment checks the current run, saved intent, exact branch head and absence of an associated PR; it writes no provider data, preserves the remote branch/evidence, disables republishing and permits local removal. Uncertain effects and incompatible evidence stay blocked. Destination remains private operator config; patch-only remains default |
37
37
 
38
38
  The current generic task form can name the Defence workflow; that is not a
39
39
  substitute for the CLI's validated incident admission. Treat the typed intake
@@ -23,32 +23,146 @@ The qualification intentionally creates failed, cancelled and interrupted tasks.
23
23
  Commit an intentional, reviewed starting point in the application first. Jobs clone committed code only; uncommitted work stays in the source checkout.
24
24
 
25
25
  ```sh
26
- software-defence-factory init --repo /absolute/path/to/app --harness codex --check "npm ci && npm test" --state /private/state/my-app --port 7331
26
+ software-defence-factory init --repo /absolute/path/to/app --harness codex --check "npm ci && npm test" --source-ref main --state /private/state/my-app --port 7331
27
27
  software-defence-factory install --state /private/state/my-app
28
28
  software-defence-factory doctor --state /private/state/my-app
29
29
  ```
30
30
 
31
- Verification commands receive `FACTORY_BASE_REVISION`, the resolved commit recorded as the candidate base. Diff-based checks should compare against this revision; the isolated checkout has no origin remote. The value comes from protected controller metadata, not the task text.
31
+ `init --source-ref` selects the configured default ref (`HEAD` when omitted). Each job resolves that ref, or an explicit `--source-ref` on `run`/`issue start`, in the configured repository and durably retains its commit before acknowledging admission. It records the canonical GitHub origin identity when available; the CLI and dashboard show the requested ref and resolved SHA. Task text and reference links do not select a repository, source ref or PR target.
32
32
 
33
- Replace the check with the application's actual verification command. `init` does not edit the app, copy global skills or start work. It creates factory.json, worker.token and model.env with private permissions. Each installation has one repository and a distinct state path/port. `--harness pi` selects Pi; `--harness custom --command-json '["executable","argument"]'` selects an available command in the job image. The bundled image provides Node, Git, Codex and Pi. Other toolchains require an intentionally built compatible image; do not claim Rust/mobile/browser capabilities from this image alone.
33
+ Verification commands receive `FACTORY_BASE_REVISION`, the resolved admission commit recorded as the candidate base. Diff-based checks should compare against this revision; the isolated checkout has no origin remote. The value comes from protected controller metadata, not the task text.
34
34
 
35
- Configure inference credentials in the private model.env file. Do not copy the operator's entire account environment or authentication folders. Codex uses its supported API credential environment; Pi uses the selected provider's configuration. Use `--model` with init for a specific model. Task-level model overrides are supported only for Codex/Pi and do not prove that the provider serves that model.
35
+ Replace the check with the application's actual verification command. `init` does not edit the app, copy global skills or start work. It creates factory.json, worker.token and model.env with private permissions. Each installation has one repository and a distinct state path/port. `--harness pi` selects Pi; `--harness custom --command-json '["executable","argument"]'` selects an available command in the job image. The bundled image provides Node, Git, Codex and Pi. Other toolchains require an intentionally built compatible image; do not claim Rust/mobile/browser capabilities from this image alone.
36
36
 
37
- A local model endpoint must be reachable from inside the job container. Host loopback addresses do not automatically refer to the host from Docker. Configure and qualify the chosen adapter/network path before dispatch; this package does not automatically expose Ollama or import its models.
37
+ Configure supported inference settings in the private `model.env` file. It
38
+ rejects unrelated names such as `DEPLOY_TOKEN`; do not copy the operator's
39
+ account environment or authentication folders. Codex receives only its OpenAI
40
+ settings. Pi receives only the selected provider's settings. Set
41
+ `--inference-provider PROVIDER` during `init`, or pin an operator-selected
42
+ `provider/model` with `--model`; if multiple provider credential groups are in
43
+ `model.env`, an explicit provider is required. Task-level model overrides never
44
+ select a credential group. See the `inferenceProvider` entry in private
45
+ `factory.json` when editing trusted installation configuration directly.
46
+
47
+ Installations using the existing Codex account-auth command may also put its
48
+ single-line JSON object in `FACTORY_CODEX_AUTH_JSON` in private `model.env`.
49
+ Factory validates that value as JSON and gives it only to the selected Codex
50
+ build/review/defence worker through a temporary private env file; the configured
51
+ operator command that consumes this setting must materialize its temporary
52
+ native `auth.json` before invoking Codex; the bundled stock `codex exec` does
53
+ not do this by itself. Pi never receives this setting, including with the OpenAI provider, and
54
+ deterministic checks and output artifacts do not receive it. Keep `model.env`
55
+ private with mode `0600`; do not copy account folders or place the value in a
56
+ task, source file or report. This setting does not replace provider API-key or
57
+ local OpenAI-compatible endpoint configuration.
58
+
59
+ Factory's Pi provider identifiers are `anthropic`, `azure-openai-responses`,
60
+ `cerebras`, `cloudflare-ai-gateway`, `cloudflare-workers-ai`, `deepseek`,
61
+ `google`, `groq`, `huggingface`, `kimi-coding`, `minimax`, `minimax-cn`,
62
+ `mistral`, `openai`, `opencode`, `opencode-go`, `openrouter`,
63
+ `vercel-ai-gateway`, `xiaomi`, `xiaomi-token-plan-ams`,
64
+ `xiaomi-token-plan-cn`, `xiaomi-token-plan-sgp`, `xai` and `zai`. Factory
65
+ forwards provider API-key and endpoint settings from this map; it does not
66
+ forward OAuth files or ambient cloud identity credentials.
67
+
68
+ `OPENAI_BASE_URL` remains available for an OpenAI-compatible local endpoint
69
+ selected for Codex; the endpoint must be reachable from inside the job
70
+ container. Host loopback addresses do not automatically refer to the host from
71
+ Docker. The package does not automatically expose Ollama or import models.
38
72
 
39
73
  ```sh
40
74
  software-defence-factory up --state /private/state/my-app
41
- software-defence-factory run --file task.md --state /private/state/my-app
75
+ software-defence-factory run --file task.md --source-ref main --state /private/state/my-app
42
76
  ```
43
77
 
44
78
  A task should describe the accepted outcome, allowed scope and observable checks. The CLI also accepts `--issue https://github.com/owner/repo/issues/123` for an issue belonging to the configured origin; it uses the operator's existing gh access outside the job. The dashboard supports the same task workflow. Source text and links do not grant additional authority.
45
79
 
46
80
  ## Review and handoff
47
81
 
48
- Inspect the task's Result, Files and History tabs. Build evidence includes candidate.json, change.patch and the implementation report. Checks and review identify their exact commit and policy hash. Approval revalidates both before writing accepted.json. Request changes creates a new implementation sequence while preserving earlier attempts.
82
+ Inspect the task's Result, Files and History tabs. Task details show the requested source ref, resolved admission SHA and previous source commits when a new base was selected. Build evidence includes candidate.json, change.patch and the implementation report; handoff records its source SHA. Checks and review identify their exact candidate commit and policy hash. Approval revalidates both before writing accepted.json. Request changes preserves the recorded source by default and starts fresh checks/review; an explicit new source ref is retained as a deliberate base change.
49
83
 
50
84
  The source application is not changed and no branch, PR, merge or deployment is published automatically. A reviewed change.patch can be checked and applied with `git apply --check` and `git apply` on an appropriate branch at its recorded base revision; then follow the application's normal integrated checks and delivery policy.
51
85
 
86
+ ### Optional trusted PR delivery
87
+
88
+ Patch-only remains the default. To enable the first delivery provider, select a
89
+ canonical GitHub origin and an explicit target while initializing the private
90
+ installation:
91
+
92
+ ```sh
93
+ software-defence-factory init --repo /absolute/path/to/app --harness codex \
94
+ --check "npm ci && npm test" --source-ref main \
95
+ --delivery-provider github \
96
+ --delivery-repository https://github.com/OWNER/REPO \
97
+ --delivery-target main --state /private/state/my-app
98
+ ```
99
+
100
+ Use `dev` only when it is the intended target. The source ref (`main` above)
101
+ and PR target are separate settings. The configured GitHub repository must
102
+ match the canonical origin captured at admission; a later repository rename or
103
+ remote change blocks delivery. Configure this before admitting work because a
104
+ configuration change invalidates earlier check/review/approval policy evidence.
105
+ Unknown providers and installations without `delivery` configuration keep the
106
+ patch-only flow.
107
+
108
+ After the ordinary check, independent review and operator approval complete,
109
+ use **Publish accepted candidate as draft PR** in task details or run:
110
+
111
+ ```sh
112
+ software-defence-factory publish JOB_ID --state /private/state/my-app
113
+ ```
114
+
115
+ Trusted publication requires protected per-run execution records for native
116
+ Codex/Pi build and review plus deterministic verification and handoff, bound to
117
+ non-synthetic candidate, check and review artifacts. Mock qualification remains
118
+ local exploration and cannot be published; missing or inconsistent provenance
119
+ keeps the action unavailable in status, CLI, API and dashboard. A saved intent
120
+ is checked again before new provider writes. Known PR receipts and PR-creation
121
+ checkpoints still allow read-only reconciliation.
122
+
123
+ New writes also require the shared GitHub Actions qualification described in
124
+ [setup](setup.md#optional-trusted-pr-delivery) and [recovery](recovery.md#trusted-pr-delivery).
125
+ Unsupported or candidate-changed workflows keep publication unavailable in
126
+ status, CLI, API and dashboard; the accepted patch remains available for normal
127
+ manual delivery.
128
+
129
+ For `push`, the qualifier evaluates branch filters when either `branches` or
130
+ `branches-ignore` is declared, even alongside tag filters; tag-only filters do
131
+ not activate a generated branch push. Docker phase logs redact literal selected
132
+ inference credential values before retention. Mounted worker reports are
133
+ redacted after the container stops and before promotion; if shutdown is
134
+ uncertain, recovery handles them after confirming the stop. This is a bounded
135
+ output filter, not general protection against encoded or transformed values.
136
+
137
+ The controller uses its existing `gh` identity. GitHub credentials are never
138
+ copied to `model.env` or mounted into jobs. The intent, generated branch, PR
139
+ identity, actual base/head/tree and triggered PR check results appear in the
140
+ same job. Unknown and pending checks remain visible and are not reported as
141
+ success. GitHub's raw `success`, `skipped` and `neutral` conclusions are
142
+ non-blocking in the Factory check summary; skipped and neutral remain visibly
143
+ distinct from an executed passing check. Unknown conclusions and incomplete
144
+ pagination keep the aggregate unknown. This summary does not determine branch
145
+ protection requirements or grant merge authorization. See [GitHub's required
146
+ status check guidance](https://docs.github.com/en/pull-requests/how-tos/merge-and-close-pull-requests/troubleshooting-required-status-checks).
147
+ Repeating `publish` refreshes/reconciles the same branch and PR; it does not
148
+ create a second PR or overwrite a changed branch. The CLI gives this bounded
149
+ multi-request action ten minutes; branch resolution uses the same bound and
150
+ other API calls keep their five-second deadline. If a client deadline expires,
151
+ inspect status and repeat `publish` to reconcile or recheck the reported branch
152
+ identity before `abandon-delivery`. Delete issue stays disabled while delivery
153
+ is unresolved, and the controller enforces the same guard on its API.
154
+ Publication does not merge, integrate, release or deploy. See [delivery
155
+ recovery](recovery.md#trusted-pr-delivery).
156
+
157
+ If status reports a reserved branch collision at the untouched `intent` stage,
158
+ inspect that branch in GitHub first. The task detail action **Abandon local
159
+ delivery; keep remote branch** or `abandon-delivery JOB_ID --branch-sha SHA`
160
+ records only a local resolution after the controller confirms the same branch
161
+ head and no associated PR. It preserves the remote branch and accepted evidence,
162
+ permits local issue removal, and permanently disables publication for that
163
+ delivery record. Changed identities, PRs and uncertain provider effects remain
164
+ blocked; see [recovery](recovery.md#trusted-pr-delivery).
165
+
52
166
  ## Remote access and operation
53
167
 
54
168
  ```sh
@@ -107,8 +221,11 @@ is tested at 100%; changing browser zoom is separate from a project theme.
107
221
  ## Environment
108
222
 
109
223
  Factory does not load a repository `.env` file. Configure the private
110
- `factory.json` through `init`; put inference credentials only in its private
111
- `model.env`. A repository `.env.example` is unnecessary for this CLI. Optional
224
+ `factory.json` through `init`; put supported inference settings only in its
225
+ private `model.env`. A provider allowlist selects the configured Codex/Pi
226
+ settings before a worker starts; unrelated host, forge, deployment, cloud
227
+ identity and application variables are rejected. A repository `.env.example`
228
+ is unnecessary for this CLI. Optional
112
229
  process settings are `SDF_AUTO_UPDATE=0` (skip automatic CLI update checks),
113
230
  `XDG_STATE_HOME`, `XDG_DATA_HOME` and `XDG_CONFIG_HOME` (user-owned state, release
114
231
  and service locations). They must be exported in the process environment.