@ihelpee/prumo-plugin-pipeline 0.0.0-stage → 0.18.2
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/LICENSE +51 -0
- package/README.md +72 -2
- package/dist/cli.d.ts +6 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +104 -0
- package/dist/config.d.ts +4 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +51 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +18 -0
- package/dist/messages.d.ts +3 -0
- package/dist/messages.d.ts.map +1 -0
- package/dist/messages.js +30 -0
- package/dist/preflight.d.ts +4 -0
- package/dist/preflight.d.ts.map +1 -0
- package/dist/preflight.js +116 -0
- package/dist/rules.d.ts +3 -0
- package/dist/rules.d.ts.map +1 -0
- package/dist/rules.js +16 -0
- package/dist/session.d.ts +24 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +52 -0
- package/dist/skills.d.ts +4 -0
- package/dist/skills.d.ts.map +1 -0
- package/dist/skills.js +22 -0
- package/dist/timeline.d.ts +13 -0
- package/dist/timeline.d.ts.map +1 -0
- package/dist/timeline.js +87 -0
- package/dist/types.d.ts +36 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/dist/yaml.d.ts +7 -0
- package/dist/yaml.d.ts.map +1 -0
- package/dist/yaml.js +113 -0
- package/package.json +66 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
Prumo Software License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 IHELPEE TECNOLOGIA LTDA (https://ihelpee.com). All rights reserved.
|
|
4
|
+
|
|
5
|
+
This license applies to Prumo version 0.17.0 and later: the @ihelpee/prumo
|
|
6
|
+
package, its plugins and i18n packages, and their documentation (the
|
|
7
|
+
"Software"). Versions published before 0.17.0 remain under the license each
|
|
8
|
+
one was published with (MIT or Apache-2.0); this license does not change them.
|
|
9
|
+
|
|
10
|
+
1. Grant. IHELPEE TECNOLOGIA LTDA ("IHELPEE") grants you a free,
|
|
11
|
+
non-exclusive, non-transferable, revocable license to install and run the
|
|
12
|
+
Software, as published by IHELPEE, to check your own projects. Your
|
|
13
|
+
projects may be commercial: a company may use the Software on the code of
|
|
14
|
+
the products it sells.
|
|
15
|
+
|
|
16
|
+
2. Commercial exploitation is reserved. Only IHELPEE may make money from the
|
|
17
|
+
Software itself. Unless IHELPEE agrees in writing, you may not charge for,
|
|
18
|
+
or receive any payment or other consideration for, the Software or any
|
|
19
|
+
product or service whose value comes mainly from it, such as reselling
|
|
20
|
+
it, bundling it, hosting it, or paid consulting, training, support,
|
|
21
|
+
setup or certification built on it.
|
|
22
|
+
|
|
23
|
+
3. Restrictions. Unless IHELPEE agrees in writing, you may not:
|
|
24
|
+
a. copy, modify, translate or create derivative works of the Software,
|
|
25
|
+
except copies made by installing and running it;
|
|
26
|
+
b. redistribute, sell, sublicense, rent or publish the Software, alone or
|
|
27
|
+
as part of another product;
|
|
28
|
+
c. offer the Software, or a substantial part of its functionality, to
|
|
29
|
+
third parties as a hosted or managed service;
|
|
30
|
+
d. reverse engineer, decompile or disassemble the Software, except where
|
|
31
|
+
the law allows it despite this restriction;
|
|
32
|
+
e. remove or alter any copyright, trademark or license notice.
|
|
33
|
+
|
|
34
|
+
4. Trademarks. This license grants no right to the "Prumo" name, logo or
|
|
35
|
+
other marks. See docs/TRADEMARK.md.
|
|
36
|
+
|
|
37
|
+
5. Termination. This license ends automatically if you breach it. On
|
|
38
|
+
termination you must stop using the Software and delete your copies.
|
|
39
|
+
|
|
40
|
+
6. No warranty. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
|
|
41
|
+
KIND, EXPRESS OR IMPLIED, INCLUDING MERCHANTABILITY, FITNESS FOR A
|
|
42
|
+
PARTICULAR PURPOSE AND NON-INFRINGEMENT.
|
|
43
|
+
|
|
44
|
+
7. Limitation of liability. TO THE EXTENT PERMITTED BY LAW, IHELPEE IS NOT
|
|
45
|
+
LIABLE FOR ANY DAMAGES ARISING FROM THE USE OF, OR INABILITY TO USE, THE
|
|
46
|
+
SOFTWARE.
|
|
47
|
+
|
|
48
|
+
8. Governing law. This license is governed by the laws of the Federative
|
|
49
|
+
Republic of Brazil.
|
|
50
|
+
|
|
51
|
+
Other licensing: alexgotardi@gmail.com
|
package/README.md
CHANGED
|
@@ -1,3 +1,73 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @ihelpee/prumo-plugin-pipeline
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Deterministic governance for agent work tracked in GitHub issues: a phase timeline per issue
|
|
4
|
+
and a **preflight** check that authorizes (or blocks) each mutation before it happens.
|
|
5
|
+
|
|
6
|
+
```ts
|
|
7
|
+
// prumo.config.ts
|
|
8
|
+
import pipeline from '@ihelpee/prumo-plugin-pipeline';
|
|
9
|
+
export default defineConfig({ plugins: [pipeline] });
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Commands
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
prumo pipeline session --issue owner/repo#N [--json] # current phase, checkpoints, sub-issues
|
|
16
|
+
prumo pipeline preflight --issue owner/repo#N --action <action> [--json] # exit 0 = allowed, 1 = blocked
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
A blocked preflight returns `blockedReason`, `lawfulNext` (the next legitimate step) and
|
|
20
|
+
`currentPhase`.
|
|
21
|
+
|
|
22
|
+
| Action | Typical requirement |
|
|
23
|
+
|---|---|
|
|
24
|
+
| `issue-edit-body` | an approval status needs the `confirmed` phase first |
|
|
25
|
+
| `issue-comment` | allowed for progress and checkpoints |
|
|
26
|
+
| `issue-close` | sub-issues done and a final board status |
|
|
27
|
+
| `project-move-status` | phases listed in `boardRules.requiredPhases` for the target status |
|
|
28
|
+
| `issue-add-sub-issue` | kind listed in `issueKinds` |
|
|
29
|
+
| `issue-edit-assignees` | valid session state |
|
|
30
|
+
| `decision-file-write` | path inside `decisionsGlob` |
|
|
31
|
+
|
|
32
|
+
## Timeline
|
|
33
|
+
|
|
34
|
+
Phases are recorded as machine-readable HTML comments on the issue:
|
|
35
|
+
|
|
36
|
+
```html
|
|
37
|
+
<!-- pipeline:phase:v1 {"phase":"confirmed","author":"reviewer","at":"2026-10-23T10:00:00Z"} -->
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Phases, in order: `briefing` → `draft` → `verified` → `confirmed` → `persisted`. Legacy
|
|
41
|
+
`<!-- odin-checkpoint v1 {...} -->` markers are read but never written.
|
|
42
|
+
|
|
43
|
+
## Configuration: `.prumo/pipeline.yaml`
|
|
44
|
+
|
|
45
|
+
```yaml
|
|
46
|
+
projectId: 13 # GitHub Projects v2 number
|
|
47
|
+
statusMap:
|
|
48
|
+
APPROVER: approver
|
|
49
|
+
DONE: done
|
|
50
|
+
issueKinds: [epic, task, sub-issue]
|
|
51
|
+
boardRules:
|
|
52
|
+
requiredPhases:
|
|
53
|
+
APPROVER: [briefing, draft, verified, confirmed]
|
|
54
|
+
DONE: [briefing, draft, verified, confirmed, persisted]
|
|
55
|
+
decisionsGlob: changelog/decisions/**
|
|
56
|
+
externalGates: []
|
|
57
|
+
timelineMarkers:
|
|
58
|
+
prefix: 'pipeline:'
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Agent skill
|
|
62
|
+
|
|
63
|
+
The plugin adds the `pipeline/foreman` skill (synced by `prumo harness sync`): load the session
|
|
64
|
+
at the start (`prumo pipeline session`) and run `prumo pipeline preflight` before every
|
|
65
|
+
mutation. It works alongside the repository's own skills.
|
|
66
|
+
|
|
67
|
+
## Offline mode
|
|
68
|
+
|
|
69
|
+
Set `PIPELINE_FIXTURE=<path/to/fixture.json>` to read issue data from a file instead of the
|
|
70
|
+
network (used by CI and `oracle/check-pipeline.ts`). Without a fixture, `GH_TOKEN` or
|
|
71
|
+
`GITHUB_TOKEN` is required; the command fails with an explicit message if neither is set.
|
|
72
|
+
|
|
73
|
+
GitHub access goes through `@ihelpee/prumo-plugin-github-project`.
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAmBA,wBAAgB,QAAQ,CAAC,OAAO,EAAE,GAAG,GAAG,IAAI,CA8F3C;;;;AAED,wBAEE"}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { resolveLocale } from '@ihelpee/prumo/core';
|
|
2
|
+
import { messages } from './messages.js';
|
|
3
|
+
import { loadPipelineSession } from './session.js';
|
|
4
|
+
import { runPreflight } from './preflight.js';
|
|
5
|
+
const VALID_ACTIONS = new Set([
|
|
6
|
+
'issue-edit-body',
|
|
7
|
+
'issue-comment',
|
|
8
|
+
'issue-close',
|
|
9
|
+
'project-move-status',
|
|
10
|
+
'issue-add-sub-issue',
|
|
11
|
+
'issue-edit-assignees',
|
|
12
|
+
'decision-file-write',
|
|
13
|
+
]);
|
|
14
|
+
export function register(program) {
|
|
15
|
+
// The core already gives this plugin the `pipeline` group (R-prumo-inv-004).
|
|
16
|
+
const pipeline = program.description('Pipeline session governance, timeline tracking, and preflight authorization');
|
|
17
|
+
pipeline
|
|
18
|
+
.command('session')
|
|
19
|
+
.description('Initialize and inspect pipeline session for an issue')
|
|
20
|
+
.requiredOption('--issue <owner/repo#N>', 'GitHub issue reference (e.g. cash-tools/nest#477)')
|
|
21
|
+
.option('--json', 'JSON output')
|
|
22
|
+
.option('--lang <locale>', 'language / idioma (en | pt-BR)')
|
|
23
|
+
.action(async (opts, cmd) => {
|
|
24
|
+
// The core's root program may consume --lang, so read it from the global options (R-prumo-inv-004).
|
|
25
|
+
const locale = resolveLocale({ cliFlag: opts.lang ?? cmd.optsWithGlobals().lang });
|
|
26
|
+
try {
|
|
27
|
+
const session = await loadPipelineSession(opts.issue, process.cwd(), locale);
|
|
28
|
+
if (opts.json) {
|
|
29
|
+
console.log(JSON.stringify(session, null, 2));
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
console.log(`\nprumo pipeline session · ${session.issueKey}`);
|
|
33
|
+
console.log(` title: ${session.title}`);
|
|
34
|
+
console.log(` state: ${session.state}`);
|
|
35
|
+
console.log(` boardStatus: ${session.boardStatus ?? '(none)'}`);
|
|
36
|
+
console.log(` currentPhase: ${session.currentPhase}`);
|
|
37
|
+
console.log(` timeline: ${session.timeline.length} checkpoints recorded`);
|
|
38
|
+
for (const t of session.timeline) {
|
|
39
|
+
const auth = t.author ? ` by @${t.author}` : '';
|
|
40
|
+
console.log(` · [${t.phase}]${auth} (${t.source})`);
|
|
41
|
+
}
|
|
42
|
+
console.log('');
|
|
43
|
+
}
|
|
44
|
+
catch (err) {
|
|
45
|
+
console.error(messages.t(locale, 'sessionLoadError', { error: err.message }));
|
|
46
|
+
process.exitCode = 1;
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
pipeline
|
|
50
|
+
.command('preflight')
|
|
51
|
+
.description('Verify whether a mutation action is authorized by pipeline rules')
|
|
52
|
+
.requiredOption('--issue <owner/repo#N>', 'GitHub issue reference (e.g. cash-tools/nest#477)')
|
|
53
|
+
.requiredOption('--action <action>', 'Mutation action to verify')
|
|
54
|
+
.option('--json', 'JSON output')
|
|
55
|
+
.option('--lang <locale>', 'language / idioma (en | pt-BR)')
|
|
56
|
+
.action(async (opts, cmd) => {
|
|
57
|
+
const locale = resolveLocale({ cliFlag: opts.lang ?? cmd.optsWithGlobals().lang });
|
|
58
|
+
if (!VALID_ACTIONS.has(opts.action)) {
|
|
59
|
+
console.error(messages.t(locale, 'invalidAction', { action: opts.action, valid: [...VALID_ACTIONS].join(', ') }));
|
|
60
|
+
process.exitCode = 2;
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
try {
|
|
64
|
+
const result = await runPreflight(opts.issue, opts.action, process.cwd(), locale);
|
|
65
|
+
if (opts.json) {
|
|
66
|
+
console.log(JSON.stringify(result, null, 2));
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
console.log(`\nprumo pipeline preflight · ${result.action} @ ${result.issue}`);
|
|
70
|
+
console.log(` authorized: ${result.authorized ? 'YES' : 'BLOCKED'}`);
|
|
71
|
+
console.log(` currentPhase: ${result.currentPhase}`);
|
|
72
|
+
console.log(` boardStatus: ${result.boardStatus ?? '(none)'}`);
|
|
73
|
+
if (!result.authorized) {
|
|
74
|
+
console.log(` blocked: ${result.blockedReason}`);
|
|
75
|
+
}
|
|
76
|
+
if (result.lawfulNext && result.lawfulNext.length > 0) {
|
|
77
|
+
console.log(` lawfulNext: ${result.lawfulNext.join(', ')}`);
|
|
78
|
+
}
|
|
79
|
+
console.log('');
|
|
80
|
+
}
|
|
81
|
+
if (!result.authorized) {
|
|
82
|
+
process.exitCode = 1;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
catch (err) {
|
|
86
|
+
if (opts.json) {
|
|
87
|
+
console.log(JSON.stringify({
|
|
88
|
+
authorized: false,
|
|
89
|
+
action: opts.action,
|
|
90
|
+
issue: opts.issue,
|
|
91
|
+
currentPhase: 'none',
|
|
92
|
+
blockedReason: err.message,
|
|
93
|
+
}, null, 2));
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
console.error(messages.t(locale, 'preflightError', { error: err.message }));
|
|
97
|
+
}
|
|
98
|
+
process.exitCode = 1;
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
export default {
|
|
103
|
+
register,
|
|
104
|
+
};
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAEjD,eAAO,MAAM,uBAAuB,EAAE,cAqBrC,CAAC;AAEF,wBAAsB,kBAAkB,CAAC,OAAO,GAAE,MAAsB,GAAG,OAAO,CAAC,cAAc,CAAC,CA0BjG"}
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { join, resolve } from 'node:path';
|
|
3
|
+
import { parseYaml } from './yaml.js';
|
|
4
|
+
export const DEFAULT_PIPELINE_CONFIG = {
|
|
5
|
+
projectId: 13,
|
|
6
|
+
statusMap: {
|
|
7
|
+
APROVADOR: 'aprovador',
|
|
8
|
+
EM_DESENVOLVIMENTO: 'em-desenvolvimento',
|
|
9
|
+
FINALIZADO: 'finalizado',
|
|
10
|
+
},
|
|
11
|
+
issueKinds: ['epic', 'task', 'fatia', 'sub-issue'],
|
|
12
|
+
boardRules: {
|
|
13
|
+
requiredPhases: {
|
|
14
|
+
APROVADOR: ['briefing', 'draft', 'verified', 'confirmed'],
|
|
15
|
+
},
|
|
16
|
+
requiredPhasesForSection: {
|
|
17
|
+
'## Aprovação': ['briefing', 'draft', 'verified', 'confirmed'],
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
decisionsGlob: 'changelog/decisoes/**',
|
|
21
|
+
externalGates: [],
|
|
22
|
+
timelineMarkers: {
|
|
23
|
+
prefix: 'pipeline:',
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
export async function loadPipelineConfig(rootDir = process.cwd()) {
|
|
27
|
+
const root = resolve(rootDir);
|
|
28
|
+
const candidates = [
|
|
29
|
+
join(root, '.prumo', 'pipeline.yaml'),
|
|
30
|
+
join(root, '.prumo', 'pipeline.yml'),
|
|
31
|
+
join(root, '.prumo', 'pipeline.json'),
|
|
32
|
+
];
|
|
33
|
+
for (const candidate of candidates) {
|
|
34
|
+
try {
|
|
35
|
+
const content = await readFile(candidate, 'utf8');
|
|
36
|
+
const parsed = parseYaml(content);
|
|
37
|
+
return {
|
|
38
|
+
...DEFAULT_PIPELINE_CONFIG,
|
|
39
|
+
...parsed,
|
|
40
|
+
boardRules: {
|
|
41
|
+
...DEFAULT_PIPELINE_CONFIG.boardRules,
|
|
42
|
+
...(parsed.boardRules ?? {}),
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
// move on to the next one
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return DEFAULT_PIPELINE_CONFIG;
|
|
51
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export * from './types.js';
|
|
2
|
+
export * from './yaml.js';
|
|
3
|
+
export * from './timeline.js';
|
|
4
|
+
export * from './config.js';
|
|
5
|
+
export * from './session.js';
|
|
6
|
+
export * from './preflight.js';
|
|
7
|
+
export * from './skills.js';
|
|
8
|
+
export * from './rules.js';
|
|
9
|
+
export declare function activate(): void;
|
|
10
|
+
declare const _default: {
|
|
11
|
+
activate: typeof activate;
|
|
12
|
+
};
|
|
13
|
+
export default _default;
|
|
14
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA,cAAc,YAAY,CAAC;AAC3B,cAAc,WAAW,CAAC;AAC1B,cAAc,eAAe,CAAC;AAC9B,cAAc,aAAa,CAAC;AAC5B,cAAc,cAAc,CAAC;AAC7B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,aAAa,CAAC;AAC5B,cAAc,YAAY,CAAC;AAE3B,wBAAgB,QAAQ,IAAI,IAAI,CAG/B;;;;AAED,wBAEE"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { registerHarnessSkills, registerCliContribution } from '@ihelpee/prumo';
|
|
2
|
+
import { PIPELINE_SKILLS } from './skills.js';
|
|
3
|
+
import { register as registerCli } from './cli.js';
|
|
4
|
+
export * from './types.js';
|
|
5
|
+
export * from './yaml.js';
|
|
6
|
+
export * from './timeline.js';
|
|
7
|
+
export * from './config.js';
|
|
8
|
+
export * from './session.js';
|
|
9
|
+
export * from './preflight.js';
|
|
10
|
+
export * from './skills.js';
|
|
11
|
+
export * from './rules.js';
|
|
12
|
+
export function activate() {
|
|
13
|
+
registerHarnessSkills('pipeline', PIPELINE_SKILLS, '');
|
|
14
|
+
registerCliContribution({ pluginId: 'pipeline', register: registerCli });
|
|
15
|
+
}
|
|
16
|
+
export default {
|
|
17
|
+
activate,
|
|
18
|
+
};
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
/** CLI and preflight messages. English is canonical; other locales fall back to it per key. */
|
|
2
|
+
export declare const messages: import("@ihelpee/prumo/core").Translator<"pipeline-board-rules" | "sessionLoadError" | "invalidAction" | "preflightError" | "externalGateFailed" | "fixExternalGate" | "approvalNeedsConfirmed" | "statusTransitionBlocked" | "closeBlocked" | "fixtureInvalid" | "fixtureReadFailed">;
|
|
3
|
+
//# sourceMappingURL=messages.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"messages.d.ts","sourceRoot":"","sources":["../src/messages.ts"],"names":[],"mappings":"AAEA,+FAA+F;AAC/F,eAAO,MAAM,QAAQ,wRA6BnB,CAAC"}
|
package/dist/messages.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { defineMessages } from '@ihelpee/prumo/core';
|
|
2
|
+
/** CLI and preflight messages. English is canonical; other locales fall back to it per key. */
|
|
3
|
+
export const messages = defineMessages({
|
|
4
|
+
en: {
|
|
5
|
+
'pipeline-board-rules': 'pipeline board and phase compliance check',
|
|
6
|
+
sessionLoadError: 'Failed to load the pipeline session: {error}',
|
|
7
|
+
invalidAction: "Invalid preflight action: '{action}'. Valid actions: {valid}",
|
|
8
|
+
preflightError: 'Pipeline preflight failed: {error}',
|
|
9
|
+
externalGateFailed: 'External gate failed: {cmd} ({error})',
|
|
10
|
+
fixExternalGate: 'fix the external gate failure',
|
|
11
|
+
approvalNeedsConfirmed: "The APROVADOR status requires the 'confirmed' phase in the timeline before persisting changes to the body (## Aprovação section). Missing phases: {missing}",
|
|
12
|
+
statusTransitionBlocked: "Status transition blocked: requires prior confirmation in the timeline ('confirmed').",
|
|
13
|
+
closeBlocked: "Issue close blocked: requires confirmation ('confirmed' or 'persisted') in the timeline.",
|
|
14
|
+
fixtureInvalid: "Fixture at {path} does not contain a valid 'issue' object",
|
|
15
|
+
fixtureReadFailed: "Failed to read PIPELINE_FIXTURE '{path}': {error}",
|
|
16
|
+
},
|
|
17
|
+
'pt-BR': {
|
|
18
|
+
'pipeline-board-rules': 'Verificação de conformidade de board e fases do pipeline',
|
|
19
|
+
sessionLoadError: 'Erro ao carregar sessão do pipeline: {error}',
|
|
20
|
+
invalidAction: "Ação de preflight inválida: '{action}'. Ações válidas: {valid}",
|
|
21
|
+
preflightError: 'Erro durante preflight do pipeline: {error}',
|
|
22
|
+
externalGateFailed: 'Gate externo falhou: {cmd} ({error})',
|
|
23
|
+
fixExternalGate: 'corrigir falha do gate externo',
|
|
24
|
+
approvalNeedsConfirmed: "Status APROVADOR exige a fase 'confirmed' na timeline antes de persistir alterações no corpo (seção ## Aprovação). Fases ausentes: {missing}",
|
|
25
|
+
statusTransitionBlocked: "Transição de status bloqueada: exige confirmação prévia na timeline ('confirmed').",
|
|
26
|
+
closeBlocked: "Fechamento de issue bloqueado: exige confirmação ('confirmed' ou 'persisted') na timeline.",
|
|
27
|
+
fixtureInvalid: "Fixture em {path} não contém objeto 'issue' válido",
|
|
28
|
+
fixtureReadFailed: "Falha ao ler PIPELINE_FIXTURE '{path}': {error}",
|
|
29
|
+
},
|
|
30
|
+
});
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { SupportedLocale } from '@ihelpee/prumo/core';
|
|
2
|
+
import type { PreflightAction, PreflightResult } from './types.js';
|
|
3
|
+
export declare function runPreflight(issueStr: string, action: PreflightAction, rootDir?: string, locale?: SupportedLocale): Promise<PreflightResult>;
|
|
4
|
+
//# sourceMappingURL=preflight.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"preflight.d.ts","sourceRoot":"","sources":["../src/preflight.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAE3D,OAAO,KAAK,EAAE,eAAe,EAAE,eAAe,EAAiB,MAAM,YAAY,CAAC;AAIlF,wBAAsB,YAAY,CAChC,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,eAAe,EACvB,OAAO,GAAE,MAAsB,EAC/B,MAAM,CAAC,EAAE,eAAe,GACvB,OAAO,CAAC,eAAe,CAAC,CAsH1B"}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { exec } from 'node:child_process';
|
|
2
|
+
import { promisify } from 'node:util';
|
|
3
|
+
import { loadPipelineSession } from './session.js';
|
|
4
|
+
import { messages } from './messages.js';
|
|
5
|
+
const sh = promisify(exec);
|
|
6
|
+
export async function runPreflight(issueStr, action, rootDir = process.cwd(), locale) {
|
|
7
|
+
const session = await loadPipelineSession(issueStr, rootDir, locale);
|
|
8
|
+
const { currentPhase, boardStatus, timeline, config, body } = session;
|
|
9
|
+
const recordedPhases = new Set(timeline.map((t) => t.phase));
|
|
10
|
+
const hasPhase = (p) => recordedPhases.has(p);
|
|
11
|
+
// 1. External gates (se configurados em pipeline.yaml)
|
|
12
|
+
if (config.externalGates && config.externalGates.length > 0) {
|
|
13
|
+
for (const gateCmd of config.externalGates) {
|
|
14
|
+
try {
|
|
15
|
+
await sh(gateCmd, { cwd: rootDir });
|
|
16
|
+
}
|
|
17
|
+
catch (err) {
|
|
18
|
+
return {
|
|
19
|
+
authorized: false,
|
|
20
|
+
action,
|
|
21
|
+
issue: session.issueKey,
|
|
22
|
+
currentPhase,
|
|
23
|
+
boardStatus,
|
|
24
|
+
blockedReason: messages.t(locale, 'externalGateFailed', { cmd: gateCmd, error: err.message }),
|
|
25
|
+
lawfulNext: [messages.t(locale, 'fixExternalGate')],
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
// 2. Action: issue-edit-body
|
|
31
|
+
if (action === 'issue-edit-body') {
|
|
32
|
+
const isAprovadorStatus = boardStatus === 'APROVADOR' || boardStatus === config.statusMap?.['APROVADOR'];
|
|
33
|
+
const touchesAprovacaoSection = body.includes('## Aprovação');
|
|
34
|
+
if (isAprovadorStatus || touchesAprovacaoSection) {
|
|
35
|
+
const requiredPhases = config.boardRules?.requiredPhases?.['APROVADOR'] ?? [
|
|
36
|
+
'briefing',
|
|
37
|
+
'draft',
|
|
38
|
+
'verified',
|
|
39
|
+
'confirmed',
|
|
40
|
+
];
|
|
41
|
+
const missingPhases = requiredPhases.filter((p) => !hasPhase(p));
|
|
42
|
+
if (missingPhases.includes('confirmed') || missingPhases.length > 0) {
|
|
43
|
+
return {
|
|
44
|
+
authorized: false,
|
|
45
|
+
action,
|
|
46
|
+
issue: session.issueKey,
|
|
47
|
+
currentPhase,
|
|
48
|
+
boardStatus,
|
|
49
|
+
blockedReason: messages.t(locale, 'approvalNeedsConfirmed', { missing: missingPhases.join(', ') }),
|
|
50
|
+
lawfulNext: ['pipeline:confirm', 'prumo pipeline session'],
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
authorized: true,
|
|
56
|
+
action,
|
|
57
|
+
issue: session.issueKey,
|
|
58
|
+
currentPhase,
|
|
59
|
+
boardStatus,
|
|
60
|
+
lawfulNext: ['pipeline:persist', 'prumo pipeline preflight --action issue-close'],
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
// 3. Action: project-move-status
|
|
64
|
+
if (action === 'project-move-status') {
|
|
65
|
+
if (boardStatus === 'APROVADOR' && !hasPhase('confirmed')) {
|
|
66
|
+
return {
|
|
67
|
+
authorized: false,
|
|
68
|
+
action,
|
|
69
|
+
issue: session.issueKey,
|
|
70
|
+
currentPhase,
|
|
71
|
+
boardStatus,
|
|
72
|
+
blockedReason: messages.t(locale, 'statusTransitionBlocked'),
|
|
73
|
+
lawfulNext: ['pipeline:confirm'],
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
authorized: true,
|
|
78
|
+
action,
|
|
79
|
+
issue: session.issueKey,
|
|
80
|
+
currentPhase,
|
|
81
|
+
boardStatus,
|
|
82
|
+
lawfulNext: ['prumo pipeline session'],
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
// 4. Action: issue-close
|
|
86
|
+
if (action === 'issue-close') {
|
|
87
|
+
if (!hasPhase('confirmed') && !hasPhase('persisted')) {
|
|
88
|
+
return {
|
|
89
|
+
authorized: false,
|
|
90
|
+
action,
|
|
91
|
+
issue: session.issueKey,
|
|
92
|
+
currentPhase,
|
|
93
|
+
boardStatus,
|
|
94
|
+
blockedReason: messages.t(locale, 'closeBlocked'),
|
|
95
|
+
lawfulNext: ['pipeline:confirm'],
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
authorized: true,
|
|
100
|
+
action,
|
|
101
|
+
issue: session.issueKey,
|
|
102
|
+
currentPhase,
|
|
103
|
+
boardStatus,
|
|
104
|
+
lawfulNext: ['gh issue close'],
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
// Other default actions (issue-comment, issue-add-sub-issue, issue-edit-assignees, decision-file-write)
|
|
108
|
+
return {
|
|
109
|
+
authorized: true,
|
|
110
|
+
action,
|
|
111
|
+
issue: session.issueKey,
|
|
112
|
+
currentPhase,
|
|
113
|
+
boardStatus,
|
|
114
|
+
lawfulNext: ['prumo pipeline session'],
|
|
115
|
+
};
|
|
116
|
+
}
|
package/dist/rules.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rules.d.ts","sourceRoot":"","sources":["../src/rules.ts"],"names":[],"mappings":"AAAA,OAAO,EAAe,KAAK,OAAO,EAAoB,MAAM,gBAAgB,CAAC;AAI7E,wBAAgB,mBAAmB,CAAC,OAAO,GAAE,MAAsB,GAAG,OAAO,EAAE,CAa9E"}
|
package/dist/rules.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { RuleBuilder } from '@ihelpee/prumo';
|
|
2
|
+
import { loadPipelineConfig } from './config.js';
|
|
3
|
+
import { messages } from './messages.js';
|
|
4
|
+
export function createPipelineRules(rootDir = process.cwd()) {
|
|
5
|
+
const rule = new RuleBuilder('pipeline-board-rules')
|
|
6
|
+
.severity('error')
|
|
7
|
+
.message(messages.text('pipeline-board-rules'))
|
|
8
|
+
.needsData('declared')
|
|
9
|
+
.assert(async (ctx) => {
|
|
10
|
+
const config = await loadPipelineConfig(ctx.root);
|
|
11
|
+
void config;
|
|
12
|
+
return [];
|
|
13
|
+
})
|
|
14
|
+
.build();
|
|
15
|
+
return [rule];
|
|
16
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type GitHubIssueRef, type RawIssueDetails } from '@ihelpee/prumo-plugin-github-project';
|
|
2
|
+
import type { SupportedLocale } from '@ihelpee/prumo/core';
|
|
3
|
+
import type { PipelineConfig, PipelinePhase, PipelineTimelineEntry } from './types.js';
|
|
4
|
+
export interface PipelineSessionData {
|
|
5
|
+
issueRef: GitHubIssueRef;
|
|
6
|
+
issueKey: string;
|
|
7
|
+
title: string;
|
|
8
|
+
body: string;
|
|
9
|
+
state: string;
|
|
10
|
+
boardStatus: string | null;
|
|
11
|
+
assignees: string[];
|
|
12
|
+
timeline: PipelineTimelineEntry[];
|
|
13
|
+
currentPhase: PipelinePhase | 'none';
|
|
14
|
+
subIssues: Array<{
|
|
15
|
+
id: string;
|
|
16
|
+
number: number;
|
|
17
|
+
title: string;
|
|
18
|
+
state: string;
|
|
19
|
+
}>;
|
|
20
|
+
config: PipelineConfig;
|
|
21
|
+
}
|
|
22
|
+
export declare function fetchIssueDetailsWithFixture(issueStr: string, config: PipelineConfig, locale?: SupportedLocale): Promise<RawIssueDetails>;
|
|
23
|
+
export declare function loadPipelineSession(issueStr: string, rootDir?: string, locale?: SupportedLocale): Promise<PipelineSessionData>;
|
|
24
|
+
//# sourceMappingURL=session.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../src/session.ts"],"names":[],"mappings":"AAEA,OAAO,EAGL,KAAK,cAAc,EACnB,KAAK,eAAe,EACrB,MAAM,sCAAsC,CAAC;AAC9C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAI3D,OAAO,KAAK,EAAE,cAAc,EAAE,aAAa,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;AAEvF,MAAM,WAAW,mBAAmB;IAClC,QAAQ,EAAE,cAAc,CAAC;IACzB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,QAAQ,EAAE,qBAAqB,EAAE,CAAC;IAClC,YAAY,EAAE,aAAa,GAAG,MAAM,CAAC;IACrC,SAAS,EAAE,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC/E,MAAM,EAAE,cAAc,CAAC;CACxB;AAED,wBAAsB,4BAA4B,CAChD,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,cAAc,EACtB,MAAM,CAAC,EAAE,eAAe,GACvB,OAAO,CAAC,eAAe,CAAC,CAoB1B;AAED,wBAAsB,mBAAmB,CACvC,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE,MAAsB,EAC/B,MAAM,CAAC,EAAE,eAAe,GACvB,OAAO,CAAC,mBAAmB,CAAC,CA0B9B"}
|
package/dist/session.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
import { GitHubProjectClient, parseIssueRef, } from '@ihelpee/prumo-plugin-github-project';
|
|
4
|
+
import { loadPipelineConfig } from './config.js';
|
|
5
|
+
import { messages } from './messages.js';
|
|
6
|
+
import { parseTimelineFromTexts } from './timeline.js';
|
|
7
|
+
export async function fetchIssueDetailsWithFixture(issueStr, config, locale) {
|
|
8
|
+
const fixturePath = process.env.PIPELINE_FIXTURE;
|
|
9
|
+
if (fixturePath) {
|
|
10
|
+
try {
|
|
11
|
+
const absPath = resolve(fixturePath);
|
|
12
|
+
const raw = await readFile(absPath, 'utf8');
|
|
13
|
+
const parsed = JSON.parse(raw);
|
|
14
|
+
const details = parsed.issue ?? parsed;
|
|
15
|
+
if (!details || typeof details.number !== 'number') {
|
|
16
|
+
throw new Error(messages.t(locale, 'fixtureInvalid', { path: fixturePath }));
|
|
17
|
+
}
|
|
18
|
+
return details;
|
|
19
|
+
}
|
|
20
|
+
catch (err) {
|
|
21
|
+
throw new Error(messages.t(locale, 'fixtureReadFailed', { path: fixturePath, error: err.message }));
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
const client = new GitHubProjectClient({ locale });
|
|
25
|
+
const ref = parseIssueRef(issueStr, locale);
|
|
26
|
+
return client.getIssueDetails(ref, config.projectId);
|
|
27
|
+
}
|
|
28
|
+
export async function loadPipelineSession(issueStr, rootDir = process.cwd(), locale) {
|
|
29
|
+
const config = await loadPipelineConfig(rootDir);
|
|
30
|
+
const details = await fetchIssueDetailsWithFixture(issueStr, config, locale);
|
|
31
|
+
const ref = {
|
|
32
|
+
owner: details.owner,
|
|
33
|
+
repo: details.repo,
|
|
34
|
+
number: details.number,
|
|
35
|
+
};
|
|
36
|
+
const textsToScan = [details.body, ...details.comments.map((c) => c.body)];
|
|
37
|
+
const timeline = parseTimelineFromTexts(textsToScan);
|
|
38
|
+
const currentPhase = timeline.length > 0 ? timeline[timeline.length - 1].phase : 'none';
|
|
39
|
+
return {
|
|
40
|
+
issueRef: ref,
|
|
41
|
+
issueKey: `${ref.owner}/${ref.repo}#${ref.number}`,
|
|
42
|
+
title: details.title,
|
|
43
|
+
body: details.body,
|
|
44
|
+
state: details.state,
|
|
45
|
+
boardStatus: details.projectStatus ?? null,
|
|
46
|
+
assignees: details.assignees,
|
|
47
|
+
timeline,
|
|
48
|
+
currentPhase,
|
|
49
|
+
subIssues: details.subIssues,
|
|
50
|
+
config,
|
|
51
|
+
};
|
|
52
|
+
}
|
package/dist/skills.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"skills.d.ts","sourceRoot":"","sources":["../src/skills.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAC;AAE5C,eAAO,MAAM,sBAAsB,EAAE,KAoBpC,CAAC;AAEF,eAAO,MAAM,eAAe,EAAE,KAAK,EAElC,CAAC"}
|
package/dist/skills.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export const PIPELINE_FOREMAN_SKILL = {
|
|
2
|
+
name: 'pipeline/foreman',
|
|
3
|
+
purpose: 'Govern GitHub pipeline session lifecycle and enforce preflight authorization checks before mutations.',
|
|
4
|
+
allowedReads: ['.prumo/**', 'docs/**', 'public/docs/**', 'AGENTS.md'],
|
|
5
|
+
forbiddenReads: [],
|
|
6
|
+
emits: ['timeline markers', 'session status'],
|
|
7
|
+
rationale: 'Ensures mutations to GitHub issues, bodies, and project boards pass through deterministic preflight gates, ' +
|
|
8
|
+
'preventing unauthorized edits and out-of-order phase transitions.',
|
|
9
|
+
gate: 'prumo pipeline session',
|
|
10
|
+
titleEn: 'Pipeline Foreman (Session & Preflight Conductor)',
|
|
11
|
+
descriptionEn: 'Foreman for GitHub issue and Project boards. Conducts session initialization and enforces preflight ' +
|
|
12
|
+
'clearance before any mutating action.',
|
|
13
|
+
workflowEn: [
|
|
14
|
+
'At session start, run `prumo pipeline session --issue <owner/repo#N>`. Read current phase, board status, and lawful next actions.',
|
|
15
|
+
'Before editing body (especially ## Aprovação), run `prumo pipeline preflight --issue <owner/repo#N> --action issue-edit-body`. If blocked, halt and do not persist.',
|
|
16
|
+
'Emit timeline markers in comments using `<!-- pipeline:phase:v1 ... -->` advancing through briefing → draft → verified → confirmed → persisted.',
|
|
17
|
+
'Before changing project status, closing issue, or adding sub-issues, run corresponding preflight action to ensure lawful progression.',
|
|
18
|
+
],
|
|
19
|
+
};
|
|
20
|
+
export const PIPELINE_SKILLS = [
|
|
21
|
+
PIPELINE_FOREMAN_SKILL,
|
|
22
|
+
];
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { PipelinePhase, PipelineTimelineEntry } from './types.js';
|
|
2
|
+
export declare function isPipelinePhase(val: unknown): val is PipelinePhase;
|
|
3
|
+
/**
|
|
4
|
+
* Emits the canonical v1 timeline marker.
|
|
5
|
+
* Never emits the legacy odin-checkpoint format.
|
|
6
|
+
*/
|
|
7
|
+
export declare function emitPipelinePhaseMarker(phase: PipelinePhase, extra?: Record<string, unknown>): string;
|
|
8
|
+
/**
|
|
9
|
+
* Extracts timeline entries from text bodies or comments.
|
|
10
|
+
* Fully reads the legacy <!-- odin-checkpoint v1 --> format.
|
|
11
|
+
*/
|
|
12
|
+
export declare function parseTimelineFromTexts(texts: string[]): PipelineTimelineEntry[];
|
|
13
|
+
//# sourceMappingURL=timeline.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"timeline.d.ts","sourceRoot":"","sources":["../src/timeline.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;AAIvE,wBAAgB,eAAe,CAAC,GAAG,EAAE,OAAO,GAAG,GAAG,IAAI,aAAa,CAElE;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,CACrC,KAAK,EAAE,aAAa,EACpB,KAAK,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,GAClC,MAAM,CAOR;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,qBAAqB,EAAE,CAmE/E"}
|
package/dist/timeline.js
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
const VALID_PHASES = new Set(['briefing', 'draft', 'verified', 'confirmed', 'persisted']);
|
|
2
|
+
export function isPipelinePhase(val) {
|
|
3
|
+
return typeof val === 'string' && VALID_PHASES.has(val);
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Emits the canonical v1 timeline marker.
|
|
7
|
+
* Never emits the legacy odin-checkpoint format.
|
|
8
|
+
*/
|
|
9
|
+
export function emitPipelinePhaseMarker(phase, extra = {}) {
|
|
10
|
+
const payload = {
|
|
11
|
+
phase,
|
|
12
|
+
timestamp: new Date().toISOString(),
|
|
13
|
+
...extra,
|
|
14
|
+
};
|
|
15
|
+
return `<!-- pipeline:phase:v1 ${JSON.stringify(payload)} -->`;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Extracts timeline entries from text bodies or comments.
|
|
19
|
+
* Fully reads the legacy <!-- odin-checkpoint v1 --> format.
|
|
20
|
+
*/
|
|
21
|
+
export function parseTimelineFromTexts(texts) {
|
|
22
|
+
const entries = [];
|
|
23
|
+
for (const text of texts) {
|
|
24
|
+
if (!text)
|
|
25
|
+
continue;
|
|
26
|
+
// 1. Canonical: <!-- pipeline:phase:v1 ... -->
|
|
27
|
+
const pipelineRegex = /<!--\s*pipeline:phase:v1\s*(\{.*?\})\s*-->/gs;
|
|
28
|
+
let match;
|
|
29
|
+
while ((match = pipelineRegex.exec(text)) !== null) {
|
|
30
|
+
try {
|
|
31
|
+
const json = JSON.parse(match[1]);
|
|
32
|
+
if (isPipelinePhase(json.phase)) {
|
|
33
|
+
entries.push({
|
|
34
|
+
phase: json.phase,
|
|
35
|
+
author: typeof json.author === 'string' ? json.author : undefined,
|
|
36
|
+
timestamp: typeof json.timestamp === 'string' ? json.timestamp : undefined,
|
|
37
|
+
source: 'pipeline',
|
|
38
|
+
rawJson: json,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
// ignora JSON corrompido
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
// 1b. Canonical block form: <!-- pipeline:phase:v1 -->\n{...}
|
|
47
|
+
const blockPipelineRegex = /<!--\s*pipeline:phase:v1\s*-->\s*(\{.*?\})/gs;
|
|
48
|
+
while ((match = blockPipelineRegex.exec(text)) !== null) {
|
|
49
|
+
try {
|
|
50
|
+
const json = JSON.parse(match[1]);
|
|
51
|
+
if (isPipelinePhase(json.phase)) {
|
|
52
|
+
entries.push({
|
|
53
|
+
phase: json.phase,
|
|
54
|
+
author: typeof json.author === 'string' ? json.author : undefined,
|
|
55
|
+
timestamp: typeof json.timestamp === 'string' ? json.timestamp : undefined,
|
|
56
|
+
source: 'pipeline',
|
|
57
|
+
rawJson: json,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
// ignora
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// 2. Compat legado: <!-- odin-checkpoint v1 ... -->
|
|
66
|
+
const odinRegex = /<!--\s*odin-checkpoint\s+v1(?::\s*|\s+)(\{.*?\})\s*-->/gs;
|
|
67
|
+
while ((match = odinRegex.exec(text)) !== null) {
|
|
68
|
+
try {
|
|
69
|
+
const json = JSON.parse(match[1]);
|
|
70
|
+
const phaseCandidate = json.phase ?? json.fase ?? json.checkpoint;
|
|
71
|
+
if (isPipelinePhase(phaseCandidate)) {
|
|
72
|
+
entries.push({
|
|
73
|
+
phase: phaseCandidate,
|
|
74
|
+
author: typeof json.author === 'string' ? json.author : undefined,
|
|
75
|
+
timestamp: typeof json.timestamp === 'string' ? json.timestamp : undefined,
|
|
76
|
+
source: 'odin-compat',
|
|
77
|
+
rawJson: json,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
// ignora
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return entries;
|
|
87
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export type PreflightAction = 'issue-edit-body' | 'issue-comment' | 'issue-close' | 'project-move-status' | 'issue-add-sub-issue' | 'issue-edit-assignees' | 'decision-file-write';
|
|
2
|
+
export type PipelinePhase = 'briefing' | 'draft' | 'verified' | 'confirmed' | 'persisted';
|
|
3
|
+
export interface PipelineTimelineEntry {
|
|
4
|
+
phase: PipelinePhase;
|
|
5
|
+
author?: string;
|
|
6
|
+
timestamp?: string;
|
|
7
|
+
source: 'pipeline' | 'odin-compat';
|
|
8
|
+
rawJson?: Record<string, unknown>;
|
|
9
|
+
}
|
|
10
|
+
export interface BoardRules {
|
|
11
|
+
requiredPhases?: Record<string, PipelinePhase[]>;
|
|
12
|
+
allowedTransitions?: Record<string, string[]>;
|
|
13
|
+
requiredPhasesForSection?: Record<string, PipelinePhase[]>;
|
|
14
|
+
[key: string]: unknown;
|
|
15
|
+
}
|
|
16
|
+
export interface PipelineConfig {
|
|
17
|
+
projectId?: string | number;
|
|
18
|
+
statusMap?: Record<string, string>;
|
|
19
|
+
issueKinds?: string[];
|
|
20
|
+
boardRules?: BoardRules;
|
|
21
|
+
decisionsGlob?: string;
|
|
22
|
+
externalGates?: string[];
|
|
23
|
+
timelineMarkers?: {
|
|
24
|
+
prefix?: string;
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
export interface PreflightResult {
|
|
28
|
+
authorized: boolean;
|
|
29
|
+
action: PreflightAction;
|
|
30
|
+
issue: string;
|
|
31
|
+
currentPhase: PipelinePhase | 'none';
|
|
32
|
+
boardStatus?: string | null;
|
|
33
|
+
blockedReason?: string;
|
|
34
|
+
lawfulNext?: string[];
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,eAAe,GACvB,iBAAiB,GACjB,eAAe,GACf,aAAa,GACb,qBAAqB,GACrB,qBAAqB,GACrB,sBAAsB,GACtB,qBAAqB,CAAC;AAE1B,MAAM,MAAM,aAAa,GACrB,UAAU,GACV,OAAO,GACP,UAAU,GACV,WAAW,GACX,WAAW,CAAC;AAEhB,MAAM,WAAW,qBAAqB;IACpC,KAAK,EAAE,aAAa,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,UAAU,GAAG,aAAa,CAAC;IACnC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACnC;AAED,MAAM,WAAW,UAAU;IACzB,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,aAAa,EAAE,CAAC,CAAC;IACjD,kBAAkB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;IAC9C,wBAAwB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,aAAa,EAAE,CAAC,CAAC;IAC3D,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,cAAc;IAC7B,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAC5B,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACnC,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IACtB,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IACzB,eAAe,CAAC,EAAE;QAChB,MAAM,CAAC,EAAE,MAAM,CAAC;KACjB,CAAC;CACH;AAED,MAAM,WAAW,eAAe;IAC9B,UAAU,EAAE,OAAO,CAAC;IACpB,MAAM,EAAE,eAAe,CAAC;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,YAAY,EAAE,aAAa,GAAG,MAAM,CAAC;IACrC,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;CACvB"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/yaml.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Small deterministic YAML reader for .prumo/pipeline.yaml.
|
|
3
|
+
* Supports comments, lists (`- item`), key-value pairs (`key: value`),
|
|
4
|
+
* nested indentation and valid JSON documents, with no external dependency.
|
|
5
|
+
*/
|
|
6
|
+
export declare function parseYaml(content: string): Record<string, unknown>;
|
|
7
|
+
//# sourceMappingURL=yaml.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"yaml.d.ts","sourceRoot":"","sources":["../src/yaml.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAoBH,wBAAgB,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAsFlE"}
|
package/dist/yaml.js
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Small deterministic YAML reader for .prumo/pipeline.yaml.
|
|
3
|
+
* Supports comments, lists (`- item`), key-value pairs (`key: value`),
|
|
4
|
+
* nested indentation and valid JSON documents, with no external dependency.
|
|
5
|
+
*/
|
|
6
|
+
function parseScalar(val) {
|
|
7
|
+
const trimmed = val.trim();
|
|
8
|
+
if (trimmed === 'true')
|
|
9
|
+
return true;
|
|
10
|
+
if (trimmed === 'false')
|
|
11
|
+
return false;
|
|
12
|
+
if (trimmed === 'null' || trimmed === '~')
|
|
13
|
+
return null;
|
|
14
|
+
if (/^-?\d+(\.\d+)?$/.test(trimmed))
|
|
15
|
+
return Number(trimmed);
|
|
16
|
+
if ((trimmed.startsWith('"') && trimmed.endsWith('"')) || (trimmed.startsWith("'") && trimmed.endsWith("'"))) {
|
|
17
|
+
return trimmed.slice(1, -1);
|
|
18
|
+
}
|
|
19
|
+
// Inline list: [a, b]
|
|
20
|
+
if (trimmed.startsWith('[') && trimmed.endsWith(']')) {
|
|
21
|
+
const inner = trimmed.slice(1, -1).trim();
|
|
22
|
+
if (!inner)
|
|
23
|
+
return [];
|
|
24
|
+
return inner.split(',').map((s) => parseScalar(s));
|
|
25
|
+
}
|
|
26
|
+
return trimmed;
|
|
27
|
+
}
|
|
28
|
+
export function parseYaml(content) {
|
|
29
|
+
const text = content.trim();
|
|
30
|
+
if (!text)
|
|
31
|
+
return {};
|
|
32
|
+
if (text.startsWith('{')) {
|
|
33
|
+
try {
|
|
34
|
+
return JSON.parse(text);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
// continua para parser linha a linha
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
const lines = text.split('\n');
|
|
41
|
+
const root = {};
|
|
42
|
+
const stack = [{ indent: -1, obj: root }];
|
|
43
|
+
for (let i = 0; i < lines.length; i++) {
|
|
44
|
+
const rawLine = lines[i];
|
|
45
|
+
// Remove inline comment if not in quotes
|
|
46
|
+
let line = rawLine;
|
|
47
|
+
const commentIdx = line.indexOf('#');
|
|
48
|
+
if (commentIdx >= 0) {
|
|
49
|
+
line = line.substring(0, commentIdx);
|
|
50
|
+
}
|
|
51
|
+
if (!line.trim())
|
|
52
|
+
continue;
|
|
53
|
+
const indent = line.search(/\S/);
|
|
54
|
+
const content = line.trim();
|
|
55
|
+
while (stack.length > 1 && stack[stack.length - 1].indent >= indent) {
|
|
56
|
+
stack.pop();
|
|
57
|
+
}
|
|
58
|
+
const current = stack[stack.length - 1].obj;
|
|
59
|
+
// Check array item: - value or - key: value
|
|
60
|
+
if (content.startsWith('- ')) {
|
|
61
|
+
const rest = content.slice(2).trim();
|
|
62
|
+
const targetArr = Array.isArray(current) ? current : [];
|
|
63
|
+
if (!Array.isArray(current)) {
|
|
64
|
+
// Parent should be array
|
|
65
|
+
}
|
|
66
|
+
if (rest.includes(':')) {
|
|
67
|
+
const colon = rest.indexOf(':');
|
|
68
|
+
const k = rest.substring(0, colon).trim();
|
|
69
|
+
const v = rest.substring(colon + 1).trim();
|
|
70
|
+
const child = {};
|
|
71
|
+
if (v)
|
|
72
|
+
child[k] = parseScalar(v);
|
|
73
|
+
else
|
|
74
|
+
child[k] = {};
|
|
75
|
+
targetArr.push(child);
|
|
76
|
+
stack.push({ indent, obj: child });
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
targetArr.push(parseScalar(rest));
|
|
80
|
+
}
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
// Key-value pair: key: value or key:
|
|
84
|
+
const colon = content.indexOf(':');
|
|
85
|
+
if (colon > 0) {
|
|
86
|
+
const key = content.substring(0, colon).trim();
|
|
87
|
+
const valStr = content.substring(colon + 1).trim();
|
|
88
|
+
if (valStr.length > 0) {
|
|
89
|
+
if (!Array.isArray(current)) {
|
|
90
|
+
current[key] = parseScalar(valStr);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
// Look ahead to see if next line is array item (- ...) or nested object
|
|
95
|
+
let isArrayNext = false;
|
|
96
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
97
|
+
const nextTrim = lines[j].trim();
|
|
98
|
+
if (!nextTrim || nextTrim.startsWith('#'))
|
|
99
|
+
continue;
|
|
100
|
+
if (nextTrim.startsWith('- '))
|
|
101
|
+
isArrayNext = true;
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
const child = isArrayNext ? [] : {};
|
|
105
|
+
if (!Array.isArray(current)) {
|
|
106
|
+
current[key] = child;
|
|
107
|
+
}
|
|
108
|
+
stack.push({ indent, obj: child });
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return root;
|
|
113
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,69 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ihelpee/prumo-plugin-pipeline",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"description": "
|
|
3
|
+
"version": "0.18.2",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Pipeline plugin for Prumo: session status, preflight actions, timeline markers, and board governance.",
|
|
6
|
+
"author": "Alex Gotardi <alexgotardi@gmail.com> (https://github.com/alexgotardi)",
|
|
7
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
8
|
+
"homepage": "https://github.com/ihelpee/prumo#readme",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/ihelpee/prumo.git"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/ihelpee/prumo/issues"
|
|
15
|
+
},
|
|
16
|
+
"maturity": "experimental",
|
|
17
|
+
"main": "./dist/index.js",
|
|
18
|
+
"types": "./dist/index.d.ts",
|
|
19
|
+
"exports": {
|
|
20
|
+
".": {
|
|
21
|
+
"types": "./dist/index.d.ts",
|
|
22
|
+
"import": "./dist/index.js"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"dist",
|
|
27
|
+
"package.json",
|
|
28
|
+
"README.md",
|
|
29
|
+
"LICENSE"
|
|
30
|
+
],
|
|
31
|
+
"publishConfig": {
|
|
32
|
+
"access": "public"
|
|
33
|
+
},
|
|
34
|
+
"verify": {
|
|
35
|
+
"script": "verify",
|
|
36
|
+
"fixtures": [
|
|
37
|
+
"fixtures/pipeline-scenarios"
|
|
38
|
+
]
|
|
39
|
+
},
|
|
40
|
+
"prumo": ">=0.12.0",
|
|
41
|
+
"contributes": {
|
|
42
|
+
"activate": "./dist/index.js",
|
|
43
|
+
"rules": "./dist/rules.js",
|
|
44
|
+
"cliCommands": "./dist/cli.js",
|
|
45
|
+
"harness": {
|
|
46
|
+
"skills": "./dist/skills.js"
|
|
47
|
+
},
|
|
48
|
+
"description": "Pipeline plugin for Prumo: session status, preflight actions, timeline markers, and board governance.",
|
|
49
|
+
"commands": [
|
|
50
|
+
"prumo pipeline session",
|
|
51
|
+
"prumo pipeline preflight"
|
|
52
|
+
]
|
|
53
|
+
},
|
|
54
|
+
"peerDependencies": {
|
|
55
|
+
"@ihelpee/prumo": "^0.18.2",
|
|
56
|
+
"@ihelpee/prumo-plugin-github-project": "0.18.2"
|
|
57
|
+
},
|
|
58
|
+
"dependencies": {
|
|
59
|
+
"@ihelpee/prumo-plugin-github-project": "0.18.2"
|
|
60
|
+
},
|
|
61
|
+
"devDependencies": {
|
|
62
|
+
"@ihelpee/prumo": "0.18.2",
|
|
63
|
+
"@ihelpee/prumo-plugin-typescript": "0.18.2"
|
|
64
|
+
},
|
|
65
|
+
"scripts": {
|
|
66
|
+
"build": "tsc -p tsconfig.json",
|
|
67
|
+
"verify": "prumo gate --root ."
|
|
68
|
+
}
|
|
6
69
|
}
|