software-defence-factory 0.5.1 → 0.7.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.
package/README.md CHANGED
@@ -37,7 +37,7 @@ The runtime supplies policy and six focused skills to its isolated jobs. `init`
37
37
 
38
38
  Each result belongs to a specific candidate commit and policy. A failed check blocks delivery. Changing the candidate or check policy invalidates earlier evidence. Approval records a handoff; publishing, merging and deployment follow the application's separate authority.
39
39
 
40
- The project dashboard has an **Inbox**, measured **Analytics**, **Agents**, **Skills**, **Automations**, **Definition** and **Infrastructure**. New issue offers the repository’s issue templates, a blank local form or a selectable GitHub issue. Review the suggested work type before Create & start. CLI `issue` exposes the same intake. Definition lives with settings above the theme control. The CLI reads the same definition and controller state. Agent roles use a selected harness such as Codex or Pi; a worker executes their isolated jobs on a host. See [concepts](docs/concepts.md) and [supported interfaces](docs/interfaces.md). Automations are not implemented yet; work starts manually.
40
+ The project dashboard has an **Inbox**, measured **Analytics**, **Agents**, **Skills**, **Automations**, **Definition** and **Infrastructure**. New issue offers the repository’s issue templates, a blank local form or a selectable GitHub issue. Create an issue on the supported repository provider, then choose Start work separately; local brief execution remains available. CLI `issue` exposes the same intake. Definition lives with settings above the theme control. The CLI reads the same definition and controller state. Agent roles use a selected harness such as Codex or Pi; a worker executes their isolated jobs on a host. See [concepts](docs/concepts.md) and [supported interfaces](docs/interfaces.md). Optional automations belong to the selected harness, which calls Factory CLI/API. Factory runs no cron scheduler. See [provider boundaries](docs/integrations.md).
41
41
 
42
42
  The optional **defence** workflow accepts scoped incident evidence and produces a private, read-only draft. It does not monitor production or claim verified recovery. See [defence integration](docs/defence-integration.md).
43
43
 
@@ -7,7 +7,6 @@ import { createServer } from 'node:net';
7
7
  import { ROOT, PINS, DEFAULT_STATE, configAt, save, json, run, stream, digest, api, sleep, stopContainers } 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
- import { readTemplates, draftFromTemplate } from '../factory/issue-templates.mjs';
11
10
  import { recommendWork } from '../factory/intake.mjs';
12
11
  import { factoryDefinition, foundationSkill } from '../factory/definition.mjs';
13
12
  import { harnessOf } from '../factory/lib.mjs';
@@ -15,6 +14,8 @@ import { admitIncident } from '../factory/incident.mjs';
15
14
  import { DEFAULT_DEMO_STATE } from '../factory/paths.mjs';
16
15
  import { bootstrap, registerInstallation, VERSION } from '../factory/updates.mjs';
17
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';
18
19
 
19
20
  try {
20
21
  const handled = await bootstrap(process.argv.slice(2));
@@ -33,18 +34,18 @@ let state = resolve(flags.state || DEFAULT_STATE);
33
34
  if(existsSync(state))state=realpathSync(state);
34
35
  const alive = pid => { try { process.kill(pid,0); return true; } catch(error) { if(error.code === 'ESRCH')return false; throw error; } };
35
36
 
36
- function init(repo, harness='codex', check='', port=7331) {
37
+ function init(repo, harness='codex', check='', port=7331, sourceRef='HEAD') {
37
38
  repo=realpathSync(resolve(repo));
38
39
  if (existsSync(join(state,'factory.json'))) throw new Error('Already configured; edit the private factory.json explicitly or choose another --state');
39
40
  if ([repo,state,ROOT].some(p=>/[,\n\r]/.test(p))) throw new Error('Paths cannot contain commas or line breaks');
40
- if (run('git',['-C',repo,'rev-parse','--show-toplevel']) !== repo) throw new Error('--repo must be the Git root');
41
- 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');
42
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']};
43
44
  const argv=harness==='custom'?JSON.parse(flags['command-json'] || 'null'):presets[harness];
44
45
  if (!argv) throw new Error('Select codex, pi, mock or custom with --command-json');
45
46
  if (flags.model && ['codex','pi'].includes(harness)) argv.splice(harness==='codex'?argv.length-1:argv.length,0,'--model',flags.model);
46
47
  mkdirSync(state,{recursive:true,mode:0o700});state=realpathSync(state);chmodSync(state,0o700);
47
- 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,
48
49
  scope:{project:'pilot',service:'app',environment:'test',owner:'operator'}});
49
50
  configAt(state);
50
51
  writeFileSync(join(state,'worker.token'),randomBytes(32).toString('hex')+'\n',{mode:0o600});
@@ -99,10 +100,10 @@ async function stop() {
99
100
  }
100
101
  stopContainers(state);console.log('Controller and its labelled containers stopped.');
101
102
  }
102
- async function submit(workflow,spec) {
103
+ async function submit(workflow,spec,sourceRef=flags['source-ref']) {
103
104
  if(Buffer.byteLength(spec)>240000)throw new Error('Task exceeds 240 KB');
104
105
  const title=workflow==='defence'?'Private incident triage':spec.split('\n').find(s=>s.trim())?.replace(/^#+\s*/, '').slice(0,100)||'Software task';
105
- 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})});
106
107
  }
107
108
  async function jobAction(action) {
108
109
  const id=positional[0];if(!/^job_[a-z0-9]+$/.test(id || ''))throw new Error('A job ID is required');
@@ -119,12 +120,12 @@ async function jobAction(action) {
119
120
  }
120
121
  // The controller validates the current run and owns reconciliation atomically.
121
122
  // A CLI-side stop after a stale snapshot could terminate a newer attempt.
122
- 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']})});
123
124
  console.log(`${action}: ${id}`);
124
125
  }
125
126
 
126
127
  try {
127
- 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); }
128
+ 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,flags['source-ref'] || 'HEAD'); }
128
129
  else if(command==='install')await withServiceOperation('install',install);
129
130
  else if(command==='up') { if(hasService(state))await manageService('controller','start',state);else await withServiceOperation('up',up); }
130
131
  else if(command==='stop') { if(hasService(state))await manageService('controller','stop',state);else await withServiceOperation('stop',stop); }
@@ -153,7 +154,7 @@ try {
153
154
  }
154
155
  else if(['infrastructure','automations','inbox'].includes(command)) {
155
156
  const snapshot=await api(state,'/api/v1/status');
156
- console.log(JSON.stringify(command==='inbox'?snapshot.jobs:snapshot[command],null,2));
157
+ console.log(JSON.stringify(command==='inbox'?snapshot.jobs:command==='automations'?snapshot.automation_control:snapshot[command],null,2));
157
158
  }
158
159
  else if(command==='status') { const snapshot=await api(state,'/api/v1/status');delete snapshot.csrf_token;console.log(JSON.stringify(snapshot,null,2)); }
159
160
  else if(command==='doctor') {
@@ -161,32 +162,46 @@ try {
161
162
  console.log(JSON.stringify({node:process.version,docker:dockerVersion,engineInstalled:imageStatus.installed,image:imageStatus.image,repo:config.repo,harness:harnessOf(config),agent:harnessOf(config),checksConfigured:!!config.check?.trim(),inference:'Not called or verified',qualification:{model:'not assessed',toolchain:'not assessed'},dashboard:`http://127.0.0.1:${config.port}`},null,2));
162
163
  if(!imageStatus.installed)process.exitCode=1;
163
164
  } else if(command==='issue') {
164
- const action=positional[0];
165
+ const action=positional[0], sourceURL=flags.url || flags.github;
165
166
  if(action==='list') {
166
- if(flags.source && !['factory','github'].includes(flags.source))throw new Error('Choose --source factory or github');
167
- console.log(JSON.stringify(flags.source==='github' ? await listIssues(configAt(state).repo,Number(flags.page || 1)) : (await api(state,'/api/v1/status')).jobs,null,2));
168
- } else if(action==='templates') console.log(JSON.stringify(await readTemplates(configAt(state).repo),null,2));
167
+ if(flags.source && !['factory','remote','github'].includes(flags.source))throw new Error('Choose --source factory or remote');
168
+ console.log(JSON.stringify(['github','remote'].includes(flags.source) ? await api(state,`/api/v1/issues?page=${encodeURIComponent(flags.page || 1)}`) : (await api(state,'/api/v1/status')).jobs,null,2));
169
+ } else if(action==='templates') console.log(JSON.stringify(await api(state,'/api/v1/issue-templates'),null,2));
170
+ else if(action==='connection') console.log(JSON.stringify(await api(state,'/api/v1/issue-connection'),null,2));
171
+ else if(action==='submissions') console.log(JSON.stringify(await api(state,'/api/v1/issue-submissions'),null,2));
172
+ else if(action==='recover') {
173
+ if(!/^[A-Za-z0-9_-]{16,100}$/.test(flags.key || ''))throw new Error('Use --key with the saved request ID');
174
+ console.log(JSON.stringify(await api(state,`/api/v1/issue-submissions/${flags.key}/recover`,{}),null,2));
175
+ }
169
176
  else if(action==='preview') {
170
- if(!flags.github)throw new Error('Use --github ISSUE_URL');
171
- console.log(JSON.stringify(await readIssue(configAt(state).repo,flags.github),null,2));
177
+ if(!sourceURL)throw new Error('Use --url ISSUE_URL');
178
+ console.log(JSON.stringify(await api(state,'/api/v1/issues/preview',{url:sourceURL}),null,2));
172
179
  } else if(action==='recommend') {
173
- if(Boolean(flags.github)===Boolean(flags.file))throw new Error('Choose --file brief.md or --github ISSUE_URL');
174
- console.log(JSON.stringify(flags.github ? (await readIssue(configAt(state).repo,flags.github)).recommendation : recommendWork({spec:readFileSync(resolve(flags.file),'utf8')}),null,2));
180
+ if(Boolean(sourceURL)===Boolean(flags.file))throw new Error('Choose --file brief.md or --url ISSUE_URL');
181
+ console.log(JSON.stringify(sourceURL ? (await api(state,'/api/v1/issues/preview',{url:sourceURL})).recommendation : recommendWork({spec:readFileSync(resolve(flags.file),'utf8')}),null,2));
175
182
  } else if(action==='draft') {
176
183
  if(!flags.file||!flags.template||!flags.sha)throw new Error('Use --template NAME --sha SHA --file answers.json with {title, answers}');
177
- console.log(JSON.stringify(await draftFromTemplate(configAt(state).repo,{...json(resolve(flags.file)),template:flags.template,sha:flags.sha}),null,2));
184
+ console.log(JSON.stringify(await api(state,'/api/v1/issue-templates/draft',{...json(resolve(flags.file)),template:flags.template,sha:flags.sha}),null,2));
178
185
  } else if(action==='create') {
186
+ if(flags.workflow||sourceURL)throw new Error('issue create publishes a new repository issue. Use issue start for execution.');
187
+ if(!flags.key)throw new Error('Provide a stable --key for safe retry and recovery.');
188
+ if(Boolean(flags.draft)===Boolean(flags.file))throw new Error('Choose --draft draft.json or --file brief.md --title TITLE');
189
+ const draft=flags.draft ? json(resolve(flags.draft)) : {title:flags.title,spec:readFileSync(resolve(flags.file),'utf8'),labels:[]};
190
+ const connection=await api(state,'/api/v1/issue-connection');
191
+ if(!connection.supported)throw new Error('No issue provider is available. Use issue start for a local brief.');
192
+ console.log(JSON.stringify(await api(state,'/api/v1/issues',{title:flags.title || draft.title,spec:draft.spec,labels:draft.labels || [],request_id:flags.key,repository:connection.repository,actor:connection.actor}),null,2));
193
+ } else if(action==='start') {
179
194
  if(!['software','defence'].includes(flags.workflow))throw new Error('Review the issue and choose --workflow software or defence');
180
- if([flags.file,flags.github,flags.draft].filter(Boolean).length!==1)throw new Error('Choose --file brief.md, --draft draft.json or --github ISSUE_URL');
195
+ if([flags.file,sourceURL,flags.draft].filter(Boolean).length!==1)throw new Error('Choose --file brief.md, --draft draft.json or --url ISSUE_URL');
181
196
  let input;
182
- if(flags.github) { const issue=await readIssue(configAt(state).repo,flags.github);input={title:issue.title,spec:issue.spec,source_url:issue.url}; }
197
+ if(sourceURL) { const issue=await api(state,'/api/v1/issues/preview',{url:sourceURL});input={title:issue.title,spec:issue.spec,source_url:issue.url}; }
183
198
  else if(flags.draft) { const draft=json(resolve(flags.draft));input={title:draft.title,spec:draft.spec}; }
184
199
  else input={title:flags.title,spec:readFileSync(resolve(flags.file),'utf8')};
185
200
  input.title=flags.title || input.title;
186
201
  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)');
187
202
  if(flags.workflow==='software'&&!configAt(state).check?.trim())throw new Error('Configure an app check before submitting software work');
188
- console.log(JSON.stringify(await api(state,'/api/v1/jobs',{...input,workflow:flags.workflow,repository:'app',model:flags.model || ''}),null,2));
189
- } else throw new Error('Use issue list|templates|preview|recommend|draft|create; see help');
203
+ 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));
204
+ } else throw new Error('Use issue list|connection|templates|preview|recommend|draft|create|start|submissions|recover; see help');
190
205
  } else if(command==='issues') {
191
206
  console.log(JSON.stringify(await listIssues(configAt(state).repo,Number(flags.page || 1)),null,2));
192
207
  } else if(command==='recommend') {
@@ -212,9 +227,7 @@ try {
212
227
  state=resolve(flags.state || DEFAULT_DEMO_STATE);
213
228
  const repo=join(state,'sample-app');
214
229
  if(!existsSync(join(state,'factory.json'))) {
215
- mkdirSync(repo,{recursive:true,mode:0o700});run('git',['init','-b','main',repo]);
216
- writeFileSync(join(repo,'value.txt'),'broken\n');run('git',['-C',repo,'add','value.txt']);
217
- run('git',['-C',repo,'-c','user.name=Factory demo','-c','user.email=demo@localhost','commit','-m','Synthetic fixture']);
230
+ initializeDemoRepository(repo);
218
231
  init(repo,'mock',"test \"$(cat value.txt)\" = fixed",Number(flags.port || 7332));
219
232
  } else if(harnessOf(configAt(state))!=='mock')throw new Error('Demo requires a mock configuration');
220
233
  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.')));
@@ -230,7 +243,7 @@ try {
230
243
  kit --output NEW_DIRECTORY Export the portable method without a runtime
231
244
  demo Install and run a synthetic sample (no model key)
232
245
  qualify --state PATH Exercise recovery and isolation with a stopped demo job
233
- init --repo PATH --harness codex|pi|custom --check "npm ci && npm test"
246
+ init --repo PATH --harness codex|pi|custom --check "npm ci && npm test" [--source-ref REF]
234
247
  install [--image LOCAL_REF] Build the standard image, or select an existing local image
235
248
  doctor | up | status | stop Inspect / operate your private installation
236
249
  foundation Read the operator setup skill; no installation required
@@ -248,22 +261,28 @@ try {
248
261
  service resume Release a reconciled maintenance reservation
249
262
  tunnel install|start|stop|status|logs|uninstall --host SSH_ALIAS --port PORT
250
263
  Persistent loopback SSH tunnel (macOS/Linux)
251
- issue list [--source github] [--page N] List local Factory issues or open GitHub issues
264
+ issue list [--source remote] [--page N] List local executions or open repository issues
252
265
  issue templates Read this repository's issue forms and contact links
253
- issue preview --github URL Preview one GitHub issue without starting work
254
- issue recommend --file brief.md | --github URL
266
+ issue preview --url URL Preview one repository issue without starting work
267
+ issue recommend --file brief.md | --url URL
255
268
  issue draft --template NAME --sha SHA --file answers.json
256
269
  Validate {title,answers}; output a local draft JSON
257
- issue create --draft draft.json | --github URL | --file brief.md --title TITLE
258
- --workflow software|defence [--model MODEL]
270
+ issue create --draft draft.json | --file brief.md --title TITLE --key REQUEST_ID
271
+ Create a repository issue; does not execute work
272
+ issue connection | submissions Inspect provider identity or durable creation receipts
273
+ issue recover --key REQUEST_ID Reconcile an uncertain creation without another write
274
+ issue start --draft draft.json | --url URL | --file brief.md --title TITLE
275
+ --workflow software|defence [--source-ref REF] [--model MODEL]
259
276
  Create a local issue and start work; no GitHub write
260
277
  issues [--page N] Browse open project issues, with next_page for more
261
278
  recommend --file task.md | --issue URL Suggest a work type without starting work
262
279
  run --file task.md | --issue URL Submit software (default), or --workflow defence
280
+ [--source-ref REF] Pin a configured-repository ref before admission
263
281
  incident --file incident.json Submit a private, read-only incident draft
264
282
  approve JOB_ID | cancel JOB_ID Review gate / stop this attempt
265
283
  retry JOB_ID Prove stop; retain old checkout and retry
266
- revise JOB_ID --file feedback.md New build/check/review after a stopped review
284
+ revise JOB_ID --file feedback.md [--source-ref REF]
285
+ New build/check/review; source stays pinned unless a new ref is explicit
267
286
  version Show the active CLI version
268
287
  update | update --check Update the npm CLI / inspect the latest release
269
288
  update --auto on|off Control automatic daily CLI updates
package/docs/concepts.md CHANGED
@@ -12,14 +12,16 @@ The CLI `definition` command and dashboard Definition page read that same catalo
12
12
  | Agent | Responsibility and instructions | Implement, Review, Investigate; one shared harness/model profile |
13
13
  | Skill | Reusable instructions | Six job skills; separate operator Factory Foundation |
14
14
  | Workflow | Ordered steps and gates | Software: Implement → Check → Review → Accept; Defence: Investigate |
15
- | Local issue | Bounded work request | Created from a form, brief or GitHub source; stored as a job with retained run attempts |
16
- | Automation | Trigger, filters and target | Planned; work starts manually |
15
+ | Issue | Bounded work request | Remote issue lives in its provider; a local brief needs no remote issue |
16
+ | Execution | Admitted work and its attempts | Stored in the private SQLite queue with source link and evidence |
17
+ | Provider | Repository issue integration | GitHub adapter first; unknown remotes retain local execution |
18
+ | Automation | External schedule and agent context | Owned by the selected harness; calls Factory CLI/API, no Factory cron |
17
19
  | Definition | Effective roles, workflows, skills and settings | Installed method plus private factory.json; read-only catalog |
18
20
 
19
- Inbox contains admitted local issues, not an automatically imported GitHub backlog.
20
- New issue previews a local draft; Create & start admits execution. GitHub issues
21
- and templates are source material: nothing is posted back to GitHub. Drafts stay
22
- in the open form until creation; there is no persistent unstarted backlog yet.
21
+ Inbox contains execution history, not a copied remote issue backlog. New issue
22
+ can create a repository issue without execution; Start work admits execution
23
+ separately. The provider owns issue content/state; SQLite owns queue/attempts and
24
+ creation receipts for recovery. An unfinished local form is not a saved backlog.
23
25
  An agent role is neither a machine nor a skill. Check is deterministic, and
24
26
  Accept is an operator gate. Triage/specification precede admission; evaluation
25
27
  is separately scoped work, not an automatic hidden agent phase.
@@ -51,7 +53,10 @@ Execution IDs (`build`, `verify`, `handoff`, `job_*`, `run_*`) are stable wire a
51
53
  evidence identifiers. Human labels explain them without rewriting stored jobs.
52
54
  Compatibility is handled at these boundaries; there is one active implementation.
53
55
 
54
- CLI `issue` groups list, templates, preview, draft, recommend and create. The
56
+ CLI `issue` groups list, connection, templates, preview, draft, recommend, create,
57
+ start, submissions and recover. Since 0.6, `issue create` only publishes; migrate
58
+ 0.5.1 execution callers to `issue start`. The
55
59
  legacy `run`, `issues` (GitHub list) and `recommend` commands remain compatible.
56
60
  Task/job field names and `/api/v1/jobs` are stable wire/storage identifiers for
57
- these same local issues; this adds no parallel scheduler or issue database.
61
+ these same local issues; the extra SQLite receipt table is external-write bookkeeping, not an issue mirror
62
+ or another scheduler.
@@ -0,0 +1,67 @@
1
+ # Repository and issue providers
2
+
3
+ Git source, issue tracking, execution and scheduling have different owners.
4
+ A Git remote does not guarantee an issue API, GitHub-compatible forms or a
5
+ particular CI system. Factory clones local Git source independently of the
6
+ optional issue integration.
7
+
8
+ | State | Owner |
9
+ | --- | --- |
10
+ | Source, branches, remote issues and their metadata | Selected repository/issue service |
11
+ | Queue, attempts, checks, approvals and evidence | Factory controller's private SQLite/files |
12
+ | External creation intent, correlation and receipt | SQLite; recovery bookkeeping, not an issue-backlog mirror |
13
+ | Schedules and scheduled-agent context | Selected harness, where it supports automations |
14
+ | Forms and actions | Dashboard and CLI over the same controller API |
15
+
16
+ ## Current selection
17
+
18
+ `factory/issue-provider.mjs` selects an adapter from the configured Git origin.
19
+ Only public github.com origins select the implemented GitHub adapter. Its own
20
+ module owns GitHub API paths, host authentication, issue formats and recovery.
21
+ Unknown/self-hosted remotes, GitHub Enterprise, Forgejo/Gitea, GitLab and Cursor
22
+ Origin are **not implemented adapters**. They expose no remote-issue capability;
23
+ local brief execution and the method remain usable. Factory never sends GitHub
24
+ credentials to a guessed host or assumes a mirrored repo's original host.
25
+
26
+ The adapter contract supplies provider identity and explicit capabilities,
27
+ connection/acting identity, paged list, preview, templates/draft, publish and
28
+ read-only recovery. The generic submission store binds a request key to provider,
29
+ repository, identity and content before publication. Another provider can supply
30
+ these operations without changing the queue, controller routes or creation form.
31
+ Unsupported capabilities must fail visibly, not invent a compatible endpoint.
32
+ A non-GitHub test adapter exercises this boundary; it is not a shipped integration.
33
+
34
+ A future adapter must qualify its actual authentication, template/label model,
35
+ error behavior and ambiguous-write recovery. Add it at this boundary, with
36
+ matching CLI/API/UI support. Do not build a plugin loader, generalized OAuth
37
+ service or second database merely to reserve future extension points. Selecting
38
+ an issue tracker independently of the Git host remains a deliberate future
39
+ configuration extension, not an inferred mapping.
40
+
41
+ ## GitHub adapter
42
+
43
+ The controller uses its existing `gh` identity. `issue connection` and the creation
44
+ form show the destination and acting user. GitHub enforces Issues write access;
45
+ label application needs suitable repository access. Browser GitHub login is not
46
+ required. Missing/revoked access produces a visible failure without execution.
47
+ No access token, browser cookie or host credential is passed to a job.
48
+
49
+ Creation sends the selected title/body/labels plus an opaque hidden correlation
50
+ marker. A lost response is recovered by bounded authenticated reads of issues
51
+ created by the original actor; no automatic second POST occurs. If the marker
52
+ was removed, the provider is unavailable or the bounded search cannot find it,
53
+ manual inspection is required. A receipt never claims current issue status;
54
+ read the provider again for current metadata. The published issue is the backlog.
55
+
56
+ Creation is separate from `issue start`. A remote issue never grants execution,
57
+ merge, deployment or production access. External harness automations use the same
58
+ explicit admission API and remain responsible for their own schedule/deduplication.
59
+
60
+ The managed updater’s OS timer maintains installed software; it is not a work-admission scheduler.
61
+
62
+ The dashboard derives its request key from the reviewed destination, identity,
63
+ title, body and labels. Reopening or reloading the same submission therefore
64
+ reuses its receipt, including a successful result whose browser response was
65
+ lost. No unsent draft is persisted by this mechanism. Identical browser content
66
+ returns the existing issue in that controller state; an intentional separate
67
+ copy requires a distinct CLI request key.
@@ -10,16 +10,17 @@ shell endpoint or a second scheduler.
10
10
  | Capability | CLI | Shared API | Dashboard | Remaining work |
11
11
  | --- | --- | --- | --- | --- |
12
12
  | Project/queue/attempt state | `status`, `inbox` JSON | `GET /api/v1/status` | Project, tasks, details/history | Stable versioned agent result/error contract |
13
- | Create local issue | `issue create --file --title`, `--draft` or `--github`, explicit `--workflow`, optional `--model` | `POST /api/v1/jobs` | New issue → review → Create & start | Persistent unstarted drafts and typed incident intake remain separate |
14
- | Browse/import GitHub issues | `issue list --source github [--page N]`, `issue preview --github URL` via operator `gh` | Authenticated `GET /api/v1/issues`, `POST /api/v1/issues/preview` using shared readers | Paged open-issue list, search loaded results, preview and explicit start for either type | Stronger issue/job identity, issue creation and qualified triggers remain; no implicit polling |
13
+ | Start local work | `issue start --file --title`, `--draft` or `--url`, explicit `--workflow`, optional `--model` | `POST /api/v1/jobs` | Local execution only → review → Create & start locally | Persistent unstarted drafts and typed incident intake remain separate |
14
+ | Browse/import repository issues | `issue list --source remote [--page N]`, `issue preview --url URL` via controller provider | Authenticated `GET /api/v1/issues`, `POST /api/v1/issues/preview` using shared readers | Paged open-issue list, search loaded results, preview and explicit start for either type | Issue → execution links retained; no implicit polling |
15
+ | Create repository issue / recovery | `issue connection`, `create --key`, `submissions`, `recover --key` | Authenticated connection, `POST /issues`, receipts and recovery | Display destination/actor, create without execution, recover uncertain result | GitHub adapter first; assignees/projects and other providers unimplemented |
15
16
  | Repository issue templates | `issue templates`, `issue draft --template --sha --file` | Authenticated template list and draft compilation | Chooser, fields/defaults/validation, review | Supports Markdown and YAML markdown/input/textarea/dropdown/checkboxes; unsupported templates link to GitHub |
16
- | Suggest task type | `issue recommend --file` or `--github` | Authenticated `POST /api/v1/intake/recommend`; issue preview includes suggestion | Editable recommendation after source selection | Deterministic label/brief rules; no model judgment or execution authority |
17
+ | Suggest task type | `issue recommend --file` or `--url` | Authenticated `POST /api/v1/intake/recommend`; issue preview includes suggestion | Editable recommendation after source selection | Deterministic label/brief rules; no model judgment or execution authority |
17
18
  | Cancel/retry/approve | Commands | Job action endpoints with current run ID | Task controls | JSON action results and consistent needs-attention outcomes |
18
19
  | Request changes | `revise --file` | `request_changes` action | Feedback form | JSON action result; retain shared stale-action guards |
19
20
  | Remove a stopped task | No command | `DELETE /api/v1/jobs/:id` | Remove action | Add CLI; keep existing recoverability/history semantics |
20
21
  | Evidence list/read/download | No command | Authenticated artifact routes | Files/preview/download | Add CLI with matching access and size/path rules |
21
22
  | Roles, workflows and packaged skills | `definition`, `agents`, `skills` JSON (also while stopped) | `GET /api/v1/definitions` | Agents, Skills and Definition | Shared read-only catalog; future editing must preserve common policy/gates |
22
- | Project repository links | Validated links in `status` | `project_links` from configured Git origin | View repo / optional GitHub issue link | Links only; no issue synchronization or creation API |
23
+ | Project repository links | Validated links in `status` | `project_links` from configured Git origin | View repo / optional GitHub issue link | Provider creates/issues reads are separate from Git source links |
23
24
  | Recorded token usage | Per-attempt `usage` and `token_usage` in `status` | Same status records | Analytics, task rows, metadata/history | No billing estimate; partial/unknown coverage stays explicit |
24
25
  | Analytics/filtering | Raw status available | Source queue records | Derived views | Expose equivalent queries/summaries without inventing usage data |
25
26
  | Scoped incident admission | `incident --file` validates/deduplicates private evidence | No equivalent typed intake endpoint | Generic Defence form is not equivalent admission | Common typed intake, gaps and deduplication before execution |
@@ -31,7 +32,7 @@ shell endpoint or a second scheduler.
31
32
  | SSH tunnels | `tunnel` | No tunnel endpoint | None | Client-host ownership; distinguish operator machine from worker |
32
33
  | Method export | `kit --output` | No export endpoint | None | Equivalent download/export preserving staging-only adoption |
33
34
  | Synthetic qualification | `demo`, `qualify` | No qualification endpoint | Synthetic disclosure only | Explicit separate state; never target an application accidentally |
34
- | Immutable source admission | Controlled-checkout workaround | Not implemented | Not implemented | #28, same recorded source in both interfaces |
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 |
35
36
  | Trusted PR handoff | Operator applies accepted patch | Not implemented | Not implemented | #29; credentials remain outside jobs |
36
37
 
37
38
  The current generic task form can name the Defence workflow; that is not a
@@ -46,8 +47,10 @@ authority. Headless use and GUI use must ultimately reach the same outcomes;
46
47
  intermediate releases must explicitly retain their unimplemented rows here.
47
48
 
48
49
  Infrastructure exposes detected host capacity and the local worker through
49
- `infrastructure` and the same status API. `automations` returns the supported
50
- empty list; the UI explicitly states that automatic admission is unavailable.
50
+ `infrastructure` and the same status API. `automations` returns the harness
51
+ ownership contract; the UI explains that external schedules are not discovered.
52
+ Factory has no cron module. The v1 `automations: []` field remains a compatibility
53
+ view; `automation_control` owns the current semantics.
51
54
  `foundation` prints the packaged operator skill without configuring anything;
52
55
  the Skills page reads that same file. Full safe setup controls remain #37.
53
56
  The roadmap is split into Defence #50, quality measurement #51, GitHub intake
@@ -23,12 +23,14 @@ 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. The CLI and dashboard show the requested ref and resolved SHA. Task text and reference links do not select a repository or ref.
32
+
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.
32
34
 
33
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.
34
36
 
@@ -38,14 +40,14 @@ A local model endpoint must be reachable from inside the job container. Host loo
38
40
 
39
41
  ```sh
40
42
  software-defence-factory up --state /private/state/my-app
41
- software-defence-factory run --file task.md --state /private/state/my-app
43
+ software-defence-factory run --file task.md --source-ref main --state /private/state/my-app
42
44
  ```
43
45
 
44
46
  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
47
 
46
48
  ## Review and handoff
47
49
 
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.
50
+ 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
51
 
50
52
  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
53
 
@@ -98,8 +100,7 @@ Defence form is not that typed intake path.
98
100
 
99
101
  The header names the configured project. View repo opens a validated GitHub
100
102
  origin. New issue opens Factory’s local chooser: repository templates, a blank
101
- form or existing GitHub issues. It does not publish a GitHub issue; Create &
102
- start queues local work. See [intake and CLI examples](workflows.md). The task detail provides previous/next within the filtered list, copy
103
+ form or existing GitHub issues. Create issue saves to the supported repository provider without execution. Start work queues local work. See [intake and CLI examples](workflows.md). The task detail provides previous/next within the filtered list, copy
103
104
  link and close (Escape). Closing preserves the list's filters and position.
104
105
 
105
106
  If the interface looks unexpectedly small, check the browser zoom. The design
package/docs/recovery.md CHANGED
@@ -4,9 +4,10 @@ Use `status --state PATH` and the private supervisor.log to identify the active
4
4
 
5
5
  - A normal `stop` signals the controller, waits for the executor process group, removes its labelled containers and retains the database and artifacts.
6
6
  - An unconfirmed running attempt becomes `interrupted` on controller restart. It is never silently considered successful.
7
- - `retry JOB_ID` reconciles the previous process group and containers. A live or unknown writer blocks retry. For a new build/defence attempt, the prior checkout is retained as previous-checkout-*.
7
+ - Admission resolves the configured source ref or an explicit `--source-ref` in the configured repository, records its identity/ref/SHA in private job metadata and retains its Git objects under that job. Build and retry restore from this retained revision; moving or deleting the source ref does not select a new commit.
8
+ - `retry JOB_ID` verifies retained objects and reconciles the previous process group and containers. A missing/corrupt retained source, live writer or unknown process blocks retry. For a new build/defence attempt, the prior checkout is retained as previous-checkout-*.
8
9
  - A failed verification can retry the same unchanged candidate after the check environment is repaired. A changed candidate needs a fresh verification/review sequence.
9
- - A requested revision retains previous evidence and starts a new build from the source repository with the accumulated feedback. It does not reuse earlier approval.
10
+ - A requested revision keeps the recorded source by default, retains previous evidence and starts a new build with accumulated feedback. Supplying a deliberate new `--source-ref` resolves and retains that base before the action; prior source metadata stays in history, and the new build gets fresh checks and review.
10
11
  - Removing a stopped task from the dashboard hides its queue record. Private artifacts and its deleted_at record remain on disk; this is not secure erasure.
11
12
 
12
13
  Do not remove active.json merely to unblock a job. Establish that its PID, process group and labelled containers are stopped. PID reuse or missing process identity requires operator investigation. Preserve logs and work before cleanup.
@@ -34,11 +35,14 @@ software-defence-factory revise JOB_ID --file /private/revision.md --state /priv
34
35
 
35
36
  Feedback must be nonempty and at most 4,000 characters. It is accumulated in the
36
37
  bounded job prompt. The controller checks the latest attempt and reconciles its
37
- processes before starting a new build from the configured source with that
38
+ processes before starting a new build from the retained source commit with that
38
39
  feedback. The old checkout moves to `previous-checkout-*`; the old failed review
39
40
  and reports remain intact. Verification, review and operator approval are all
40
41
  required again. A stale or duplicate request cannot approve or restart a newer
41
- attempt. **Retry** only repeats the stopped phase and is suitable for a repaired
42
+ attempt. To deliberately change the base, pass `--source-ref REF`; the controller
43
+ resolves and retains it from the same configured repository before changing the
44
+ job record. If the request cannot reconcile, its unlinked source copy is removed.
45
+ **Retry** only repeats the stopped phase and is suitable for a repaired
42
46
  execution environment; retrying review cannot change the candidate.
43
47
 
44
48
  A crashed review without a validated verdict cannot request implementation
@@ -91,6 +95,22 @@ facts in a separate private incident note with evidence paths and unknowns; leav
91
95
  the original queue/evidence unchanged. Without that evidence, model/image facts
92
96
  cannot be reconstructed reliably.
93
97
 
98
+ ### Legacy source records
99
+
100
+ Jobs admitted before source retention keep their historical candidate and review
101
+ evidence without an invented admission-time SHA. A complete existing candidate
102
+ can still pass the normal current candidate, policy, review and approval guards;
103
+ the handoff labels its source **Not recorded (legacy/unknown)**. An unpinned
104
+ queued job is blocked on controller restart, and an unpinned job cannot retry or
105
+ request implementation changes. Submit a replacement job to capture the
106
+ configured or explicit source ref before execution. Do not reconstruct proof
107
+ from the current checkout, issue text or a later ref value.
108
+
109
+ If a retained store is missing or corrupt, retry and revision stop with an
110
+ explicit source error. Restore the private state backup containing that job's
111
+ retained Git objects, or submit a replacement from a source ref that still
112
+ resolves. The controller never substitutes the mutable configured checkout.
113
+
94
114
  ### Interrupted image selection or controller startup
95
115
 
96
116
  `installation.lock` serializes image changes with controller startup, including
@@ -99,3 +119,10 @@ can run. If an operation is interrupted, preserve the lock and inspect its PID
99
119
  and action. Remove it only after confirming that process and its image build or
100
120
  startup have stopped, then run `doctor` and reconcile image metadata before
101
121
  starting again. The CLI does not automatically clear an unknown lock.
122
+
123
+ ## Unconfirmed repository issue creation
124
+
125
+ Use `issue submissions` and `issue recover --key REQUEST_ID` against the same
126
+ controller state. Recovery reads the original provider using the original
127
+ identity; it does not publish again. Do not use a new request key to retry an
128
+ uncertain write. See [provider ownership and recovery limits](integrations.md).
package/docs/setup.md CHANGED
@@ -103,7 +103,7 @@ qualification record; stop this controller unless it is the selected dashboard.
103
103
  ## 4. Prepare each application and inference profile
104
104
 
105
105
  Preserve existing commits, branches, uncommitted work and licenses before moving
106
- anything. Verify the canonical GitHub origin and main branch's tracking target;
106
+ anything. Verify the canonical Git origin and main branch's tracking target;
107
107
  a fork may still track its upstream product. Move application sources into the
108
108
  chosen workspace, not into the npm package or private runtime state.
109
109
 
@@ -115,7 +115,7 @@ Before admitting development work, establish:
115
115
  - Reproducible toolchain/dependency pins and an actual build/check command.
116
116
  - A compatible job image for native libraries, browser/mobile tools or custom
117
117
  model adapters; the standard image does not supply every application stack.
118
- - Meaningful checks in GitHub CI on the selected revision, plus the intended
118
+ - Meaningful checks in the selected CI on the selected revision, plus the intended
119
119
  review/branch policy. Record plan/access limits if enforcement is unavailable.
120
120
  - Applicable application instructions, design/scope, resource limits, secret
121
121
  references and a clear delivery destination. Keep unfinished WIP separate